From 0360831056bbaf8391cb6cea897c14a9653c1d7f Mon Sep 17 00:00:00 2001 From: midhun-aot Date: Tue, 12 Mar 2024 13:36:01 -0700 Subject: [PATCH] adding bcbox changes --- bcbox/.codeclimate.yml | 57 + bcbox/.dockerignore | 42 + bcbox/.editorconfig | 23 + bcbox/.gitattributes | 8 + bcbox/.gitignore | 42 + bcbox/.vscode/extensions.json | 14 + bcbox/.vscode/launch.json | 16 + bcbox/.vscode/settings.json | 26 + bcbox/CODE-OF-CONDUCT.md | 46 + bcbox/COMPLIANCE.yaml | 11 + bcbox/CONTRIBUTING.md | 9 + bcbox/Dockerfile | 49 + bcbox/LICENSE | 201 + bcbox/LICENSE.md | 202 + bcbox/README.md | 87 + bcbox/SECURITY.md | 52 + bcbox/_config.yml | 1 + bcbox/app/.eslintrc.js | 45 + bcbox/app/.gitignore | 43 + bcbox/app/.prettierignore | 29 + bcbox/app/.prettierrc | 13 + bcbox/app/app.ts | 149 + bcbox/app/bin/www.ts | 72 + .../config/custom-environment-variables.json | 24 + bcbox/app/config/default.json | 21 + bcbox/app/config/idplist-default.json | 23 + bcbox/app/config/production.json | 1 + bcbox/app/config/test.json | 22 + bcbox/app/frontend-utils.ts | 158 + bcbox/app/jest.config.js | 6 + bcbox/app/lcov-fix.ts | 16 + bcbox/app/package-lock.json | 7571 ++++++++++ bcbox/app/package.json | 56 + bcbox/app/src/components/log.ts | 106 + bcbox/app/src/components/utils.ts | 62 + bcbox/app/tests/common/helper.ts | 51 + bcbox/app/tests/unit/components/log.spec.ts | 39 + bcbox/app/tests/unit/components/utils.spec.ts | 21 + bcbox/app/tsconfig.json | 99 + bcbox/bcgovpubcode.yaml | 58 + bcbox/charts/bcbox/.helmignore | 23 + bcbox/charts/bcbox/Chart.yaml | 42 + bcbox/charts/bcbox/README.md | 81 + bcbox/charts/bcbox/templates/NOTES.txt | 24 + bcbox/charts/bcbox/templates/_helpers.tpl | 77 + bcbox/charts/bcbox/templates/configmap.yaml | 12 + .../bcbox/templates/deploymentconfig.yaml | 69 + bcbox/charts/bcbox/templates/hpa.yaml | 37 + .../charts/bcbox/templates/networkpolicy.yaml | 22 + bcbox/charts/bcbox/templates/route.yaml | 28 + bcbox/charts/bcbox/templates/service.yaml | 16 + .../bcbox/templates/serviceaccount.yaml | 13 + bcbox/charts/bcbox/values.yaml | 126 + bcbox/frontend/.eslintrc.js | 56 + bcbox/frontend/.gitignore | 28 + bcbox/frontend/.prettierignore | 29 + bcbox/frontend/.prettierrc | 13 + bcbox/frontend/README.md | 46 + bcbox/frontend/env.d.ts | 1 + bcbox/frontend/index.html | 13 + bcbox/frontend/package-lock.json | 12111 ++++++++++++++++ bcbox/frontend/package.json | 73 + bcbox/frontend/public/favicon.ico | Bin 0 -> 22486 bytes bcbox/frontend/src/App.vue | 58 + bcbox/frontend/src/assets/base.css | 86 + bcbox/frontend/src/assets/images/bc_logo.svg | 1 + .../src/assets/images/bc_logo_print.svg | 1 + .../src/assets/images/bc_logo_square.svg | 22 + bcbox/frontend/src/assets/images/bcboxy.png | Bin 0 -> 29806 bytes bcbox/frontend/src/assets/images/home_1.png | Bin 0 -> 130458 bytes bcbox/frontend/src/assets/images/home_2.png | Bin 0 -> 38646 bytes bcbox/frontend/src/assets/images/home_3.png | Bin 0 -> 107203 bytes bcbox/frontend/src/assets/logo.svg | 1 + bcbox/frontend/src/assets/main.scss | 186 + bcbox/frontend/src/assets/primevue.scss | 121 + bcbox/frontend/src/assets/variables.scss | 9 + .../components/bucket/BucketConfigForm.vue | 176 + .../src/components/bucket/BucketList.vue | 148 + .../components/bucket/BucketPermission.vue | 207 + .../bucket/BucketPermissionAddUser.vue | 36 + .../src/components/bucket/BucketSidebar.vue | 130 + .../src/components/bucket/BucketTable.vue | 200 + bcbox/frontend/src/components/bucket/index.ts | 6 + .../src/components/common/SyncButton.vue | 119 + bcbox/frontend/src/components/common/index.ts | 1 + .../src/components/form/CopyToClipboard.vue | 48 + .../frontend/src/components/form/GridRow.vue | 40 + .../frontend/src/components/form/Password.vue | 47 + .../src/components/form/SearchUsers.vue | 182 + .../src/components/form/TextInput.vue | 47 + .../src/components/guards/RequireAuth.vue | 37 + .../components/guards/RequirePublicOrAuth.vue | 51 + bcbox/frontend/src/components/guards/index.ts | 2 + .../src/components/layout/AppLayout.vue | 47 + .../frontend/src/components/layout/Footer.vue | 78 + .../frontend/src/components/layout/Header.vue | 58 + .../src/components/layout/LoginButton.vue | 46 + .../frontend/src/components/layout/Navbar.vue | 105 + .../src/components/layout/ProgressLoader.vue | 33 + .../src/components/layout/Spinner.vue | 16 + bcbox/frontend/src/components/layout/index.ts | 7 + .../components/object/DeleteObjectButton.vue | 97 + .../object/DownloadObjectButton.vue | 80 + .../src/components/object/ObjectAccess.vue | 72 + .../components/object/ObjectFileDetails.vue | 255 + .../src/components/object/ObjectFilters.vue | 194 + .../src/components/object/ObjectList.vue | 147 + .../src/components/object/ObjectMetadata.vue | 154 + .../object/ObjectMetadataTagForm.vue | 216 + .../components/object/ObjectPermission.vue | 222 + .../object/ObjectPermissionAddUser.vue | 36 + .../components/object/ObjectProperties.vue | 106 + .../components/object/ObjectPublicToggle.vue | 53 + .../src/components/object/ObjectSidebar.vue | 119 + .../src/components/object/ObjectTable.vue | 301 + .../src/components/object/ObjectTag.vue | 155 + .../src/components/object/ObjectUpload.vue | 191 + .../components/object/ObjectUploadBasic.vue | 159 + .../components/object/ObjectUploadFile.vue | 135 + .../src/components/object/ObjectVersion.vue | 211 + bcbox/frontend/src/components/object/index.ts | 19 + .../object/share/ShareLinkContent.vue | 56 + .../object/share/ShareObjectButton.vue | 108 + .../src/components/object/share/index.ts | 2 + bcbox/frontend/src/composables/useAlert.ts | 16 + bcbox/frontend/src/interfaces/IAudit.ts | 6 + bcbox/frontend/src/interfaces/IChangeEvent.ts | 3 + bcbox/frontend/src/interfaces/IInputEvent.ts | 3 + bcbox/frontend/src/interfaces/index.ts | 3 + bcbox/frontend/src/lib/primevue/index.ts | 31 + bcbox/frontend/src/lib/primevue/useToast.ts | 29 + bcbox/frontend/src/main.ts | 61 + bcbox/frontend/src/router/index.ts | 157 + bcbox/frontend/src/services/authService.ts | 118 + bcbox/frontend/src/services/bucketService.ts | 58 + bcbox/frontend/src/services/configService.ts | 65 + bcbox/frontend/src/services/index.ts | 7 + bcbox/frontend/src/services/interceptors.ts | 35 + bcbox/frontend/src/services/objectService.ts | 370 + .../src/services/permissionService.ts | 106 + bcbox/frontend/src/services/userService.ts | 44 + bcbox/frontend/src/services/versionService.ts | 37 + bcbox/frontend/src/shims.d.ts | 5 + bcbox/frontend/src/store/appStore.ts | 97 + bcbox/frontend/src/store/authStore.ts | 128 + bcbox/frontend/src/store/bucketStore.ts | 131 + bcbox/frontend/src/store/configStore.ts | 44 + bcbox/frontend/src/store/index.ts | 11 + bcbox/frontend/src/store/metadataStore.ts | 116 + bcbox/frontend/src/store/navStore.ts | 63 + bcbox/frontend/src/store/objectStore.ts | 285 + bcbox/frontend/src/store/permissionStore.ts | 347 + bcbox/frontend/src/store/tagStore.ts | 112 + bcbox/frontend/src/store/userStore.ts | 87 + bcbox/frontend/src/store/versionStore.ts | 150 + bcbox/frontend/src/types/Bucket.ts | 13 + bcbox/frontend/src/types/BucketPermission.ts | 8 + bcbox/frontend/src/types/COMSObject.ts | 10 + .../src/types/COMSObjectPermission.ts | 8 + bcbox/frontend/src/types/IdentityProvider.ts | 7 + bcbox/frontend/src/types/Metadata.ts | 7 + bcbox/frontend/src/types/MetadataPair.ts | 4 + bcbox/frontend/src/types/Permission.ts | 6 + bcbox/frontend/src/types/Tag.ts | 4 + bcbox/frontend/src/types/Tagging.ts | 7 + bcbox/frontend/src/types/User.ts | 14 + bcbox/frontend/src/types/UserPermissions.ts | 11 + bcbox/frontend/src/types/Version.ts | 9 + bcbox/frontend/src/types/index.ts | 30 + .../options/BucketAddPermissionsOptions.ts | 4 + .../options/BucketDeletePermissionsOptions.ts | 4 + .../options/BucketGetPermissionsOptions.ts | 4 + .../options/BucketSearchPermissionsOptions.ts | 6 + .../src/types/options/GetMetadataOptions.ts | 3 + .../types/options/GetObjectTaggingOptions.ts | 3 + .../options/GetVersionMetadataOptions.ts | 3 + .../src/types/options/GetVersionOptions.ts | 3 + .../types/options/GetVersionTaggingOptions.ts | 3 + .../options/ObjectAddPermissionsOptions.ts | 4 + .../options/ObjectDeletePermissionsOptions.ts | 4 + .../options/ObjectGetPermissionsOptions.ts | 4 + .../options/ObjectSearchPermissionsOptions.ts | 7 + .../src/types/options/SearchBucketsOptions.ts | 6 + .../src/types/options/SearchObjectsOptions.ts | 12 + .../src/types/options/SearchUsersOptions.ts | 8 + bcbox/frontend/src/utils/constants.ts | 63 + bcbox/frontend/src/utils/enums.ts | 13 + bcbox/frontend/src/utils/formatters.ts | 39 + bcbox/frontend/src/utils/utils.ts | 118 + bcbox/frontend/src/views/DeveloperView.vue | 46 + bcbox/frontend/src/views/Forbidden.vue | 5 + bcbox/frontend/src/views/GenericView.vue | 5 + bcbox/frontend/src/views/HomeView.vue | 230 + bcbox/frontend/src/views/NotFound.vue | 5 + .../src/views/detail/DetailObjectsView.vue | 59 + .../src/views/list/ListBucketsView.vue | 7 + .../src/views/list/ListObjectsView.vue | 92 + .../src/views/oidc/OidcCallbackView.vue | 37 + .../frontend/src/views/oidc/OidcLoginView.vue | 29 + .../src/views/oidc/OidcLogoutView.vue | 34 + bcbox/frontend/tests/unit/App.spec.ts | 7 + .../bucket/BucketConfigForm.spec.ts | 80 + .../unit/components/bucket/BucketList.spec.ts | 70 + .../bucket/BucketPermission.spec.ts | 55 + .../bucket/BucketPermissionAddUser.spec.ts | 51 + .../components/bucket/BucketSidebar.spec.ts | 137 + .../components/bucket/BucketTable.spec.ts | 59 + .../unit/components/form/TextInput.spec.ts | 79 + .../unit/components/layout/Footer.spec.ts | 53 + .../unit/components/layout/Header.spec.ts | 43 + .../components/layout/LoginButton.spec.ts | 152 + .../unit/components/layout/Navbar.spec.ts | 83 + .../components/layout/ProgressLoader.spec.ts | 24 + .../unit/components/layout/Spinner.spec.ts | 23 + .../tests/unit/store/appStore.spec.ts | 48 + .../tests/unit/store/authStore.spec.ts | 72 + .../tests/unit/store/bucketStore.spec.ts | 178 + .../tests/unit/store/configStore.spec.ts | 56 + .../tests/unit/store/metadataStore.spec.ts | 116 + .../tests/unit/store/objectStore.spec.ts | 282 + .../tests/unit/store/tagStore.spec.ts | 98 + .../tests/unit/store/userStore.spec.ts | 143 + .../tests/unit/store/versionStore.spec.ts | 278 + .../tests/unit/utils/formatters.spec.ts | 14 + bcbox/frontend/tsconfig.config.json | 12 + bcbox/frontend/tsconfig.json | 125 + bcbox/frontend/vite.config.ts | 46 + bcbox/frontend/volar.config.js | 14 + comsapi/.codeclimate.yml | 53 + comsapi/.dockerignore | 42 + comsapi/.editorconfig | 23 + comsapi/.gitattributes | 8 + comsapi/.gitignore | 41 + comsapi/.vscode/extensions.json | 11 + comsapi/.vscode/launch.json | 17 + comsapi/.vscode/settings.json | 23 + comsapi/CODE-OF-CONDUCT.md | 46 + comsapi/COMPLIANCE.yaml | 11 + comsapi/CONTRIBUTING.md | 9 + comsapi/Dockerfile | 20 + comsapi/LICENSE | 201 + comsapi/README.md | 81 + comsapi/SECURITY.md | 52 + comsapi/_config.yaml | 1 + comsapi/app/.editorconfig | 23 + comsapi/app/.eslintignore | 4 + comsapi/app/.eslintrc.js | 41 + comsapi/app/.prettierrc | 4 + comsapi/app/README.md | 312 + comsapi/app/app.js | 247 + comsapi/app/bin/www | 71 + .../config/custom-environment-variables.json | 43 + comsapi/app/config/default.json | 26 + comsapi/app/config/production.json | 1 + comsapi/app/config/test.json | 5 + comsapi/app/jest.config.js | 16 + comsapi/app/knexfile.js | 65 + comsapi/app/lcov-fix.js | 15 + comsapi/app/nodemon.json | 9 + comsapi/app/package-lock.json | 10857 ++++++++++++++ comsapi/app/package.json | 64 + comsapi/app/src/components/constants.js | 90 + comsapi/app/src/components/crypt.js | 84 + comsapi/app/src/components/errorToProblem.js | 50 + comsapi/app/src/components/log.js | 102 + comsapi/app/src/components/queueManager.js | 130 + comsapi/app/src/components/utils.js | 442 + comsapi/app/src/controllers/bucket.js | 257 + .../app/src/controllers/bucketPermission.js | 123 + comsapi/app/src/controllers/index.js | 11 + comsapi/app/src/controllers/metadata.js | 36 + comsapi/app/src/controllers/object.js | 1062 ++ .../app/src/controllers/objectPermission.js | 120 + comsapi/app/src/controllers/sync.js | 82 + comsapi/app/src/controllers/tag.js | 37 + comsapi/app/src/controllers/user.js | 60 + comsapi/app/src/controllers/version.js | 75 + comsapi/app/src/db/.editorconfig | 2 + comsapi/app/src/db/dataConnection.js | 170 + .../db/migrations/20220130133615_000-init.js | 169 + .../migrations/20220516000000_001-version.js | 90 + .../20220627000000_002-metadata-tags.js | 134 + .../20221014000000_003-multi-bucket.js | 81 + ...0230306000000_004-s3-version-tag-expand.js | 36 + .../20230420000000_005-permission-indexes.js | 27 + .../20230427000000_006-metadata-fix.js | 23 + .../20230503000000_007-default-updatedat.js | 69 + .../20230518000000_008-filename-etag.js | 32 + ...0810000000_009-user-identity-constraint.js | 28 + .../20230814000000_010-sync-queue.js | 63 + .../20231010000000_011-metadata-text.js | 17 + comsapi/app/src/db/models/index.js | 20 + comsapi/app/src/db/models/jsonSchema.js | 6 + comsapi/app/src/db/models/mixins/encrypt.js | 70 + comsapi/app/src/db/models/mixins/index.js | 6 + .../app/src/db/models/mixins/timestamps.js | 27 + comsapi/app/src/db/models/tables/bucket.js | 98 + .../src/db/models/tables/bucketPermission.js | 75 + .../src/db/models/tables/identityProvider.js | 56 + comsapi/app/src/db/models/tables/metadata.js | 87 + .../app/src/db/models/tables/objectModel.js | 201 + .../src/db/models/tables/objectPermission.js | 83 + .../app/src/db/models/tables/objectQueue.js | 53 + .../app/src/db/models/tables/permission.js | 56 + comsapi/app/src/db/models/tables/tag.js | 88 + comsapi/app/src/db/models/tables/user.js | 107 + comsapi/app/src/db/models/tables/version.js | 100 + .../src/db/models/tables/versionMetadata.js | 65 + .../app/src/db/models/tables/versionTag.js | 65 + comsapi/app/src/db/models/utils.js | 88 + comsapi/app/src/db/stamps.js | 8 + comsapi/app/src/docs/docs.js | 23 + comsapi/app/src/docs/v1.api-spec.yaml | 2805 ++++ comsapi/app/src/middleware/authentication.js | 114 + comsapi/app/src/middleware/authorization.js | 148 + comsapi/app/src/middleware/featureToggle.js | 63 + comsapi/app/src/middleware/upload.js | 69 + comsapi/app/src/middleware/validation.js | 34 + comsapi/app/src/routes/v1/bucket.js | 53 + comsapi/app/src/routes/v1/docs.js | 34 + comsapi/app/src/routes/v1/index.js | 50 + comsapi/app/src/routes/v1/metadata.js | 16 + comsapi/app/src/routes/v1/object.js | 98 + .../routes/v1/permission/bucketPermission.js | 33 + comsapi/app/src/routes/v1/permission/index.js | 19 + .../routes/v1/permission/objectPermission.js | 33 + comsapi/app/src/routes/v1/sync.js | 21 + comsapi/app/src/routes/v1/tag.js | 17 + comsapi/app/src/routes/v1/user.js | 21 + comsapi/app/src/routes/v1/version.js | 21 + comsapi/app/src/services/bucket.js | 238 + comsapi/app/src/services/bucketPermission.js | 143 + comsapi/app/src/services/index.js | 13 + comsapi/app/src/services/metadata.js | 322 + comsapi/app/src/services/object.js | 218 + comsapi/app/src/services/objectPermission.js | 152 + comsapi/app/src/services/objectQueue.js | 84 + comsapi/app/src/services/storage.js | 523 + comsapi/app/src/services/sync.js | 470 + comsapi/app/src/services/tag.js | 346 + comsapi/app/src/services/user.js | 281 + comsapi/app/src/services/version.js | 327 + comsapi/app/src/validators/bucket.js | 80 + .../app/src/validators/bucketPermission.js | 68 + comsapi/app/src/validators/common.js | 88 + comsapi/app/src/validators/index.js | 10 + comsapi/app/src/validators/metadata.js | 15 + comsapi/app/src/validators/object.js | 199 + .../app/src/validators/objectPermission.js | 68 + comsapi/app/src/validators/tag.js | 20 + comsapi/app/src/validators/user.js | 35 + comsapi/app/src/validators/version.js | 31 + comsapi/app/tests/common/helper.js | 69 + .../app/tests/unit/components/crypt.spec.js | 121 + .../unit/components/errorToProblem.spec.js | 116 + comsapi/app/tests/unit/components/log.spec.js | 36 + .../unit/components/queueManager.spec.js | 333 + .../app/tests/unit/components/utils.spec.js | 577 + .../app/tests/unit/controllers/bucket.spec.js | 411 + .../tests/unit/controllers/metadata.spec.js | 78 + .../app/tests/unit/controllers/object.spec.js | 619 + .../unit/controllers/objectPermission.spec.js | 161 + .../app/tests/unit/controllers/sync.spec.js | 136 + .../app/tests/unit/controllers/tag.spec.js | 86 + .../app/tests/unit/db/models/utils.spec.js | 133 + .../unit/middleware/authentication.spec.js | 251 + .../unit/middleware/authorization.spec.js | 411 + .../unit/middleware/featureToggle.spec.js | 98 + .../app/tests/unit/middleware/upload.spec.js | 64 + .../tests/unit/middleware/validation.spec.js | 64 + comsapi/app/tests/unit/routes/v1.spec.js | 34 + comsapi/app/tests/unit/routes/v1/docs.spec.js | 46 + .../app/tests/unit/routes/v1/object.spec.js | 29 + comsapi/app/tests/unit/routes/v1/user.spec.js | 91 + .../app/tests/unit/services/bucket.spec.js | 169 + .../unit/services/bucketPermission.spec.js | 116 + .../app/tests/unit/services/metadata.spec.js | 232 + .../app/tests/unit/services/object.spec.js | 172 + .../unit/services/objectPermission.spec.js | 111 + .../tests/unit/services/objectQueue.spec.js | 88 + .../app/tests/unit/services/storage.spec.js | 1055 ++ comsapi/app/tests/unit/services/sync.spec.js | 1310 ++ comsapi/app/tests/unit/services/tag.spec.js | 302 + comsapi/app/tests/unit/services/user.spec.js | 382 + .../app/tests/unit/services/version.spec.js | 210 + .../app/tests/unit/validators/bucket.spec.js | 299 + .../unit/validators/bucketPermission.spec.js | 187 + .../app/tests/unit/validators/common.spec.js | 427 + .../tests/unit/validators/metadata.spec.js | 15 + .../app/tests/unit/validators/object.spec.js | 594 + .../unit/validators/objectPermission.spec.js | 195 + comsapi/app/tests/unit/validators/tag.spec.js | 21 + .../app/tests/unit/validators/user.spec.js | 187 + .../app/tests/unit/validators/version.spec.js | 57 + comsapi/bcgovpubcode.yml | 50 + comsapi/charts/coms/.helmignore | 23 + comsapi/charts/coms/Chart.yaml | 48 + comsapi/charts/coms/README.md | 81 + comsapi/charts/coms/templates/NOTES.txt | 24 + comsapi/charts/coms/templates/_helpers.tpl | 77 + comsapi/charts/coms/templates/configmap.yaml | 12 + .../coms/templates/deploymentconfig.yaml | 175 + comsapi/charts/coms/templates/hpa.yaml | 37 + .../charts/coms/templates/networkpolicy.yaml | 57 + comsapi/charts/coms/templates/route.yaml | 28 + comsapi/charts/coms/templates/secret.yaml | 80 + comsapi/charts/coms/templates/service.yaml | 16 + .../charts/coms/templates/serviceaccount.yaml | 13 + comsapi/charts/coms/values.yaml | 188 + comsapi/k6/README.md | 14 + comsapi/k6/createObject.js | 73 + comsapi/k6/readObject.js | 51 + comsapi/k6/searchObject.js | 45 + 413 files changed, 70163 insertions(+) create mode 100644 bcbox/.codeclimate.yml create mode 100644 bcbox/.dockerignore create mode 100644 bcbox/.editorconfig create mode 100644 bcbox/.gitattributes create mode 100644 bcbox/.gitignore create mode 100644 bcbox/.vscode/extensions.json create mode 100644 bcbox/.vscode/launch.json create mode 100644 bcbox/.vscode/settings.json create mode 100644 bcbox/CODE-OF-CONDUCT.md create mode 100644 bcbox/COMPLIANCE.yaml create mode 100644 bcbox/CONTRIBUTING.md create mode 100644 bcbox/Dockerfile create mode 100644 bcbox/LICENSE create mode 100644 bcbox/LICENSE.md create mode 100644 bcbox/README.md create mode 100644 bcbox/SECURITY.md create mode 100644 bcbox/_config.yml create mode 100644 bcbox/app/.eslintrc.js create mode 100644 bcbox/app/.gitignore create mode 100644 bcbox/app/.prettierignore create mode 100644 bcbox/app/.prettierrc create mode 100644 bcbox/app/app.ts create mode 100644 bcbox/app/bin/www.ts create mode 100644 bcbox/app/config/custom-environment-variables.json create mode 100644 bcbox/app/config/default.json create mode 100644 bcbox/app/config/idplist-default.json create mode 100644 bcbox/app/config/production.json create mode 100644 bcbox/app/config/test.json create mode 100644 bcbox/app/frontend-utils.ts create mode 100644 bcbox/app/jest.config.js create mode 100644 bcbox/app/lcov-fix.ts create mode 100644 bcbox/app/package-lock.json create mode 100644 bcbox/app/package.json create mode 100644 bcbox/app/src/components/log.ts create mode 100644 bcbox/app/src/components/utils.ts create mode 100644 bcbox/app/tests/common/helper.ts create mode 100644 bcbox/app/tests/unit/components/log.spec.ts create mode 100644 bcbox/app/tests/unit/components/utils.spec.ts create mode 100644 bcbox/app/tsconfig.json create mode 100644 bcbox/bcgovpubcode.yaml create mode 100644 bcbox/charts/bcbox/.helmignore create mode 100644 bcbox/charts/bcbox/Chart.yaml create mode 100644 bcbox/charts/bcbox/README.md create mode 100644 bcbox/charts/bcbox/templates/NOTES.txt create mode 100644 bcbox/charts/bcbox/templates/_helpers.tpl create mode 100644 bcbox/charts/bcbox/templates/configmap.yaml create mode 100644 bcbox/charts/bcbox/templates/deploymentconfig.yaml create mode 100644 bcbox/charts/bcbox/templates/hpa.yaml create mode 100644 bcbox/charts/bcbox/templates/networkpolicy.yaml create mode 100644 bcbox/charts/bcbox/templates/route.yaml create mode 100644 bcbox/charts/bcbox/templates/service.yaml create mode 100644 bcbox/charts/bcbox/templates/serviceaccount.yaml create mode 100644 bcbox/charts/bcbox/values.yaml create mode 100644 bcbox/frontend/.eslintrc.js create mode 100644 bcbox/frontend/.gitignore create mode 100644 bcbox/frontend/.prettierignore create mode 100644 bcbox/frontend/.prettierrc create mode 100644 bcbox/frontend/README.md create mode 100644 bcbox/frontend/env.d.ts create mode 100644 bcbox/frontend/index.html create mode 100644 bcbox/frontend/package-lock.json create mode 100644 bcbox/frontend/package.json create mode 100644 bcbox/frontend/public/favicon.ico create mode 100644 bcbox/frontend/src/App.vue create mode 100644 bcbox/frontend/src/assets/base.css create mode 100644 bcbox/frontend/src/assets/images/bc_logo.svg create mode 100644 bcbox/frontend/src/assets/images/bc_logo_print.svg create mode 100644 bcbox/frontend/src/assets/images/bc_logo_square.svg create mode 100644 bcbox/frontend/src/assets/images/bcboxy.png create mode 100644 bcbox/frontend/src/assets/images/home_1.png create mode 100644 bcbox/frontend/src/assets/images/home_2.png create mode 100644 bcbox/frontend/src/assets/images/home_3.png create mode 100644 bcbox/frontend/src/assets/logo.svg create mode 100644 bcbox/frontend/src/assets/main.scss create mode 100644 bcbox/frontend/src/assets/primevue.scss create mode 100644 bcbox/frontend/src/assets/variables.scss create mode 100644 bcbox/frontend/src/components/bucket/BucketConfigForm.vue create mode 100644 bcbox/frontend/src/components/bucket/BucketList.vue create mode 100644 bcbox/frontend/src/components/bucket/BucketPermission.vue create mode 100644 bcbox/frontend/src/components/bucket/BucketPermissionAddUser.vue create mode 100644 bcbox/frontend/src/components/bucket/BucketSidebar.vue create mode 100644 bcbox/frontend/src/components/bucket/BucketTable.vue create mode 100644 bcbox/frontend/src/components/bucket/index.ts create mode 100644 bcbox/frontend/src/components/common/SyncButton.vue create mode 100644 bcbox/frontend/src/components/common/index.ts create mode 100644 bcbox/frontend/src/components/form/CopyToClipboard.vue create mode 100644 bcbox/frontend/src/components/form/GridRow.vue create mode 100644 bcbox/frontend/src/components/form/Password.vue create mode 100644 bcbox/frontend/src/components/form/SearchUsers.vue create mode 100644 bcbox/frontend/src/components/form/TextInput.vue create mode 100644 bcbox/frontend/src/components/guards/RequireAuth.vue create mode 100644 bcbox/frontend/src/components/guards/RequirePublicOrAuth.vue create mode 100644 bcbox/frontend/src/components/guards/index.ts create mode 100644 bcbox/frontend/src/components/layout/AppLayout.vue create mode 100644 bcbox/frontend/src/components/layout/Footer.vue create mode 100644 bcbox/frontend/src/components/layout/Header.vue create mode 100644 bcbox/frontend/src/components/layout/LoginButton.vue create mode 100644 bcbox/frontend/src/components/layout/Navbar.vue create mode 100644 bcbox/frontend/src/components/layout/ProgressLoader.vue create mode 100644 bcbox/frontend/src/components/layout/Spinner.vue create mode 100644 bcbox/frontend/src/components/layout/index.ts create mode 100644 bcbox/frontend/src/components/object/DeleteObjectButton.vue create mode 100644 bcbox/frontend/src/components/object/DownloadObjectButton.vue create mode 100644 bcbox/frontend/src/components/object/ObjectAccess.vue create mode 100644 bcbox/frontend/src/components/object/ObjectFileDetails.vue create mode 100644 bcbox/frontend/src/components/object/ObjectFilters.vue create mode 100644 bcbox/frontend/src/components/object/ObjectList.vue create mode 100644 bcbox/frontend/src/components/object/ObjectMetadata.vue create mode 100644 bcbox/frontend/src/components/object/ObjectMetadataTagForm.vue create mode 100644 bcbox/frontend/src/components/object/ObjectPermission.vue create mode 100644 bcbox/frontend/src/components/object/ObjectPermissionAddUser.vue create mode 100644 bcbox/frontend/src/components/object/ObjectProperties.vue create mode 100644 bcbox/frontend/src/components/object/ObjectPublicToggle.vue create mode 100644 bcbox/frontend/src/components/object/ObjectSidebar.vue create mode 100644 bcbox/frontend/src/components/object/ObjectTable.vue create mode 100644 bcbox/frontend/src/components/object/ObjectTag.vue create mode 100644 bcbox/frontend/src/components/object/ObjectUpload.vue create mode 100644 bcbox/frontend/src/components/object/ObjectUploadBasic.vue create mode 100644 bcbox/frontend/src/components/object/ObjectUploadFile.vue create mode 100644 bcbox/frontend/src/components/object/ObjectVersion.vue create mode 100644 bcbox/frontend/src/components/object/index.ts create mode 100644 bcbox/frontend/src/components/object/share/ShareLinkContent.vue create mode 100644 bcbox/frontend/src/components/object/share/ShareObjectButton.vue create mode 100644 bcbox/frontend/src/components/object/share/index.ts create mode 100644 bcbox/frontend/src/composables/useAlert.ts create mode 100644 bcbox/frontend/src/interfaces/IAudit.ts create mode 100644 bcbox/frontend/src/interfaces/IChangeEvent.ts create mode 100644 bcbox/frontend/src/interfaces/IInputEvent.ts create mode 100644 bcbox/frontend/src/interfaces/index.ts create mode 100644 bcbox/frontend/src/lib/primevue/index.ts create mode 100644 bcbox/frontend/src/lib/primevue/useToast.ts create mode 100644 bcbox/frontend/src/main.ts create mode 100644 bcbox/frontend/src/router/index.ts create mode 100644 bcbox/frontend/src/services/authService.ts create mode 100644 bcbox/frontend/src/services/bucketService.ts create mode 100644 bcbox/frontend/src/services/configService.ts create mode 100644 bcbox/frontend/src/services/index.ts create mode 100644 bcbox/frontend/src/services/interceptors.ts create mode 100644 bcbox/frontend/src/services/objectService.ts create mode 100644 bcbox/frontend/src/services/permissionService.ts create mode 100644 bcbox/frontend/src/services/userService.ts create mode 100644 bcbox/frontend/src/services/versionService.ts create mode 100644 bcbox/frontend/src/shims.d.ts create mode 100644 bcbox/frontend/src/store/appStore.ts create mode 100644 bcbox/frontend/src/store/authStore.ts create mode 100644 bcbox/frontend/src/store/bucketStore.ts create mode 100644 bcbox/frontend/src/store/configStore.ts create mode 100644 bcbox/frontend/src/store/index.ts create mode 100644 bcbox/frontend/src/store/metadataStore.ts create mode 100644 bcbox/frontend/src/store/navStore.ts create mode 100644 bcbox/frontend/src/store/objectStore.ts create mode 100644 bcbox/frontend/src/store/permissionStore.ts create mode 100644 bcbox/frontend/src/store/tagStore.ts create mode 100644 bcbox/frontend/src/store/userStore.ts create mode 100644 bcbox/frontend/src/store/versionStore.ts create mode 100644 bcbox/frontend/src/types/Bucket.ts create mode 100644 bcbox/frontend/src/types/BucketPermission.ts create mode 100644 bcbox/frontend/src/types/COMSObject.ts create mode 100644 bcbox/frontend/src/types/COMSObjectPermission.ts create mode 100644 bcbox/frontend/src/types/IdentityProvider.ts create mode 100644 bcbox/frontend/src/types/Metadata.ts create mode 100644 bcbox/frontend/src/types/MetadataPair.ts create mode 100644 bcbox/frontend/src/types/Permission.ts create mode 100644 bcbox/frontend/src/types/Tag.ts create mode 100644 bcbox/frontend/src/types/Tagging.ts create mode 100644 bcbox/frontend/src/types/User.ts create mode 100644 bcbox/frontend/src/types/UserPermissions.ts create mode 100644 bcbox/frontend/src/types/Version.ts create mode 100644 bcbox/frontend/src/types/index.ts create mode 100644 bcbox/frontend/src/types/options/BucketAddPermissionsOptions.ts create mode 100644 bcbox/frontend/src/types/options/BucketDeletePermissionsOptions.ts create mode 100644 bcbox/frontend/src/types/options/BucketGetPermissionsOptions.ts create mode 100644 bcbox/frontend/src/types/options/BucketSearchPermissionsOptions.ts create mode 100644 bcbox/frontend/src/types/options/GetMetadataOptions.ts create mode 100644 bcbox/frontend/src/types/options/GetObjectTaggingOptions.ts create mode 100644 bcbox/frontend/src/types/options/GetVersionMetadataOptions.ts create mode 100644 bcbox/frontend/src/types/options/GetVersionOptions.ts create mode 100644 bcbox/frontend/src/types/options/GetVersionTaggingOptions.ts create mode 100644 bcbox/frontend/src/types/options/ObjectAddPermissionsOptions.ts create mode 100644 bcbox/frontend/src/types/options/ObjectDeletePermissionsOptions.ts create mode 100644 bcbox/frontend/src/types/options/ObjectGetPermissionsOptions.ts create mode 100644 bcbox/frontend/src/types/options/ObjectSearchPermissionsOptions.ts create mode 100644 bcbox/frontend/src/types/options/SearchBucketsOptions.ts create mode 100644 bcbox/frontend/src/types/options/SearchObjectsOptions.ts create mode 100644 bcbox/frontend/src/types/options/SearchUsersOptions.ts create mode 100644 bcbox/frontend/src/utils/constants.ts create mode 100644 bcbox/frontend/src/utils/enums.ts create mode 100644 bcbox/frontend/src/utils/formatters.ts create mode 100644 bcbox/frontend/src/utils/utils.ts create mode 100644 bcbox/frontend/src/views/DeveloperView.vue create mode 100644 bcbox/frontend/src/views/Forbidden.vue create mode 100644 bcbox/frontend/src/views/GenericView.vue create mode 100644 bcbox/frontend/src/views/HomeView.vue create mode 100644 bcbox/frontend/src/views/NotFound.vue create mode 100644 bcbox/frontend/src/views/detail/DetailObjectsView.vue create mode 100644 bcbox/frontend/src/views/list/ListBucketsView.vue create mode 100644 bcbox/frontend/src/views/list/ListObjectsView.vue create mode 100644 bcbox/frontend/src/views/oidc/OidcCallbackView.vue create mode 100644 bcbox/frontend/src/views/oidc/OidcLoginView.vue create mode 100644 bcbox/frontend/src/views/oidc/OidcLogoutView.vue create mode 100644 bcbox/frontend/tests/unit/App.spec.ts create mode 100644 bcbox/frontend/tests/unit/components/bucket/BucketConfigForm.spec.ts create mode 100644 bcbox/frontend/tests/unit/components/bucket/BucketList.spec.ts create mode 100644 bcbox/frontend/tests/unit/components/bucket/BucketPermission.spec.ts create mode 100644 bcbox/frontend/tests/unit/components/bucket/BucketPermissionAddUser.spec.ts create mode 100644 bcbox/frontend/tests/unit/components/bucket/BucketSidebar.spec.ts create mode 100644 bcbox/frontend/tests/unit/components/bucket/BucketTable.spec.ts create mode 100644 bcbox/frontend/tests/unit/components/form/TextInput.spec.ts create mode 100644 bcbox/frontend/tests/unit/components/layout/Footer.spec.ts create mode 100644 bcbox/frontend/tests/unit/components/layout/Header.spec.ts create mode 100644 bcbox/frontend/tests/unit/components/layout/LoginButton.spec.ts create mode 100644 bcbox/frontend/tests/unit/components/layout/Navbar.spec.ts create mode 100644 bcbox/frontend/tests/unit/components/layout/ProgressLoader.spec.ts create mode 100644 bcbox/frontend/tests/unit/components/layout/Spinner.spec.ts create mode 100644 bcbox/frontend/tests/unit/store/appStore.spec.ts create mode 100644 bcbox/frontend/tests/unit/store/authStore.spec.ts create mode 100644 bcbox/frontend/tests/unit/store/bucketStore.spec.ts create mode 100644 bcbox/frontend/tests/unit/store/configStore.spec.ts create mode 100644 bcbox/frontend/tests/unit/store/metadataStore.spec.ts create mode 100644 bcbox/frontend/tests/unit/store/objectStore.spec.ts create mode 100644 bcbox/frontend/tests/unit/store/tagStore.spec.ts create mode 100644 bcbox/frontend/tests/unit/store/userStore.spec.ts create mode 100644 bcbox/frontend/tests/unit/store/versionStore.spec.ts create mode 100644 bcbox/frontend/tests/unit/utils/formatters.spec.ts create mode 100644 bcbox/frontend/tsconfig.config.json create mode 100644 bcbox/frontend/tsconfig.json create mode 100644 bcbox/frontend/vite.config.ts create mode 100644 bcbox/frontend/volar.config.js create mode 100644 comsapi/.codeclimate.yml create mode 100644 comsapi/.dockerignore create mode 100644 comsapi/.editorconfig create mode 100644 comsapi/.gitattributes create mode 100644 comsapi/.gitignore create mode 100644 comsapi/.vscode/extensions.json create mode 100644 comsapi/.vscode/launch.json create mode 100644 comsapi/.vscode/settings.json create mode 100644 comsapi/CODE-OF-CONDUCT.md create mode 100644 comsapi/COMPLIANCE.yaml create mode 100644 comsapi/CONTRIBUTING.md create mode 100644 comsapi/Dockerfile create mode 100644 comsapi/LICENSE create mode 100644 comsapi/README.md create mode 100644 comsapi/SECURITY.md create mode 100644 comsapi/_config.yaml create mode 100644 comsapi/app/.editorconfig create mode 100644 comsapi/app/.eslintignore create mode 100644 comsapi/app/.eslintrc.js create mode 100644 comsapi/app/.prettierrc create mode 100644 comsapi/app/README.md create mode 100644 comsapi/app/app.js create mode 100644 comsapi/app/bin/www create mode 100644 comsapi/app/config/custom-environment-variables.json create mode 100644 comsapi/app/config/default.json create mode 100644 comsapi/app/config/production.json create mode 100644 comsapi/app/config/test.json create mode 100644 comsapi/app/jest.config.js create mode 100644 comsapi/app/knexfile.js create mode 100644 comsapi/app/lcov-fix.js create mode 100644 comsapi/app/nodemon.json create mode 100644 comsapi/app/package-lock.json create mode 100644 comsapi/app/package.json create mode 100644 comsapi/app/src/components/constants.js create mode 100644 comsapi/app/src/components/crypt.js create mode 100644 comsapi/app/src/components/errorToProblem.js create mode 100644 comsapi/app/src/components/log.js create mode 100644 comsapi/app/src/components/queueManager.js create mode 100644 comsapi/app/src/components/utils.js create mode 100644 comsapi/app/src/controllers/bucket.js create mode 100644 comsapi/app/src/controllers/bucketPermission.js create mode 100644 comsapi/app/src/controllers/index.js create mode 100644 comsapi/app/src/controllers/metadata.js create mode 100644 comsapi/app/src/controllers/object.js create mode 100644 comsapi/app/src/controllers/objectPermission.js create mode 100644 comsapi/app/src/controllers/sync.js create mode 100644 comsapi/app/src/controllers/tag.js create mode 100644 comsapi/app/src/controllers/user.js create mode 100644 comsapi/app/src/controllers/version.js create mode 100644 comsapi/app/src/db/.editorconfig create mode 100644 comsapi/app/src/db/dataConnection.js create mode 100644 comsapi/app/src/db/migrations/20220130133615_000-init.js create mode 100644 comsapi/app/src/db/migrations/20220516000000_001-version.js create mode 100644 comsapi/app/src/db/migrations/20220627000000_002-metadata-tags.js create mode 100644 comsapi/app/src/db/migrations/20221014000000_003-multi-bucket.js create mode 100644 comsapi/app/src/db/migrations/20230306000000_004-s3-version-tag-expand.js create mode 100644 comsapi/app/src/db/migrations/20230420000000_005-permission-indexes.js create mode 100644 comsapi/app/src/db/migrations/20230427000000_006-metadata-fix.js create mode 100644 comsapi/app/src/db/migrations/20230503000000_007-default-updatedat.js create mode 100644 comsapi/app/src/db/migrations/20230518000000_008-filename-etag.js create mode 100644 comsapi/app/src/db/migrations/20230810000000_009-user-identity-constraint.js create mode 100644 comsapi/app/src/db/migrations/20230814000000_010-sync-queue.js create mode 100644 comsapi/app/src/db/migrations/20231010000000_011-metadata-text.js create mode 100644 comsapi/app/src/db/models/index.js create mode 100644 comsapi/app/src/db/models/jsonSchema.js create mode 100644 comsapi/app/src/db/models/mixins/encrypt.js create mode 100644 comsapi/app/src/db/models/mixins/index.js create mode 100644 comsapi/app/src/db/models/mixins/timestamps.js create mode 100644 comsapi/app/src/db/models/tables/bucket.js create mode 100644 comsapi/app/src/db/models/tables/bucketPermission.js create mode 100644 comsapi/app/src/db/models/tables/identityProvider.js create mode 100644 comsapi/app/src/db/models/tables/metadata.js create mode 100644 comsapi/app/src/db/models/tables/objectModel.js create mode 100644 comsapi/app/src/db/models/tables/objectPermission.js create mode 100644 comsapi/app/src/db/models/tables/objectQueue.js create mode 100644 comsapi/app/src/db/models/tables/permission.js create mode 100644 comsapi/app/src/db/models/tables/tag.js create mode 100644 comsapi/app/src/db/models/tables/user.js create mode 100644 comsapi/app/src/db/models/tables/version.js create mode 100644 comsapi/app/src/db/models/tables/versionMetadata.js create mode 100644 comsapi/app/src/db/models/tables/versionTag.js create mode 100644 comsapi/app/src/db/models/utils.js create mode 100644 comsapi/app/src/db/stamps.js create mode 100644 comsapi/app/src/docs/docs.js create mode 100644 comsapi/app/src/docs/v1.api-spec.yaml create mode 100644 comsapi/app/src/middleware/authentication.js create mode 100644 comsapi/app/src/middleware/authorization.js create mode 100644 comsapi/app/src/middleware/featureToggle.js create mode 100644 comsapi/app/src/middleware/upload.js create mode 100644 comsapi/app/src/middleware/validation.js create mode 100644 comsapi/app/src/routes/v1/bucket.js create mode 100644 comsapi/app/src/routes/v1/docs.js create mode 100644 comsapi/app/src/routes/v1/index.js create mode 100644 comsapi/app/src/routes/v1/metadata.js create mode 100644 comsapi/app/src/routes/v1/object.js create mode 100644 comsapi/app/src/routes/v1/permission/bucketPermission.js create mode 100644 comsapi/app/src/routes/v1/permission/index.js create mode 100644 comsapi/app/src/routes/v1/permission/objectPermission.js create mode 100644 comsapi/app/src/routes/v1/sync.js create mode 100644 comsapi/app/src/routes/v1/tag.js create mode 100644 comsapi/app/src/routes/v1/user.js create mode 100644 comsapi/app/src/routes/v1/version.js create mode 100644 comsapi/app/src/services/bucket.js create mode 100644 comsapi/app/src/services/bucketPermission.js create mode 100644 comsapi/app/src/services/index.js create mode 100644 comsapi/app/src/services/metadata.js create mode 100644 comsapi/app/src/services/object.js create mode 100644 comsapi/app/src/services/objectPermission.js create mode 100644 comsapi/app/src/services/objectQueue.js create mode 100644 comsapi/app/src/services/storage.js create mode 100644 comsapi/app/src/services/sync.js create mode 100644 comsapi/app/src/services/tag.js create mode 100644 comsapi/app/src/services/user.js create mode 100644 comsapi/app/src/services/version.js create mode 100644 comsapi/app/src/validators/bucket.js create mode 100644 comsapi/app/src/validators/bucketPermission.js create mode 100644 comsapi/app/src/validators/common.js create mode 100644 comsapi/app/src/validators/index.js create mode 100644 comsapi/app/src/validators/metadata.js create mode 100644 comsapi/app/src/validators/object.js create mode 100644 comsapi/app/src/validators/objectPermission.js create mode 100644 comsapi/app/src/validators/tag.js create mode 100644 comsapi/app/src/validators/user.js create mode 100644 comsapi/app/src/validators/version.js create mode 100644 comsapi/app/tests/common/helper.js create mode 100644 comsapi/app/tests/unit/components/crypt.spec.js create mode 100644 comsapi/app/tests/unit/components/errorToProblem.spec.js create mode 100644 comsapi/app/tests/unit/components/log.spec.js create mode 100644 comsapi/app/tests/unit/components/queueManager.spec.js create mode 100644 comsapi/app/tests/unit/components/utils.spec.js create mode 100644 comsapi/app/tests/unit/controllers/bucket.spec.js create mode 100644 comsapi/app/tests/unit/controllers/metadata.spec.js create mode 100644 comsapi/app/tests/unit/controllers/object.spec.js create mode 100644 comsapi/app/tests/unit/controllers/objectPermission.spec.js create mode 100644 comsapi/app/tests/unit/controllers/sync.spec.js create mode 100644 comsapi/app/tests/unit/controllers/tag.spec.js create mode 100644 comsapi/app/tests/unit/db/models/utils.spec.js create mode 100644 comsapi/app/tests/unit/middleware/authentication.spec.js create mode 100644 comsapi/app/tests/unit/middleware/authorization.spec.js create mode 100644 comsapi/app/tests/unit/middleware/featureToggle.spec.js create mode 100644 comsapi/app/tests/unit/middleware/upload.spec.js create mode 100644 comsapi/app/tests/unit/middleware/validation.spec.js create mode 100644 comsapi/app/tests/unit/routes/v1.spec.js create mode 100644 comsapi/app/tests/unit/routes/v1/docs.spec.js create mode 100644 comsapi/app/tests/unit/routes/v1/object.spec.js create mode 100644 comsapi/app/tests/unit/routes/v1/user.spec.js create mode 100644 comsapi/app/tests/unit/services/bucket.spec.js create mode 100644 comsapi/app/tests/unit/services/bucketPermission.spec.js create mode 100644 comsapi/app/tests/unit/services/metadata.spec.js create mode 100644 comsapi/app/tests/unit/services/object.spec.js create mode 100644 comsapi/app/tests/unit/services/objectPermission.spec.js create mode 100644 comsapi/app/tests/unit/services/objectQueue.spec.js create mode 100644 comsapi/app/tests/unit/services/storage.spec.js create mode 100644 comsapi/app/tests/unit/services/sync.spec.js create mode 100644 comsapi/app/tests/unit/services/tag.spec.js create mode 100644 comsapi/app/tests/unit/services/user.spec.js create mode 100644 comsapi/app/tests/unit/services/version.spec.js create mode 100644 comsapi/app/tests/unit/validators/bucket.spec.js create mode 100644 comsapi/app/tests/unit/validators/bucketPermission.spec.js create mode 100644 comsapi/app/tests/unit/validators/common.spec.js create mode 100644 comsapi/app/tests/unit/validators/metadata.spec.js create mode 100644 comsapi/app/tests/unit/validators/object.spec.js create mode 100644 comsapi/app/tests/unit/validators/objectPermission.spec.js create mode 100644 comsapi/app/tests/unit/validators/tag.spec.js create mode 100644 comsapi/app/tests/unit/validators/user.spec.js create mode 100644 comsapi/app/tests/unit/validators/version.spec.js create mode 100644 comsapi/bcgovpubcode.yml create mode 100644 comsapi/charts/coms/.helmignore create mode 100644 comsapi/charts/coms/Chart.yaml create mode 100644 comsapi/charts/coms/README.md create mode 100644 comsapi/charts/coms/templates/NOTES.txt create mode 100644 comsapi/charts/coms/templates/_helpers.tpl create mode 100644 comsapi/charts/coms/templates/configmap.yaml create mode 100644 comsapi/charts/coms/templates/deploymentconfig.yaml create mode 100644 comsapi/charts/coms/templates/hpa.yaml create mode 100644 comsapi/charts/coms/templates/networkpolicy.yaml create mode 100644 comsapi/charts/coms/templates/route.yaml create mode 100644 comsapi/charts/coms/templates/secret.yaml create mode 100644 comsapi/charts/coms/templates/service.yaml create mode 100644 comsapi/charts/coms/templates/serviceaccount.yaml create mode 100644 comsapi/charts/coms/values.yaml create mode 100644 comsapi/k6/README.md create mode 100644 comsapi/k6/createObject.js create mode 100644 comsapi/k6/readObject.js create mode 100644 comsapi/k6/searchObject.js diff --git a/bcbox/.codeclimate.yml b/bcbox/.codeclimate.yml new file mode 100644 index 00000000..10764a49 --- /dev/null +++ b/bcbox/.codeclimate.yml @@ -0,0 +1,57 @@ +version: "2" +exclude_patterns: + - config/ + - db/ + - dist/ + - features/ + - "**/node_modules/" + - script/ + - "**/spec/" + - "**/test/" + - "**/tests/" + - Tests/ + - "**/vendor/" + - "**/*_test.go" + - "**/*.d.ts" +plugins: + csslint: + enabled: true + editorconfig: + enabled: true + checks: + END_OF_LINE: + enabled: false + INDENTATION_SPACES: + enabled: false + INDENTATION_SPACES_AMOUNT: + enabled: false + TRAILINGSPACES: + enabled: false + eslint: + enabled: true + channel: "eslint-7" + config: + config: app/.eslintrc.js + fixme: + enabled: true + git-legal: + enabled: true + markdownlint: + enabled: true + checks: + MD002: + enabled: false + MD013: + enabled: false + MD029: + enabled: false + MD046: + enabled: false + nodesecurity: + enabled: true + sass-lint: + enabled: true + rules: + nesting-depth: + - 2 + - max-depth: 5 diff --git a/bcbox/.dockerignore b/bcbox/.dockerignore new file mode 100644 index 00000000..d95ee2a1 --- /dev/null +++ b/bcbox/.dockerignore @@ -0,0 +1,42 @@ +# Editor directories and files +.DS_Store +.gradle +.nyc_output +.scannerwork +build +coverage +dist +files +**/e2e/videos +node_modules +# Ignore only top-level package-lock.json +/package-lock.json + +# Ignore Helm subcharts +charts/**/charts +Chart.lock + +# local env files +local.* +local-*.* +.env.local +.env.*.local + +# Log files +npm-debug.log* +yarn-debug.log* +yarn-error.log* + +# Editor directories and files +.idea +.vscode +*.iml +*.suo +*.ntvs* +*.njsproj +*.sln +*.sw? +*.mp4 + +# temp office files +~$* diff --git a/bcbox/.editorconfig b/bcbox/.editorconfig new file mode 100644 index 00000000..689f0b86 --- /dev/null +++ b/bcbox/.editorconfig @@ -0,0 +1,23 @@ +root = true + +[*] +charset = utf-8 +end_of_line = lf +insert_final_newline = true +trim_trailing_whitespace = true + +[*.html] +indent_style = space +indent_size = 2 + +[*.{css,js,json,jsx,scss,ts,tsx,vue}] +indent_style = space +indent_size = 2 + +[.{babelrc,eslintrc}] +indent_style = space +indent_size = 2 + +[Jenkinsfile*] +indent_style = space +indent_size = 2 diff --git a/bcbox/.gitattributes b/bcbox/.gitattributes new file mode 100644 index 00000000..fd57b60d --- /dev/null +++ b/bcbox/.gitattributes @@ -0,0 +1,8 @@ +# Autodetect text files and forces unix eols, so Windows does not break them +* text=auto eol=lf + +# Force images/fonts to be handled as binaries +*.jpg binary +*.jpeg binary +*.gif binary +*.png binary diff --git a/bcbox/.gitignore b/bcbox/.gitignore new file mode 100644 index 00000000..1b452670 --- /dev/null +++ b/bcbox/.gitignore @@ -0,0 +1,42 @@ +# Editor directories and files +.DS_Store +.gradle +.nyc_output +.scannerwork +build +coverage +dist +files +**/e2e/videos +node_modules +# Ignore only top-level package-lock.json +/package-lock.json + +# Ignore Helm subcharts +charts/**/charts +Chart.lock + +# local env files +*local.* +*local-*.* +.env.local +.env.*.local + + +# Log files +npm-debug.log* +yarn-debug.log* +yarn-error.log* + +# Editor directories and files +.idea +*.iml +*.suo +*.ntvs* +*.njsproj +*.sln +*.sw? +*.mp4 + +# temp office files +~$* diff --git a/bcbox/.vscode/extensions.json b/bcbox/.vscode/extensions.json new file mode 100644 index 00000000..8e31fb92 --- /dev/null +++ b/bcbox/.vscode/extensions.json @@ -0,0 +1,14 @@ +{ + "recommendations": [ + "bierner.markdown-preview-github-styles", + "davidanson.vscode-markdownlint", + "dbaeumer.vscode-eslint", + "eamodio.gitlens", + "editorconfig.editorconfig", + "esbenp.prettier-vscode", + "redhat.vscode-yaml", + "ryanluker.vscode-coverage-gutters", + "vue.volar", + "vue.vscode-typescript-vue-plugin" + ] +} diff --git a/bcbox/.vscode/launch.json b/bcbox/.vscode/launch.json new file mode 100644 index 00000000..68475fe2 --- /dev/null +++ b/bcbox/.vscode/launch.json @@ -0,0 +1,16 @@ +{ + // Use IntelliSense to learn about possible attributes. + // Hover to view descriptions of existing attributes. + // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 + "version": "0.2.0", + "configurations": [ + { + "type": "chrome", + "request": "launch", + "sourceMaps": true, + "name": "Launch Chrome against localhost", + "url": "http://localhost:8080", + "webRoot": "${workspaceFolder}" + } + ] +} diff --git a/bcbox/.vscode/settings.json b/bcbox/.vscode/settings.json new file mode 100644 index 00000000..bc775e89 --- /dev/null +++ b/bcbox/.vscode/settings.json @@ -0,0 +1,26 @@ +{ + "[html]": { + "editor.defaultFormatter": "vscode.html-language-features" + }, + "[javascript]": { + "editor.defaultFormatter": "esbenp.prettier-vscode" + }, + "[json]": { + "editor.defaultFormatter": "vscode.json-language-features" + }, + "[jsonc]": { + "editor.defaultFormatter": "vscode.json-language-features" + }, + "[typescript]": { + "editor.defaultFormatter": "esbenp.prettier-vscode" + }, + "[vue]": { + "editor.defaultFormatter": "esbenp.prettier-vscode" + }, + "coverage-gutters.showGutterCoverage": false, + "coverage-gutters.showLineCoverage": true, + "editor.defaultFormatter": "dbaeumer.vscode-eslint", + "editor.formatOnSave": true, + "eslint.format.enable": true, + "files.insertFinalNewline": true +} diff --git a/bcbox/CODE-OF-CONDUCT.md b/bcbox/CODE-OF-CONDUCT.md new file mode 100644 index 00000000..0c1e1bbe --- /dev/null +++ b/bcbox/CODE-OF-CONDUCT.md @@ -0,0 +1,46 @@ +# Contributor Covenant Code of Conduct + +## Our Pledge + +In the interest of fostering an open and welcoming environment, we as contributors and maintainers pledge to making participation in our project and our community a harassment-free experience for everyone, regardless of age, body size, disability, ethnicity, gender identity and expression, level of experience, nationality, personal appearance, race, religion, or sexual identity and orientation. + +## Our Standards + +Examples of behavior that contributes to creating a positive environment include: + +* Using welcoming and inclusive language +* Being respectful of differing viewpoints and experiences +* Gracefully accepting constructive criticism +* Focusing on what is best for the community +* Showing empathy towards other community members + +Examples of unacceptable behavior by participants include: + +* The use of sexualized language or imagery and unwelcome sexual attention or advances +* Trolling, insulting/derogatory comments, and personal or political attacks +* Public or private harassment +* Publishing others' private information, such as a physical or electronic address, without explicit permission +* Other conduct which could reasonably be considered inappropriate in a professional setting + +## Our Responsibilities + +Project maintainers are responsible for clarifying the standards of acceptable behavior and are expected to take appropriate and fair corrective action in response to any instances of unacceptable behavior. + +Project maintainers have the right and responsibility to remove, edit, or reject comments, commits, code, wiki edits, issues, and other contributions that are not aligned to this Code of Conduct, or to ban temporarily or permanently any contributor for other behaviors that they deem inappropriate, threatening, offensive, or harmful. + +## Scope + +This Code of Conduct applies both within project spaces and in public spaces when an individual is representing the project or its community. Examples of representing a project or community include using an official project e-mail address, posting via an official social media account, or acting as an appointed representative at an online or offline event. Representation of a project may be further defined and clarified by project maintainers. + +## Enforcement + +Instances of abusive, harassing, or otherwise unacceptable behavior may be reported by contacting the project team at matthew.hall@gov.bc.ca. The project team will review and investigate all complaints, and will respond in a way that it deems appropriate to the circumstances. The project team is obligated to maintain confidentiality with regard to the reporter of an incident. Further details of specific enforcement policies may be posted separately. + +Project maintainers who do not follow or enforce the Code of Conduct in good faith may face temporary or permanent repercussions as determined by other members of the project's leadership. + +## Attribution + +This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4, available at [http://contributor-covenant.org/version/1/4][version] + +[homepage]: http://contributor-covenant.org +[version]: http://contributor-covenant.org/version/1/4/ diff --git a/bcbox/COMPLIANCE.yaml b/bcbox/COMPLIANCE.yaml new file mode 100644 index 00000000..825499c8 --- /dev/null +++ b/bcbox/COMPLIANCE.yaml @@ -0,0 +1,11 @@ +name: compliance +description: | + This document is used to track a projects PIA and STRA + compliance. +spec: + - name: PIA + status: TBD + last-updated: "2022-12-19T00:00:00.000Z" + - name: STRA + status: TBD + last-updated: "2022-12-19T00:00:00.000Z" diff --git a/bcbox/CONTRIBUTING.md b/bcbox/CONTRIBUTING.md new file mode 100644 index 00000000..43d4d4a7 --- /dev/null +++ b/bcbox/CONTRIBUTING.md @@ -0,0 +1,9 @@ +# How to contribute + +Government employees, public and members of the private sector are encouraged to contribute to the repository by **forking and submitting a pull request**. + +(If you are new to GitHub, you might start with a [basic tutorial](https://help.github.com/articles/set-up-git) and check out a more detailed guide to [pull requests](https://help.github.com/articles/using-pull-requests/).) + +Pull requests will be evaluated by the repository guardians on a schedule and if deemed beneficial will be committed to the master. + +All contributors retain the original copyright to their stuff, but by contributing to this project, you grant a world-wide, royalty-free, perpetual, irrevocable, non-exclusive, transferable license to all users **under the terms of the [license](./LICENSE) under which this project is distributed**. diff --git a/bcbox/Dockerfile b/bcbox/Dockerfile new file mode 100644 index 00000000..db97a497 --- /dev/null +++ b/bcbox/Dockerfile @@ -0,0 +1,49 @@ +ARG APP_ROOT=/opt/app-root/src +ARG BASE_IMAGE=docker.io/node:20.9.0-alpine + +# +# Build the frontend +# +FROM ${BASE_IMAGE} as frontend + +ARG APP_ROOT +ENV NO_UPDATE_NOTIFIER=true + +# NPM Permission Fix +RUN mkdir -p /.npm +RUN chown -R 1001:0 /.npm + +# Build Frontend +COPY frontend ${APP_ROOT} +RUN chown -R 1001:0 ${APP_ROOT} +USER 1001 +WORKDIR ${APP_ROOT} +RUN npm ci && npm run build + +# +# Create the final container image +# +FROM ${BASE_IMAGE} + +ARG APP_ROOT +ENV APP_PORT=8080 \ + NO_UPDATE_NOTIFIER=true + +# NPM Permission Fix +RUN mkdir -p /.npm +RUN chown -R 1001:0 /.npm + +# Install File Structure +COPY --from=frontend ${APP_ROOT}/dist ${APP_ROOT}/dist +# COPY .git ${APP_ROOT}/.git # Temporarily commented out line due to OCP build failure. Unsure why it is necessary in the first place. +COPY app ${APP_ROOT} +WORKDIR ${APP_ROOT} + +# Install Application +RUN chown -R 1001:0 ${APP_ROOT} +USER 1001 +WORKDIR ${APP_ROOT}/app +RUN npm ci --omit=dev + +EXPOSE ${APP_PORT} +CMD ["npm", "run", "start"] diff --git a/bcbox/LICENSE b/bcbox/LICENSE new file mode 100644 index 00000000..62dc6edd --- /dev/null +++ b/bcbox/LICENSE @@ -0,0 +1,201 @@ + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright 2022 Province of British Columbia + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. diff --git a/bcbox/LICENSE.md b/bcbox/LICENSE.md new file mode 100644 index 00000000..427417b6 --- /dev/null +++ b/bcbox/LICENSE.md @@ -0,0 +1,202 @@ + + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "{}" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright {yyyy} {name of copyright owner} + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. diff --git a/bcbox/README.md b/bcbox/README.md new file mode 100644 index 00000000..8ef891e7 --- /dev/null +++ b/bcbox/README.md @@ -0,0 +1,87 @@ + +# BCBox + +[![License](https://img.shields.io/badge/License-Apache%202.0-blue.svg)](LICENSE) +[![Lifecycle:Maturing](https://img.shields.io/badge/Lifecycle-Maturing-007EC6)](https://github.com/bcgov/repomountie/blob/master/doc/lifecycle-badges.md) + +![Tests](https://github.com/bcgov/bcbox/workflows/Tests/badge.svg) +[![Maintainability](https://api.codeclimate.com/v1/badges/bfaf1cdb7fe730c10840/maintainability)](https://codeclimate.com/github/bcgov/bcbox/maintainability) +[![Test Coverage](https://api.codeclimate.com/v1/badges/bfaf1cdb7fe730c10840/test_coverage)](https://codeclimate.com/github/bcgov/bcbox/test_coverage) + +A frontend UI for managing access control to S3 Objects + +To learn more about the **Common Services** available visit the [Common Services Showcase](https://bcgov.github.io/common-service-showcase/) page. + +## Directory Structure + +```txt +.github/ - PR, Issue templates and CI/CD +.vscode/ - VSCode environment configurations +app/ - Application Root +├── src/ - Node.js web application +│ └── components/ - Components Layer +└── tests/ - Node.js web application tests +chart/ - General Helm Charts +└── bcbox/ - BCBox Helm Chart Repository + └── templates/ - BCBox Helm Chart Template manifests +frontend/ - Frontend Root +├── src/ - Node.js web application +│ ├── assets/ - Static File Assets +│ ├── components/ - Components Layer +│ ├── composables/ - Common composition elements +│ ├── interfaces/ - Typescript interface definitions +│ ├── lib/ - Repackaged external libraries +│ ├── router/ - Router Layer +│ ├── services/ - Services Layer +│ ├── store/ - Store Layer +│ ├── types/ - Typescript type definitions +│ ├── utils/ - Utility components +│ └── views/ - View Layer +└── tests/ - Node.js web application tests +bcgovpubcode.yaml - BCGov Public Code manifest +CODE-OF-CONDUCT.md - Code of Conduct +COMPLIANCE.yaml - BCGov PIA/STRA compliance status +CONTRIBUTING.md - Contributing Guidelines +Dockerfile - Dockerfile Image definition +LICENSE - License +SECURITY.md - Security Policy and Reporting +``` + +## Documentation + +* [Application Readme](frontend/README.md) +* [Product Roadmap](https://github.com/bcgov/bcbox/wiki/Product-Roadmap) +* [Product Wiki](https://github.com/bcgov/bcbox/wiki) +* [Security Reporting](SECURITY.md) + +## Getting Help or Reporting an Issue + +To report bugs/issues/features requests, please file an [issue](https://github.com/bcgov/bcbox/issues). + +## How to Contribute + +If you would like to contribute, please see our [contributing](CONTRIBUTING.md) guidelines. + +Please note that this project is released with a [Contributor Code of Conduct](CODE-OF-CONDUCT.md). By participating in this project you agree to abide by its terms. + +## Forking & Self-hosting + +If you intend to fork, host and support your own version of this application, please ensure that you re-brand the application's name and content. Please remove any notices, links or contact information and consider contributing new features back to this repository. + +## License + +```txt +Copyright 2022 Province of British Columbia + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +``` diff --git a/bcbox/SECURITY.md b/bcbox/SECURITY.md new file mode 100644 index 00000000..9a0e0360 --- /dev/null +++ b/bcbox/SECURITY.md @@ -0,0 +1,52 @@ +# Security Policies and Procedures + +This document outlines security procedures and general policies for the BCBox +project. + +- [Supported Versions](#supported-versions) +- [Reporting a Bug](#reporting-a-bug) +- [Disclosure Policy](#disclosure-policy) +- [Comments on this Policy](#comments-on-this-policy) + +## Supported Versions + +At this time, only the latest version of BCBox is supported. + +| Version | Supported | +| ------- | ------------------ | +| 0.5.0 | :white_check_mark: | +| < 0.5.x | :x: | + +## Reporting a Bug + +The `CSS` team and community take all security bugs in `BCBox` seriously. +Thank you for improving the security of `BCBox`. We appreciate your efforts and +responsible disclosure and will make every effort to acknowledge your +contributions. + +Report security bugs by sending an email to . + +The `CSS` team will acknowledge your email within 48 hours, and will send a +more detailed response within 48 hours indicating the next steps in handling +your report. After the initial reply to your report, the security team will +endeavor to keep you informed of the progress towards a fix and full +announcement, and may ask for additional information or guidance. + +Report security bugs in third-party modules to the person or team maintaining +the module. + +## Disclosure Policy + +When the security team receives a security bug report, they will assign it to a +primary handler. This person will coordinate the fix and release process, +involving the following steps: + +- Confirm the problem and determine the affected versions. +- Audit code to find any potential similar problems. +- Prepare fixes for all releases still under maintenance. These fixes will be + released as fast as possible. + +## Comments on this Policy + +If you have suggestions on how this process could be improved please submit a +pull request. diff --git a/bcbox/_config.yml b/bcbox/_config.yml new file mode 100644 index 00000000..277f1f2c --- /dev/null +++ b/bcbox/_config.yml @@ -0,0 +1 @@ +theme: jekyll-theme-cayman diff --git a/bcbox/app/.eslintrc.js b/bcbox/app/.eslintrc.js new file mode 100644 index 00000000..9bd438c3 --- /dev/null +++ b/bcbox/app/.eslintrc.js @@ -0,0 +1,45 @@ +module.exports = { + root: true, + env: { + browser: true, + es2020: true, + jest: true, + node: true + }, + extends: [ + 'prettier', + 'eslint:recommended', + 'plugin:@typescript-eslint/eslint-recommended', + 'plugin:@typescript-eslint/recommended' + ], + globals: { + Atomics: 'readonly', + SharedArrayBuffer: 'readonly', + _: false + }, + plugins: ['@typescript-eslint'], + rules: { + 'eol-last': ['error', 'always'], + indent: [ + 'error', + 2, + { + SwitchCase: 1 + } + ], + 'linebreak-style': ['error', 'unix'], + 'max-len': ['warn', { code: 120, comments: 120, ignoreUrls: true }], + 'no-console': process.env.NODE_ENV === 'production' ? 'error' : 'warn', + 'no-debugger': process.env.NODE_ENV === 'production' ? 'error' : 'warn', + quotes: ['error', 'single'], + semi: ['error', 'always'] + }, + overrides: [ + { + files: ['**/__tests__/*.{j,t}s?(x)', '**/tests/unit/**/*.spec.{j,t}s?(x)'], + env: { + jest: true + } + } + ] +}; diff --git a/bcbox/app/.gitignore b/bcbox/app/.gitignore new file mode 100644 index 00000000..af47744e --- /dev/null +++ b/bcbox/app/.gitignore @@ -0,0 +1,43 @@ +# Editor directories and files +.DS_Store +.gradle +.nyc_output +.scannerwork +build +coverage +dist +files +**/e2e/videos +node_modules + +# Ignore Helm subcharts +charts/**/charts +Chart.lock + +# local env files +local.* +local-*.* +.env.local +.env.*.local + +# Log files +npm-debug.log* +yarn-debug.log* +yarn-error.log* + +# Editor directories and files +.idea +.vscode +*.iml +*.suo +*.ntvs* +*.njsproj +*.sln +*.sw? +*.mp4 + +# temp office files +~$* + +# contains idp configuration - overrides default +idplist-override.json diff --git a/bcbox/app/.prettierignore b/bcbox/app/.prettierignore new file mode 100644 index 00000000..53d7d115 --- /dev/null +++ b/bcbox/app/.prettierignore @@ -0,0 +1,29 @@ +# Logs +logs +*.log +npm-debug.log* +yarn-debug.log* +yarn-error.log* +pnpm-debug.log* +lerna-debug.log* + +build +node_modules +.DS_Store +dist +dist-ssr +coverage +*.local + +/cypress/videos/ +/cypress/screenshots/ + +# Editor directories and files +.vscode/* +!.vscode/extensions.json +.idea +*.suo +*.ntvs* +*.njsproj +*.sln +*.sw? diff --git a/bcbox/app/.prettierrc b/bcbox/app/.prettierrc new file mode 100644 index 00000000..e5f42d76 --- /dev/null +++ b/bcbox/app/.prettierrc @@ -0,0 +1,13 @@ +{ + "arrowParens": "always", + "bracketSameLine": false, + "bracketSpacing": true, + "htmlWhitespaceSensitivity": "ignore", + "printWidth": 120, + "semi": true, + "singleAttributePerLine": true, + "singleQuote": true, + "tabWidth": 2, + "trailingComma": "none", + "useTabs": false +} diff --git a/bcbox/app/app.ts b/bcbox/app/app.ts new file mode 100644 index 00000000..090b170b --- /dev/null +++ b/bcbox/app/app.ts @@ -0,0 +1,149 @@ +import compression from 'compression'; +import config from 'config'; +import express from 'express'; +import { join } from 'path'; +// @ts-expect-error api-problem lacks a defined interface; code still works fine +import Problem from 'api-problem'; +import querystring from 'querystring'; + +import { getLogger, httpLogger } from './src/components/log'; +import { getGitRevision, readIdpList } from './src/components/utils'; + +import type { Request, Response } from 'express'; + +const log = getLogger(module.filename); + +const state = { + idpList: readIdpList(), + gitRev: getGitRevision(), + ready: true, // No dependencies so application is always ready + shutdown: false +}; + +const appRouter = express.Router(); +const app = express(); +app.use(compression()); +app.use(express.json({ limit: config.get('server.bodyLimit') })); +app.use(express.urlencoded({ extended: true })); + +// Skip if running tests +if (process.env.NODE_ENV !== 'test') { + app.use(httpLogger); +} + +// Block requests until service is ready +app.use((_req: Request, res: Response, next: () => void): void => { + if (state.shutdown) { + new Problem(503, { details: 'Server is shutting down' }).send(res); + } else if (!state.ready) { + new Problem(503, { details: 'Server is not ready' }).send(res); + } else { + next(); + } +}); + +// Frontend configuration endpoint +appRouter.get('/config', (_req: Request, res: Response, next: (err: unknown) => void): void => { + try { + res.status(200).json({ + ...config.get('frontend'), + gitRev: state.gitRev, + idpList: state.idpList, + version: process.env.npm_package_version + }); + } catch (err) { + next(err); + } +}); + +// Base API Directory +appRouter.get('/api', (_req: Request, res: Response): void => { + if (state.shutdown) { + throw new Error('Server shutting down'); + } else { + res.status(200).json({ + app: { + gitRev: state.gitRev, + name: process.env.npm_package_name, + nodeVersion: process.version, + version: process.env.npm_package_version + }, + endpoints: ['/api/v1'], + versions: [1] + }); + } +}); + +// Host the static frontend assets +appRouter.use('/', express.static(join(__dirname, 'dist'))); + +// Mount application endpoints +app.use('/', appRouter); + +// Handle 500 +// eslint-disable-next-line no-unused-vars, @typescript-eslint/no-unused-vars +app.use((err: Problem, _req: Request, res: Response, _next: () => void): void => { + if (err.stack) { + log.error(err); + } + + if (err instanceof Problem) { + err.send(res, null); + } else { + new Problem(500, 'Server Error', { + detail: (err.message) ? err.message : err + }).send(res); + } +}); + +// Handle 404 +app.use((req: Request, res: Response): void => { + if (req.originalUrl.startsWith('/api')) { + // Return a 404 problem if attempting to access API + new Problem(404, 'Page Not Found', { + detail: req.originalUrl + }).send(res); + } else { + // Redirect any non-API requests to static frontend with redirect breadcrumb + const query = querystring.stringify({ ...req.query, r: req.path }); + res.redirect(`/?${query}`); + } +}); + +// Prevent unhandled errors from crashing application +process.on('unhandledRejection', (err: Error): void => { + if (err && err.stack) { + log.error(err); + } +}); + +// Graceful shutdown support +process.on('SIGTERM', shutdown); +process.on('SIGINT', shutdown); +process.on('SIGUSR1', shutdown); +process.on('SIGUSR2', shutdown); +process.on('exit', () => { + log.info('Exiting...'); +}); + +/** + * @function shutdown + * Shuts down this application after at least 3 seconds. + */ +function shutdown(): void { + log.info('Received kill signal. Shutting down...'); + // Wait 3 seconds before starting cleanup + if (!state.shutdown) setTimeout(cleanup, 3000); +} + +/** + * @function cleanup + * Cleans up connections in this application. + */ +function cleanup(): void { + log.info('Service no longer accepting traffic'); + state.shutdown = true; + process.exit(); +} + +export default app; diff --git a/bcbox/app/bin/www.ts b/bcbox/app/bin/www.ts new file mode 100644 index 00000000..1103cc34 --- /dev/null +++ b/bcbox/app/bin/www.ts @@ -0,0 +1,72 @@ +#!/usr/bin/env node + +/** Module dependencies */ +import config from 'config'; +import http from 'http'; + +import app from '../app'; +import getLogger from '../src/components/log'; +const log = getLogger(module.filename); + +/** Normalize a port into a number, string, or false. */ +const normalizePort = (val: string) => { + const port = parseInt(val, 10); + + if (isNaN(port)) { + // named pipe + return val; + } + + if (port >= 0) { + // port number + return port; + } + + return false; +}; + +/** Event listener for HTTP server "error" event. */ +const onError = (error: { syscall: string; code: string; }) => { + if (error.syscall !== 'listen') { + throw error; + } + + const bind = typeof port === 'string' ? + 'Pipe ' + port : + 'Port ' + port; + + // handle specific listen errors with friendly messages + switch (error.code) { + case 'EACCES': + log.error(bind + ' requires elevated privileges'); + process.exit(1); + break; + case 'EADDRINUSE': + log.error(bind + ' is already in use'); + process.exit(1); + break; + default: + throw error; + } +}; + +/** Event listener for HTTP server "listening" event. */ +const onListening = () => { + const addr = server.address(); + const bind = typeof addr === 'string' ? + 'pipe ' + addr : + 'port ' + addr?.port; + log.info('Listening on ' + bind); +}; + +/** Get port from environment and store in Express. */ +const port = normalizePort(config.get('server.port')); +app.set('port', port); + +/** Create HTTP server. */ +const server = http.createServer(app); + +/** Listen on provided port, on all network interfaces. */ +server.listen(port); +server.on('error', onError); +server.on('listening', onListening); diff --git a/bcbox/app/config/custom-environment-variables.json b/bcbox/app/config/custom-environment-variables.json new file mode 100644 index 00000000..094705cf --- /dev/null +++ b/bcbox/app/config/custom-environment-variables.json @@ -0,0 +1,24 @@ +{ + "frontend": { + "apiPath": "FRONTEND_APIPATH", + "coms": { + "apiPath": "FRONTEND_COMS_APIPATH" + }, + "exclude": { + "metadata": "FRONTEND_EXCLUDE_METADATA", + "tagset": "FRONTEND_EXCLUDE_TAGSET" + }, + "notificationBanner": "FRONTEND_NOTIFICATION_BANNER", + "oidc": { + "authority": "https://epd-keycloak-dev.apps.silver.devops.gov.bc.ca/auth/realms/forms-flow-ai/", + "clientId": "epd-local" + } + }, + "server": { + "apiPath": "SERVER_APIPATH", + "bodyLimit": "SERVER_BODYLIMIT", + "logFile": "SERVER_LOGFILE", + "logLevel": "SERVER_LOGLEVEL", + "port": "SERVER_PORT" + } +} diff --git a/bcbox/app/config/default.json b/bcbox/app/config/default.json new file mode 100644 index 00000000..3e4218cd --- /dev/null +++ b/bcbox/app/config/default.json @@ -0,0 +1,21 @@ +{ + "frontend": { + "coms": { + "apiPath": "http://localhost:4050/api/v1/" + }, + "apiPath": "api/v1", + "exclude": { + "tagset": "coms-id" + }, + "oidc": { + "authority": "https://epd-keycloak-dev.apps.silver.devops.gov.bc.ca/auth/realms/forms-flow-ai/", + "clientId": "epd-local" + } + }, + "server": { + "apiPath": "/api/v1", + "bodyLimit": "30mb", + "logLevel": "http", + "port": "8080" + } +} diff --git a/bcbox/app/config/idplist-default.json b/bcbox/app/config/idplist-default.json new file mode 100644 index 00000000..fdd9aba0 --- /dev/null +++ b/bcbox/app/config/idplist-default.json @@ -0,0 +1,23 @@ +[ + { + "name": "IDIR", + "elevatedRights": true, + "idp": "idir", + "identityKey": "idir_user_guid", + "searchable": true + }, + { + "name": "BCeID Basic", + "elevatedRights": false, + "idp": "bceidbasic", + "identityKey": "sub", + "searchable": false + }, + { + "name": "BCeID Business", + "elevatedRights": false, + "idp": "bceidbusiness", + "identityKey": "bceid_user_guid", + "searchable": false + } +] diff --git a/bcbox/app/config/production.json b/bcbox/app/config/production.json new file mode 100644 index 00000000..0967ef42 --- /dev/null +++ b/bcbox/app/config/production.json @@ -0,0 +1 @@ +{} diff --git a/bcbox/app/config/test.json b/bcbox/app/config/test.json new file mode 100644 index 00000000..1b6d8b1a --- /dev/null +++ b/bcbox/app/config/test.json @@ -0,0 +1,22 @@ +{ + "frontend": { + "oidc": { + "authority": "https://some.authority", + "clientId": "client-frontend-local" + } + }, + "server": { + "keycloak": { + "clientId": "client", + "realm": "01234567", + "clientSecret": "password" + }, + "logLevel": "silent" + }, + "serviceClient": { + "commonServices": { + "username": "username", + "password": "password" + } + } +} diff --git a/bcbox/app/frontend-utils.ts b/bcbox/app/frontend-utils.ts new file mode 100644 index 00000000..f2d03f4b --- /dev/null +++ b/bcbox/app/frontend-utils.ts @@ -0,0 +1,158 @@ +// This script attempts to gracefully rebuild and update bcbox-frontend if necessary +/* eslint-disable no-console */ +import { existsSync, lstatSync, mkdirSync, readdirSync, readFileSync, rmSync, statSync, writeFileSync } from 'fs'; +import { basename, join } from 'path'; + +const FRONTEND_DIR = '../frontend'; +const DIST_DIR = 'dist'; +const TITLE = 'bcbox-frontend'; + +try { + const args = process.argv.slice(2); + switch (args[0]) { + case 'build': + buildComponents(); + break; + case 'clean': + cleanComponents(); + break; + case 'deploy': + deployComponents(); + break; + case 'purge': + console.log(`Purging "${DIST_DIR}"...`); + if (existsSync(DIST_DIR)) rmSync(DIST_DIR, { recursive: true }); + break; + default: + if (!existsSync(DIST_DIR) || !readdirSync(DIST_DIR).length) { + console.log(`${TITLE} not found under "${DIST_DIR}"`); + buildComponents(); + deployComponents(); + } else if (statSync(DIST_DIR).mtime < statSync(FRONTEND_DIR).mtime) { + console.log(`${TITLE} "${FRONTEND_DIR}" directory has been modified`); + buildComponents(); + deployComponents(); + } else { + console.log(`${TITLE} is present and up to date`); + } + } +} catch (err) { + console.log(`An error occured while managing ${TITLE}`); + process.exit(1); +} + +// +// Task Functions +// + +/** + * @function buildComponents + * @description Rebuild `bcbox-frontend` library + */ +function buildComponents() { + if (!existsSync(`${FRONTEND_DIR}/node_modules`)) { + console.warn(`${TITLE} missing dependencies. Reinstalling...`); + runSync('npm ci', FRONTEND_DIR); + } + console.log(`Rebuilding ${TITLE}...`); + runSync('npm run build', FRONTEND_DIR); + console.log(`${TITLE} has been rebuilt`); +} + +/** + * @function cleanComponents + * @description Clean `bcbox-frontend` library directory + */ +function cleanComponents() { + console.log(`Cleaning ${TITLE}...`); + runSync('npm run clean', FRONTEND_DIR); + console.log(`${TITLE} has been cleaned`); +} + +/** + * @function deployComponents + * @description Redeploy `bcbox-frontend` library + */ +function deployComponents() { + console.log(`Redeploying ${TITLE}...`); + if (existsSync(DIST_DIR)) rmSync(DIST_DIR, { recursive: true }); + copyDirRecursiveSync(`${FRONTEND_DIR}/${DIST_DIR}`, '.'); + console.log(`${TITLE} has been redeployed`); +} + +// +// Helper Functions +// + +/** + * @function runSync + * @description Execute a single shell command where `cmd` is a string + * @param {string} cmd Shell command to run + * @param {string} [cwd] Working directory of the command to run + */ +export function runSync(cmd: string, cwd: string | undefined) { + // eslint-disable-next-line @typescript-eslint/no-var-requires + const { spawnSync } = require('child_process'); + const parts = cmd.split(/\s+/g); + const opts = { + cwd: cwd || undefined, + stdio: 'inherit', + shell: true + }; + + const p = spawnSync(parts[0], parts.slice(1), opts); + if (p.status) { + throw new Error(`Command "${cmd}" exited with status code "${p.status}"`); + } + if (p.signal) { + throw new Error(`Command "${cmd}" exited with signal "${p.signal}"`); + } +} + +/** + * @function copyFileSync + * @description Copies `source` file to `target` file + * @param {string} source Source file location + * @param {string} target Target file location + */ +export function copyFileSync(source: string, target: string) { + let targetFile = target; + + // If target is a directory, a new file with the same name will be created + if (existsSync(target)) { + if (lstatSync(target).isDirectory()) { + targetFile = join(target, basename(source)); + } + } + + writeFileSync(targetFile, readFileSync(source)); +} + +/** + * @function copyDirRecursiveSync + * @description Recursively copies `source` directory contents to `target` directory + * @param {string} source Source directory location + * @param {string} target Target directory location + */ +export function copyDirRecursiveSync(source: string, target: string) { + let files = []; + + // Check if folder needs to be created or integrated + const targetFolder = join(target, basename(source)); + if (!existsSync(targetFolder)) { + mkdirSync(targetFolder, { recursive: true }); + } + + // Copy + if (lstatSync(source).isDirectory()) { + files = readdirSync(source); + files.forEach((file) => { + const curSource = join(source, file); + if (lstatSync(curSource).isDirectory()) { + copyDirRecursiveSync(curSource, targetFolder); + } else { + copyFileSync(curSource, targetFolder); + } + }); + } +} diff --git a/bcbox/app/jest.config.js b/bcbox/app/jest.config.js new file mode 100644 index 00000000..6329f3a6 --- /dev/null +++ b/bcbox/app/jest.config.js @@ -0,0 +1,6 @@ +/** @type {import('ts-jest').JestConfigWithTsJest} */ +module.exports = { + preset: 'ts-jest', + testEnvironment: 'node', + collectCoverage: true +}; diff --git a/bcbox/app/lcov-fix.ts b/bcbox/app/lcov-fix.ts new file mode 100644 index 00000000..7ca68c74 --- /dev/null +++ b/bcbox/app/lcov-fix.ts @@ -0,0 +1,16 @@ +// Jest 25.x onwards emits coverage reports on a different source path +// https://stackoverflow.com/q/60323177 +import fs from 'fs'; +import process from 'process'; +const file = './coverage/lcov.info'; + +fs.readFile(file, 'utf8', (err, data) => { + if (err) { + return console.error(err); // eslint-disable-line no-console + } + const result = data.replace(/src/g, `${process.cwd()}/src`); + + fs.writeFile(file, result, 'utf8', err => { + if (err) return console.error(err); // eslint-disable-line no-console + }); +}); diff --git a/bcbox/app/package-lock.json b/bcbox/app/package-lock.json new file mode 100644 index 00000000..88d63113 --- /dev/null +++ b/bcbox/app/package-lock.json @@ -0,0 +1,7571 @@ +{ + "name": "bcbox-app", + "version": "0.5.0", + "lockfileVersion": 1, + "requires": true, + "dependencies": { + "@aashutoshrathi/word-wrap": { + "version": "1.2.6", + "resolved": "https://registry.npmjs.org/@aashutoshrathi/word-wrap/-/word-wrap-1.2.6.tgz", + "integrity": "sha512-1Yjs2SvM8TflER/OD3cOjhWWOZb58A2t7wpE2S9XfBYTiIl+XFhQG2bjy4Pu1I+EAlCNUzRDYDdFwFYUKvXcIA==", + "dev": true + }, + "@ampproject/remapping": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/@ampproject/remapping/-/remapping-2.2.0.tgz", + "integrity": "sha512-qRmjj8nj9qmLTQXXmaR1cck3UXSRMPrbsLJAasZpF+t3riI71BXed5ebIOYwQntykeZuhjsdweEc9BxH5Jc26w==", + "dev": true, + "requires": { + "@jridgewell/gen-mapping": "^0.1.0", + "@jridgewell/trace-mapping": "^0.3.9" + } + }, + "@babel/code-frame": { + "version": "7.22.13", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.22.13.tgz", + "integrity": "sha512-XktuhWlJ5g+3TJXc5upd9Ks1HutSArik6jf2eAjYFyIOf4ej3RN+184cZbzDvbPnuTJIUhPKKJE3cIsYTiAT3w==", + "dev": true, + "requires": { + "@babel/highlight": "^7.22.13", + "chalk": "^2.4.2" + }, + "dependencies": { + "ansi-styles": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz", + "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==", + "dev": true, + "requires": { + "color-convert": "^1.9.0" + } + }, + "chalk": { + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", + "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==", + "dev": true, + "requires": { + "ansi-styles": "^3.2.1", + "escape-string-regexp": "^1.0.5", + "supports-color": "^5.3.0" + } + }, + "color-convert": { + "version": "1.9.3", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz", + "integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==", + "dev": true, + "requires": { + "color-name": "1.1.3" + } + }, + "color-name": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz", + "integrity": "sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw==", + "dev": true + }, + "escape-string-regexp": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", + "integrity": "sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg==", + "dev": true + }, + "has-flag": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", + "integrity": "sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==", + "dev": true + }, + "supports-color": { + "version": "5.5.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", + "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", + "dev": true, + "requires": { + "has-flag": "^3.0.0" + } + } + } + }, + "@babel/compat-data": { + "version": "7.20.1", + "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.20.1.tgz", + "integrity": "sha512-EWZ4mE2diW3QALKvDMiXnbZpRvlj+nayZ112nK93SnhqOtpdsbVD4W+2tEoT3YNBAG9RBR0ISY758ZkOgsn6pQ==", + "dev": true + }, + "@babel/core": { + "version": "7.19.6", + "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.19.6.tgz", + "integrity": "sha512-D2Ue4KHpc6Ys2+AxpIx1BZ8+UegLLLE2p3KJEuJRKmokHOtl49jQ5ny1773KsGLZs8MQvBidAF6yWUJxRqtKtg==", + "dev": true, + "requires": { + "@ampproject/remapping": "^2.1.0", + "@babel/code-frame": "^7.18.6", + "@babel/generator": "^7.19.6", + "@babel/helper-compilation-targets": "^7.19.3", + "@babel/helper-module-transforms": "^7.19.6", + "@babel/helpers": "^7.19.4", + "@babel/parser": "^7.19.6", + "@babel/template": "^7.18.10", + "@babel/traverse": "^7.19.6", + "@babel/types": "^7.19.4", + "convert-source-map": "^1.7.0", + "debug": "^4.1.0", + "gensync": "^1.0.0-beta.2", + "json5": "^2.2.1", + "semver": "^6.3.0" + }, + "dependencies": { + "debug": { + "version": "4.3.4", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", + "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==", + "dev": true, + "requires": { + "ms": "2.1.2" + } + }, + "ms": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", + "dev": true + } + } + }, + "@babel/generator": { + "version": "7.23.0", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.23.0.tgz", + "integrity": "sha512-lN85QRR+5IbYrMWM6Y4pE/noaQtg4pNiqeNGX60eqOfo6gtEj6uw/JagelB8vVztSd7R6M5n1+PQkDbHbBRU4g==", + "dev": true, + "requires": { + "@babel/types": "^7.23.0", + "@jridgewell/gen-mapping": "^0.3.2", + "@jridgewell/trace-mapping": "^0.3.17", + "jsesc": "^2.5.1" + }, + "dependencies": { + "@jridgewell/gen-mapping": { + "version": "0.3.2", + "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.2.tgz", + "integrity": "sha512-mh65xKQAzI6iBcFzwv28KVWSmCkdRBWoOh+bYQGW3+6OZvbbN3TqMGo5hqYxQniRcH9F2VZIoJCm4pa3BPDK/A==", + "dev": true, + "requires": { + "@jridgewell/set-array": "^1.0.1", + "@jridgewell/sourcemap-codec": "^1.4.10", + "@jridgewell/trace-mapping": "^0.3.9" + } + } + } + }, + "@babel/helper-compilation-targets": { + "version": "7.20.0", + "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.20.0.tgz", + "integrity": "sha512-0jp//vDGp9e8hZzBc6N/KwA5ZK3Wsm/pfm4CrY7vzegkVxc65SgSn6wYOnwHe9Js9HRQ1YTCKLGPzDtaS3RoLQ==", + "dev": true, + "requires": { + "@babel/compat-data": "^7.20.0", + "@babel/helper-validator-option": "^7.18.6", + "browserslist": "^4.21.3", + "semver": "^6.3.0" + } + }, + "@babel/helper-environment-visitor": { + "version": "7.22.20", + "resolved": "https://registry.npmjs.org/@babel/helper-environment-visitor/-/helper-environment-visitor-7.22.20.tgz", + "integrity": "sha512-zfedSIzFhat/gFhWfHtgWvlec0nqB9YEIVrpuwjruLlXfUSnA8cJB0miHKwqDnQ7d32aKo2xt88/xZptwxbfhA==", + "dev": true + }, + "@babel/helper-function-name": { + "version": "7.23.0", + "resolved": "https://registry.npmjs.org/@babel/helper-function-name/-/helper-function-name-7.23.0.tgz", + "integrity": "sha512-OErEqsrxjZTJciZ4Oo+eoZqeW9UIiOcuYKRJA4ZAgV9myA+pOXhhmpfNCKjEH/auVfEYVFJ6y1Tc4r0eIApqiw==", + "dev": true, + "requires": { + "@babel/template": "^7.22.15", + "@babel/types": "^7.23.0" + } + }, + "@babel/helper-hoist-variables": { + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/helper-hoist-variables/-/helper-hoist-variables-7.22.5.tgz", + "integrity": "sha512-wGjk9QZVzvknA6yKIUURb8zY3grXCcOZt+/7Wcy8O2uctxhplmUPkOdlgoNhmdVee2c92JXbf1xpMtVNbfoxRw==", + "dev": true, + "requires": { + "@babel/types": "^7.22.5" + } + }, + "@babel/helper-module-imports": { + "version": "7.18.6", + "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.18.6.tgz", + "integrity": "sha512-0NFvs3VkuSYbFi1x2Vd6tKrywq+z/cLeYC/RJNFrIX/30Bf5aiGYbtvGXolEktzJH8o5E5KJ3tT+nkxuuZFVlA==", + "dev": true, + "requires": { + "@babel/types": "^7.18.6" + } + }, + "@babel/helper-module-transforms": { + "version": "7.19.6", + "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.19.6.tgz", + "integrity": "sha512-fCmcfQo/KYr/VXXDIyd3CBGZ6AFhPFy1TfSEJ+PilGVlQT6jcbqtHAM4C1EciRqMza7/TpOUZliuSH+U6HAhJw==", + "dev": true, + "requires": { + "@babel/helper-environment-visitor": "^7.18.9", + "@babel/helper-module-imports": "^7.18.6", + "@babel/helper-simple-access": "^7.19.4", + "@babel/helper-split-export-declaration": "^7.18.6", + "@babel/helper-validator-identifier": "^7.19.1", + "@babel/template": "^7.18.10", + "@babel/traverse": "^7.19.6", + "@babel/types": "^7.19.4" + } + }, + "@babel/helper-plugin-utils": { + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.22.5.tgz", + "integrity": "sha512-uLls06UVKgFG9QD4OeFYLEGteMIAa5kpTPcFL28yuCIIzsf6ZyKZMllKVOCZFhiZ5ptnwX4mtKdWCBE/uT4amg==", + "dev": true + }, + "@babel/helper-simple-access": { + "version": "7.19.4", + "resolved": "https://registry.npmjs.org/@babel/helper-simple-access/-/helper-simple-access-7.19.4.tgz", + "integrity": "sha512-f9Xq6WqBFqaDfbCzn2w85hwklswz5qsKlh7f08w4Y9yhJHpnNC0QemtSkK5YyOY8kPGvyiwdzZksGUhnGdaUIg==", + "dev": true, + "requires": { + "@babel/types": "^7.19.4" + } + }, + "@babel/helper-split-export-declaration": { + "version": "7.22.6", + "resolved": "https://registry.npmjs.org/@babel/helper-split-export-declaration/-/helper-split-export-declaration-7.22.6.tgz", + "integrity": "sha512-AsUnxuLhRYsisFiaJwvp1QF+I3KjD5FOxut14q/GzovUe6orHLesW2C7d754kRm53h5gqrz6sFl6sxc4BVtE/g==", + "dev": true, + "requires": { + "@babel/types": "^7.22.5" + } + }, + "@babel/helper-string-parser": { + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.22.5.tgz", + "integrity": "sha512-mM4COjgZox8U+JcXQwPijIZLElkgEpO5rsERVDJTc2qfCDfERyob6k5WegS14SX18IIjv+XD+GrqNumY5JRCDw==", + "dev": true + }, + "@babel/helper-validator-identifier": { + "version": "7.22.20", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.22.20.tgz", + "integrity": "sha512-Y4OZ+ytlatR8AI+8KZfKuL5urKp7qey08ha31L8b3BwewJAoJamTzyvxPR/5D+KkdJCGPq/+8TukHBlY10FX9A==", + "dev": true + }, + "@babel/helper-validator-option": { + "version": "7.18.6", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.18.6.tgz", + "integrity": "sha512-XO7gESt5ouv/LRJdrVjkShckw6STTaB7l9BrpBaAHDeF5YZT+01PCwmR0SJHnkW6i8OwW/EVWRShfi4j2x+KQw==", + "dev": true + }, + "@babel/helpers": { + "version": "7.20.1", + "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.20.1.tgz", + "integrity": "sha512-J77mUVaDTUJFZ5BpP6mMn6OIl3rEWymk2ZxDBQJUG3P+PbmyMcF3bYWvz0ma69Af1oobDqT/iAsvzhB58xhQUg==", + "dev": true, + "requires": { + "@babel/template": "^7.18.10", + "@babel/traverse": "^7.20.1", + "@babel/types": "^7.20.0" + } + }, + "@babel/highlight": { + "version": "7.22.20", + "resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.22.20.tgz", + "integrity": "sha512-dkdMCN3py0+ksCgYmGG8jKeGA/8Tk+gJwSYYlFGxG5lmhfKNoAy004YpLxpS1W2J8m/EK2Ew+yOs9pVRwO89mg==", + "dev": true, + "requires": { + "@babel/helper-validator-identifier": "^7.22.20", + "chalk": "^2.4.2", + "js-tokens": "^4.0.0" + }, + "dependencies": { + "ansi-styles": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz", + "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==", + "dev": true, + "requires": { + "color-convert": "^1.9.0" + } + }, + "chalk": { + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", + "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==", + "dev": true, + "requires": { + "ansi-styles": "^3.2.1", + "escape-string-regexp": "^1.0.5", + "supports-color": "^5.3.0" + } + }, + "color-convert": { + "version": "1.9.3", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz", + "integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==", + "dev": true, + "requires": { + "color-name": "1.1.3" + } + }, + "color-name": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz", + "integrity": "sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw==", + "dev": true + }, + "escape-string-regexp": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", + "integrity": "sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg==", + "dev": true + }, + "has-flag": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", + "integrity": "sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==", + "dev": true + }, + "supports-color": { + "version": "5.5.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", + "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", + "dev": true, + "requires": { + "has-flag": "^3.0.0" + } + } + } + }, + "@babel/parser": { + "version": "7.23.0", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.23.0.tgz", + "integrity": "sha512-vvPKKdMemU85V9WE/l5wZEmImpCtLqbnTvqDS2U1fJ96KrxoW7KrXhNsNCblQlg8Ck4b85yxdTyelsMUgFUXiw==", + "dev": true + }, + "@babel/plugin-syntax-async-generators": { + "version": "7.8.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-async-generators/-/plugin-syntax-async-generators-7.8.4.tgz", + "integrity": "sha512-tycmZxkGfZaxhMRbXlPXuVFpdWlXpir2W4AMhSJgRKzk/eDlIXOhb2LHWoLpDF7TEHylV5zNhykX6KAgHJmTNw==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.8.0" + } + }, + "@babel/plugin-syntax-bigint": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-bigint/-/plugin-syntax-bigint-7.8.3.tgz", + "integrity": "sha512-wnTnFlG+YxQm3vDxpGE57Pj0srRU4sHE/mDkt1qv2YJJSeUAec2ma4WLUnUPeKjyrfntVwe/N6dCXpU+zL3Npg==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.8.0" + } + }, + "@babel/plugin-syntax-class-properties": { + "version": "7.12.13", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-class-properties/-/plugin-syntax-class-properties-7.12.13.tgz", + "integrity": "sha512-fm4idjKla0YahUNgFNLCB0qySdsoPiZP3iQE3rky0mBUtMZ23yDJ9SJdg6dXTSDnulOVqiF3Hgr9nbXvXTQZYA==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.12.13" + } + }, + "@babel/plugin-syntax-import-meta": { + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-import-meta/-/plugin-syntax-import-meta-7.10.4.tgz", + "integrity": "sha512-Yqfm+XDx0+Prh3VSeEQCPU81yC+JWZ2pDPFSS4ZdpfZhp4MkFMaDC1UqseovEKwSUpnIL7+vK+Clp7bfh0iD7g==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.10.4" + } + }, + "@babel/plugin-syntax-json-strings": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-json-strings/-/plugin-syntax-json-strings-7.8.3.tgz", + "integrity": "sha512-lY6kdGpWHvjoe2vk4WrAapEuBR69EMxZl+RoGRhrFGNYVK8mOPAW8VfbT/ZgrFbXlDNiiaxQnAtgVCZ6jv30EA==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.8.0" + } + }, + "@babel/plugin-syntax-jsx": { + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-jsx/-/plugin-syntax-jsx-7.22.5.tgz", + "integrity": "sha512-gvyP4hZrgrs/wWMaocvxZ44Hw0b3W8Pe+cMxc8V1ULQ07oh8VNbIRaoD1LRZVTvD+0nieDKjfgKg89sD7rrKrg==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.22.5" + } + }, + "@babel/plugin-syntax-logical-assignment-operators": { + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-logical-assignment-operators/-/plugin-syntax-logical-assignment-operators-7.10.4.tgz", + "integrity": "sha512-d8waShlpFDinQ5MtvGU9xDAOzKH47+FFoney2baFIoMr952hKOLp1HR7VszoZvOsV/4+RRszNY7D17ba0te0ig==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.10.4" + } + }, + "@babel/plugin-syntax-nullish-coalescing-operator": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-nullish-coalescing-operator/-/plugin-syntax-nullish-coalescing-operator-7.8.3.tgz", + "integrity": "sha512-aSff4zPII1u2QD7y+F8oDsz19ew4IGEJg9SVW+bqwpwtfFleiQDMdzA/R+UlWDzfnHFCxxleFT0PMIrR36XLNQ==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.8.0" + } + }, + "@babel/plugin-syntax-numeric-separator": { + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-numeric-separator/-/plugin-syntax-numeric-separator-7.10.4.tgz", + "integrity": "sha512-9H6YdfkcK/uOnY/K7/aA2xpzaAgkQn37yzWUMRK7OaPOqOpGS1+n0H5hxT9AUw9EsSjPW8SVyMJwYRtWs3X3ug==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.10.4" + } + }, + "@babel/plugin-syntax-object-rest-spread": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-object-rest-spread/-/plugin-syntax-object-rest-spread-7.8.3.tgz", + "integrity": "sha512-XoqMijGZb9y3y2XskN+P1wUGiVwWZ5JmoDRwx5+3GmEplNyVM2s2Dg8ILFQm8rWM48orGy5YpI5Bl8U1y7ydlA==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.8.0" + } + }, + "@babel/plugin-syntax-optional-catch-binding": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-optional-catch-binding/-/plugin-syntax-optional-catch-binding-7.8.3.tgz", + "integrity": "sha512-6VPD0Pc1lpTqw0aKoeRTMiB+kWhAoT24PA+ksWSBrFtl5SIRVpZlwN3NNPQjehA2E/91FV3RjLWoVTglWcSV3Q==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.8.0" + } + }, + "@babel/plugin-syntax-optional-chaining": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-optional-chaining/-/plugin-syntax-optional-chaining-7.8.3.tgz", + "integrity": "sha512-KoK9ErH1MBlCPxV0VANkXW2/dw4vlbGDrFgz8bmUsBGYkFRcbRwMh6cIJubdPrkxRwuGdtCk0v/wPTKbQgBjkg==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.8.0" + } + }, + "@babel/plugin-syntax-top-level-await": { + "version": "7.14.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-top-level-await/-/plugin-syntax-top-level-await-7.14.5.tgz", + "integrity": "sha512-hx++upLv5U1rgYfwe1xBQUhRmU41NEvpUvrp8jkrSCdvGSnM5/qdRMtylJ6PG5OFkBaHkbTAKTnd3/YyESRHFw==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.14.5" + } + }, + "@babel/plugin-syntax-typescript": { + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-typescript/-/plugin-syntax-typescript-7.22.5.tgz", + "integrity": "sha512-1mS2o03i7t1c6VzH6fdQ3OA8tcEIxwG18zIPRp+UY1Ihv6W+XZzBCVxExF9upussPXJ0xE9XRHwMoNs1ep/nRQ==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.22.5" + } + }, + "@babel/template": { + "version": "7.22.15", + "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.22.15.tgz", + "integrity": "sha512-QPErUVm4uyJa60rkI73qneDacvdvzxshT3kksGqlGWYdOTIUOwJ7RDUL8sGqslY1uXWSL6xMFKEXDS3ox2uF0w==", + "dev": true, + "requires": { + "@babel/code-frame": "^7.22.13", + "@babel/parser": "^7.22.15", + "@babel/types": "^7.22.15" + } + }, + "@babel/traverse": { + "version": "7.23.2", + "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.23.2.tgz", + "integrity": "sha512-azpe59SQ48qG6nu2CzcMLbxUudtN+dOM9kDbUqGq3HXUJRlo7i8fvPoxQUzYgLZ4cMVmuZgm8vvBpNeRhd6XSw==", + "dev": true, + "requires": { + "@babel/code-frame": "^7.22.13", + "@babel/generator": "^7.23.0", + "@babel/helper-environment-visitor": "^7.22.20", + "@babel/helper-function-name": "^7.23.0", + "@babel/helper-hoist-variables": "^7.22.5", + "@babel/helper-split-export-declaration": "^7.22.6", + "@babel/parser": "^7.23.0", + "@babel/types": "^7.23.0", + "debug": "^4.1.0", + "globals": "^11.1.0" + }, + "dependencies": { + "debug": { + "version": "4.3.4", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", + "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==", + "dev": true, + "requires": { + "ms": "2.1.2" + } + }, + "ms": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", + "dev": true + } + } + }, + "@babel/types": { + "version": "7.23.0", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.23.0.tgz", + "integrity": "sha512-0oIyUfKoI3mSqMvsxBdclDwxXKXAUA8v/apZbc+iSyARYou1o8ZGDxbUYyLFoW2arqS2jDGqJuZvv1d/io1axg==", + "dev": true, + "requires": { + "@babel/helper-string-parser": "^7.22.5", + "@babel/helper-validator-identifier": "^7.22.20", + "to-fast-properties": "^2.0.0" + } + }, + "@bcoe/v8-coverage": { + "version": "0.2.3", + "resolved": "https://registry.npmjs.org/@bcoe/v8-coverage/-/v8-coverage-0.2.3.tgz", + "integrity": "sha512-0hYQ8SB4Db5zvZB4axdMHGwEaQjkZzFjQiN9LVYvIFB2nSUHW9tYpxWriPrWDASIxiaXax83REcLxuSdnGPZtw==", + "dev": true + }, + "@colors/colors": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/@colors/colors/-/colors-1.5.0.tgz", + "integrity": "sha512-ooWCrlZP11i8GImSjTHYHLkvFDP48nS4+204nGb1RiX/WXYHmJA2III9/e2DWVabCESdW7hBAEzHRqUn9OUVvQ==" + }, + "@cspotcode/source-map-support": { + "version": "0.8.1", + "resolved": "https://registry.npmjs.org/@cspotcode/source-map-support/-/source-map-support-0.8.1.tgz", + "integrity": "sha512-IchNf6dN4tHoMFIn/7OE8LWZ19Y6q/67Bmf6vnGREv8RSbBVb9LPJxEcnwrcwX6ixSvaiGoomAUvu4YSxXrVgw==", + "requires": { + "@jridgewell/trace-mapping": "0.3.9" + }, + "dependencies": { + "@jridgewell/trace-mapping": { + "version": "0.3.9", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.9.tgz", + "integrity": "sha512-3Belt6tdc8bPgAtbcmdtNJlirVoTmEb5e2gC94PnkwEW9jI6CAHUeoG85tjWP5WquqfavoMtMwiG4P926ZKKuQ==", + "requires": { + "@jridgewell/resolve-uri": "^3.0.3", + "@jridgewell/sourcemap-codec": "^1.4.10" + } + } + } + }, + "@dabh/diagnostics": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/@dabh/diagnostics/-/diagnostics-2.0.3.tgz", + "integrity": "sha512-hrlQOIi7hAfzsMqlGSFyVucrx38O+j6wiGOf//H2ecvIEqYN4ADBSS2iLMh5UFyDunCNniUIPk/q3riFv45xRA==", + "requires": { + "colorspace": "1.1.x", + "enabled": "2.0.x", + "kuler": "^2.0.0" + } + }, + "@eslint-community/eslint-utils": { + "version": "4.4.0", + "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.4.0.tgz", + "integrity": "sha512-1/sA4dwrzBAyeUoQ6oxahHKmrZvsnLCg4RfxW3ZFGGmQkSNQPFNLV9CUEFQP1x9EYXHTo5p6xdhZM1Ne9p/AfA==", + "dev": true, + "requires": { + "eslint-visitor-keys": "^3.3.0" + } + }, + "@eslint-community/regexpp": { + "version": "4.8.0", + "resolved": "https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.8.0.tgz", + "integrity": "sha512-JylOEEzDiOryeUnFbQz+oViCXS0KsvR1mvHkoMiu5+UiBvy+RYX7tzlIIIEstF/gVa2tj9AQXk3dgnxv6KxhFg==", + "dev": true + }, + "@eslint/eslintrc": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-2.1.2.tgz", + "integrity": "sha512-+wvgpDsrB1YqAMdEUCcnTlpfVBH7Vqn6A/NT3D8WVXFIaKMlErPIZT3oCIAVCOtarRpMtelZLqJeU3t7WY6X6g==", + "dev": true, + "requires": { + "ajv": "^6.12.4", + "debug": "^4.3.2", + "espree": "^9.6.0", + "globals": "^13.19.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" + }, + "dependencies": { + "argparse": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", + "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", + "dev": true + }, + "debug": { + "version": "4.3.4", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", + "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==", + "dev": true, + "requires": { + "ms": "2.1.2" + } + }, + "globals": { + "version": "13.21.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-13.21.0.tgz", + "integrity": "sha512-ybyme3s4yy/t/3s35bewwXKOf7cvzfreG2lH0lZl0JB7I4GxRP2ghxOK/Nb9EkRXdbBXZLfq/p/0W2JUONB/Gg==", + "dev": true, + "requires": { + "type-fest": "^0.20.2" + } + }, + "js-yaml": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz", + "integrity": "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==", + "dev": true, + "requires": { + "argparse": "^2.0.1" + } + }, + "ms": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", + "dev": true + }, + "type-fest": { + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.20.2.tgz", + "integrity": "sha512-Ne+eE4r0/iWnpAxD852z3A+N0Bt5RN//NjJwRd2VFHEmrywxf5vsZlh4R6lixl6B+wz/8d+maTSAkN1FIkI3LQ==", + "dev": true + } + } + }, + "@eslint/js": { + "version": "8.52.0", + "resolved": "https://registry.npmjs.org/@eslint/js/-/js-8.52.0.tgz", + "integrity": "sha512-mjZVbpaeMZludF2fsWLD0Z9gCref1Tk4i9+wddjRvpUNqqcndPkBD09N/Mapey0b3jaXbLm2kICwFv2E64QinA==", + "dev": true + }, + "@humanwhocodes/config-array": { + "version": "0.11.13", + "resolved": "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.11.13.tgz", + "integrity": "sha512-JSBDMiDKSzQVngfRjOdFXgFfklaXI4K9nLF49Auh21lmBWRLIK3+xTErTWD4KU54pb6coM6ESE7Awz/FNU3zgQ==", + "dev": true, + "requires": { + "@humanwhocodes/object-schema": "^2.0.1", + "debug": "^4.1.1", + "minimatch": "^3.0.5" + }, + "dependencies": { + "debug": { + "version": "4.3.4", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", + "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==", + "dev": true, + "requires": { + "ms": "2.1.2" + } + }, + "ms": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", + "dev": true + } + } + }, + "@humanwhocodes/module-importer": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@humanwhocodes/module-importer/-/module-importer-1.0.1.tgz", + "integrity": "sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==", + "dev": true + }, + "@humanwhocodes/object-schema": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/@humanwhocodes/object-schema/-/object-schema-2.0.1.tgz", + "integrity": "sha512-dvuCeX5fC9dXgJn9t+X5atfmgQAzUOWqS1254Gh0m6i8wKd10ebXkfNKiRK+1GWi/yTvvLDHpoxLr0xxxeslWw==", + "dev": true + }, + "@isaacs/cliui": { + "version": "8.0.2", + "resolved": "https://registry.npmjs.org/@isaacs/cliui/-/cliui-8.0.2.tgz", + "integrity": "sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==", + "dev": true, + "requires": { + "string-width": "^5.1.2", + "string-width-cjs": "npm:string-width@^4.2.0", + "strip-ansi": "^7.0.1", + "strip-ansi-cjs": "npm:strip-ansi@^6.0.1", + "wrap-ansi": "^8.1.0", + "wrap-ansi-cjs": "npm:wrap-ansi@^7.0.0" + }, + "dependencies": { + "ansi-regex": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.0.1.tgz", + "integrity": "sha512-n5M855fKb2SsfMIiFFoVrABHJC8QtHwVx+mHWP3QcEqBHYienj5dHSgjbxtC0WEZXYt4wcD6zrQElDPhFuZgfA==", + "dev": true + }, + "ansi-styles": { + "version": "6.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.1.tgz", + "integrity": "sha512-bN798gFfQX+viw3R7yrGWRqnrN2oRkEkUjjl4JNn4E8GxxbjtG3FbrEIIY3l8/hrwUwIeCZvi4QuOTP4MErVug==", + "dev": true + }, + "emoji-regex": { + "version": "9.2.2", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-9.2.2.tgz", + "integrity": "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==", + "dev": true + }, + "string-width": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-5.1.2.tgz", + "integrity": "sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==", + "dev": true, + "requires": { + "eastasianwidth": "^0.2.0", + "emoji-regex": "^9.2.2", + "strip-ansi": "^7.0.1" + } + }, + "string-width-cjs": { + "version": "npm:string-width@4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "dev": true, + "requires": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "dependencies": { + "ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "dev": true + }, + "emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "dev": true + }, + "strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, + "requires": { + "ansi-regex": "^5.0.1" + } + } + } + }, + "strip-ansi": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.0.tgz", + "integrity": "sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ==", + "dev": true, + "requires": { + "ansi-regex": "^6.0.1" + } + }, + "strip-ansi-cjs": { + "version": "npm:strip-ansi@6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, + "requires": { + "ansi-regex": "^5.0.1" + }, + "dependencies": { + "ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "dev": true + } + } + }, + "wrap-ansi": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-8.1.0.tgz", + "integrity": "sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ==", + "dev": true, + "requires": { + "ansi-styles": "^6.1.0", + "string-width": "^5.0.1", + "strip-ansi": "^7.0.1" + } + }, + "wrap-ansi-cjs": { + "version": "npm:wrap-ansi@7.0.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", + "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", + "dev": true, + "requires": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "dependencies": { + "ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "dev": true + }, + "ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "requires": { + "color-convert": "^2.0.1" + } + }, + "emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "dev": true + }, + "string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "dev": true, + "requires": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + } + }, + "strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, + "requires": { + "ansi-regex": "^5.0.1" + } + } + } + } + } + }, + "@istanbuljs/load-nyc-config": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@istanbuljs/load-nyc-config/-/load-nyc-config-1.1.0.tgz", + "integrity": "sha512-VjeHSlIzpv/NyD3N0YuHfXOPDIixcA1q2ZV98wsMqcYlPmv2n3Yb2lYP9XMElnaFVXg5A7YLTeLu6V84uQDjmQ==", + "dev": true, + "requires": { + "camelcase": "^5.3.1", + "find-up": "^4.1.0", + "get-package-type": "^0.1.0", + "js-yaml": "^3.13.1", + "resolve-from": "^5.0.0" + } + }, + "@istanbuljs/schema": { + "version": "0.1.3", + "resolved": "https://registry.npmjs.org/@istanbuljs/schema/-/schema-0.1.3.tgz", + "integrity": "sha512-ZXRY4jNvVgSVQ8DL3LTcakaAtXwTVUxE81hslsyD2AtoXW/wVob10HkOJ1X/pAlcI7D+2YoZKg5do8G/w6RYgA==", + "dev": true + }, + "@jest/console": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/console/-/console-29.7.0.tgz", + "integrity": "sha512-5Ni4CU7XHQi32IJ398EEP4RrB8eV09sXP2ROqD4bksHrnTree52PsxvX8tpL8LvTZ3pFzXyPbNQReSN41CAhOg==", + "dev": true, + "requires": { + "@jest/types": "^29.6.3", + "@types/node": "*", + "chalk": "^4.0.0", + "jest-message-util": "^29.7.0", + "jest-util": "^29.7.0", + "slash": "^3.0.0" + } + }, + "@jest/core": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/core/-/core-29.7.0.tgz", + "integrity": "sha512-n7aeXWKMnGtDA48y8TLWJPJmLmmZ642Ceo78cYWEpiD7FzDgmNDV/GCVRorPABdXLJZ/9wzzgZAlHjXjxDHGsg==", + "dev": true, + "requires": { + "@jest/console": "^29.7.0", + "@jest/reporters": "^29.7.0", + "@jest/test-result": "^29.7.0", + "@jest/transform": "^29.7.0", + "@jest/types": "^29.6.3", + "@types/node": "*", + "ansi-escapes": "^4.2.1", + "chalk": "^4.0.0", + "ci-info": "^3.2.0", + "exit": "^0.1.2", + "graceful-fs": "^4.2.9", + "jest-changed-files": "^29.7.0", + "jest-config": "^29.7.0", + "jest-haste-map": "^29.7.0", + "jest-message-util": "^29.7.0", + "jest-regex-util": "^29.6.3", + "jest-resolve": "^29.7.0", + "jest-resolve-dependencies": "^29.7.0", + "jest-runner": "^29.7.0", + "jest-runtime": "^29.7.0", + "jest-snapshot": "^29.7.0", + "jest-util": "^29.7.0", + "jest-validate": "^29.7.0", + "jest-watcher": "^29.7.0", + "micromatch": "^4.0.4", + "pretty-format": "^29.7.0", + "slash": "^3.0.0", + "strip-ansi": "^6.0.0" + } + }, + "@jest/environment": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/environment/-/environment-29.7.0.tgz", + "integrity": "sha512-aQIfHDq33ExsN4jP1NWGXhxgQ/wixs60gDiKO+XVMd8Mn0NWPWgc34ZQDTb2jKaUWQ7MuwoitXAsN2XVXNMpAw==", + "dev": true, + "requires": { + "@jest/fake-timers": "^29.7.0", + "@jest/types": "^29.6.3", + "@types/node": "*", + "jest-mock": "^29.7.0" + } + }, + "@jest/expect": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/expect/-/expect-29.7.0.tgz", + "integrity": "sha512-8uMeAMycttpva3P1lBHB8VciS9V0XAr3GymPpipdyQXbBcuhkLQOSe8E/p92RyAdToS6ZD1tFkX+CkhoECE0dQ==", + "dev": true, + "requires": { + "expect": "^29.7.0", + "jest-snapshot": "^29.7.0" + } + }, + "@jest/expect-utils": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/expect-utils/-/expect-utils-29.7.0.tgz", + "integrity": "sha512-GlsNBWiFQFCVi9QVSx7f5AgMeLxe9YCCs5PuP2O2LdjDAA8Jh9eX7lA1Jq/xdXw3Wb3hyvlFNfZIfcRetSzYcA==", + "dev": true, + "requires": { + "jest-get-type": "^29.6.3" + } + }, + "@jest/fake-timers": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/fake-timers/-/fake-timers-29.7.0.tgz", + "integrity": "sha512-q4DH1Ha4TTFPdxLsqDXK1d3+ioSL7yL5oCMJZgDYm6i+6CygW5E5xVr/D1HdsGxjt1ZWSfUAs9OxSB/BNelWrQ==", + "dev": true, + "requires": { + "@jest/types": "^29.6.3", + "@sinonjs/fake-timers": "^10.0.2", + "@types/node": "*", + "jest-message-util": "^29.7.0", + "jest-mock": "^29.7.0", + "jest-util": "^29.7.0" + } + }, + "@jest/globals": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/globals/-/globals-29.7.0.tgz", + "integrity": "sha512-mpiz3dutLbkW2MNFubUGUEVLkTGiqW6yLVTA+JbP6fI6J5iL9Y0Nlg8k95pcF8ctKwCS7WVxteBs29hhfAotzQ==", + "dev": true, + "requires": { + "@jest/environment": "^29.7.0", + "@jest/expect": "^29.7.0", + "@jest/types": "^29.6.3", + "jest-mock": "^29.7.0" + } + }, + "@jest/reporters": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/reporters/-/reporters-29.7.0.tgz", + "integrity": "sha512-DApq0KJbJOEzAFYjHADNNxAE3KbhxQB1y5Kplb5Waqw6zVbuWatSnMjE5gs8FUgEPmNsnZA3NCWl9NG0ia04Pg==", + "dev": true, + "requires": { + "@bcoe/v8-coverage": "^0.2.3", + "@jest/console": "^29.7.0", + "@jest/test-result": "^29.7.0", + "@jest/transform": "^29.7.0", + "@jest/types": "^29.6.3", + "@jridgewell/trace-mapping": "^0.3.18", + "@types/node": "*", + "chalk": "^4.0.0", + "collect-v8-coverage": "^1.0.0", + "exit": "^0.1.2", + "glob": "^7.1.3", + "graceful-fs": "^4.2.9", + "istanbul-lib-coverage": "^3.0.0", + "istanbul-lib-instrument": "^6.0.0", + "istanbul-lib-report": "^3.0.0", + "istanbul-lib-source-maps": "^4.0.0", + "istanbul-reports": "^3.1.3", + "jest-message-util": "^29.7.0", + "jest-util": "^29.7.0", + "jest-worker": "^29.7.0", + "slash": "^3.0.0", + "string-length": "^4.0.1", + "strip-ansi": "^6.0.0", + "v8-to-istanbul": "^9.0.1" + }, + "dependencies": { + "istanbul-lib-instrument": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/istanbul-lib-instrument/-/istanbul-lib-instrument-6.0.1.tgz", + "integrity": "sha512-EAMEJBsYuyyztxMxW3g7ugGPkrZsV57v0Hmv3mm1uQsmB+QnZuepg731CRaIgeUVSdmsTngOkSnauNF8p7FIhA==", + "dev": true, + "requires": { + "@babel/core": "^7.12.3", + "@babel/parser": "^7.14.7", + "@istanbuljs/schema": "^0.1.2", + "istanbul-lib-coverage": "^3.2.0", + "semver": "^7.5.4" + } + }, + "semver": { + "version": "7.5.4", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.5.4.tgz", + "integrity": "sha512-1bCSESV6Pv+i21Hvpxp3Dx+pSD8lIPt8uVjRrxAUt/nbswYc+tK6Y2btiULjd4+fnq15PX+nqQDC7Oft7WkwcA==", + "dev": true, + "requires": { + "lru-cache": "^6.0.0" + } + } + } + }, + "@jest/schemas": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/@jest/schemas/-/schemas-29.6.3.tgz", + "integrity": "sha512-mo5j5X+jIZmJQveBKeS/clAueipV7KgiX1vMgCxam1RNYiqE1w62n0/tJJnHtjW8ZHcQco5gY85jA3mi0L+nSA==", + "dev": true, + "requires": { + "@sinclair/typebox": "^0.27.8" + } + }, + "@jest/source-map": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/@jest/source-map/-/source-map-29.6.3.tgz", + "integrity": "sha512-MHjT95QuipcPrpLM+8JMSzFx6eHp5Bm+4XeFDJlwsvVBjmKNiIAvasGK2fxz2WbGRlnvqehFbh07MMa7n3YJnw==", + "dev": true, + "requires": { + "@jridgewell/trace-mapping": "^0.3.18", + "callsites": "^3.0.0", + "graceful-fs": "^4.2.9" + } + }, + "@jest/test-result": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/test-result/-/test-result-29.7.0.tgz", + "integrity": "sha512-Fdx+tv6x1zlkJPcWXmMDAG2HBnaR9XPSd5aDWQVsfrZmLVT3lU1cwyxLgRmXR9yrq4NBoEm9BMsfgFzTQAbJYA==", + "dev": true, + "requires": { + "@jest/console": "^29.7.0", + "@jest/types": "^29.6.3", + "@types/istanbul-lib-coverage": "^2.0.0", + "collect-v8-coverage": "^1.0.0" + } + }, + "@jest/test-sequencer": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/test-sequencer/-/test-sequencer-29.7.0.tgz", + "integrity": "sha512-GQwJ5WZVrKnOJuiYiAF52UNUJXgTZx1NHjFSEB0qEMmSZKAkdMoIzw/Cj6x6NF4AvV23AUqDpFzQkN/eYCYTxw==", + "dev": true, + "requires": { + "@jest/test-result": "^29.7.0", + "graceful-fs": "^4.2.9", + "jest-haste-map": "^29.7.0", + "slash": "^3.0.0" + } + }, + "@jest/transform": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/transform/-/transform-29.7.0.tgz", + "integrity": "sha512-ok/BTPFzFKVMwO5eOHRrvnBVHdRy9IrsrW1GpMaQ9MCnilNLXQKmAX8s1YXDFaai9xJpac2ySzV0YeRRECr2Vw==", + "dev": true, + "requires": { + "@babel/core": "^7.11.6", + "@jest/types": "^29.6.3", + "@jridgewell/trace-mapping": "^0.3.18", + "babel-plugin-istanbul": "^6.1.1", + "chalk": "^4.0.0", + "convert-source-map": "^2.0.0", + "fast-json-stable-stringify": "^2.1.0", + "graceful-fs": "^4.2.9", + "jest-haste-map": "^29.7.0", + "jest-regex-util": "^29.6.3", + "jest-util": "^29.7.0", + "micromatch": "^4.0.4", + "pirates": "^4.0.4", + "slash": "^3.0.0", + "write-file-atomic": "^4.0.2" + }, + "dependencies": { + "convert-source-map": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz", + "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==", + "dev": true + } + } + }, + "@jest/types": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/@jest/types/-/types-29.6.3.tgz", + "integrity": "sha512-u3UPsIilWKOM3F9CXtrG8LEJmNxwoCQC/XVj4IKYXvvpx7QIi/Kg1LI5uDmDpKlac62NUtX7eLjRh+jVZcLOzw==", + "dev": true, + "requires": { + "@jest/schemas": "^29.6.3", + "@types/istanbul-lib-coverage": "^2.0.0", + "@types/istanbul-reports": "^3.0.0", + "@types/node": "*", + "@types/yargs": "^17.0.8", + "chalk": "^4.0.0" + } + }, + "@jridgewell/gen-mapping": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.1.1.tgz", + "integrity": "sha512-sQXCasFk+U8lWYEe66WxRDOE9PjVz4vSM51fTu3Hw+ClTpUSQb718772vH3pyS5pShp6lvQM7SxgIDXXXmOX7w==", + "dev": true, + "requires": { + "@jridgewell/set-array": "^1.0.0", + "@jridgewell/sourcemap-codec": "^1.4.10" + } + }, + "@jridgewell/resolve-uri": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.0.tgz", + "integrity": "sha512-F2msla3tad+Mfht5cJq7LSXcdudKTWCVYUgw6pLFOOHSTtZlj6SWNYAp+AhuqLmWdBO2X5hPrLcu8cVP8fy28w==" + }, + "@jridgewell/set-array": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@jridgewell/set-array/-/set-array-1.1.2.tgz", + "integrity": "sha512-xnkseuNADM0gt2bs+BvhO0p78Mk762YnZdsuzFV018NoG1Sj1SCQvpSqa7XUaTam5vAGasABV9qXASMKnFMwMw==", + "dev": true + }, + "@jridgewell/sourcemap-codec": { + "version": "1.4.14", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.4.14.tgz", + "integrity": "sha512-XPSJHWmi394fuUuzDnGz1wiKqWfo1yXecHQMRf2l6hztTO+nPru658AyDngaBe7isIxEkRsPR3FZh+s7iVa4Uw==" + }, + "@jridgewell/trace-mapping": { + "version": "0.3.19", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.19.tgz", + "integrity": "sha512-kf37QtfW+Hwx/buWGMPcR60iF9ziHa6r/CZJIHbmcm4+0qrXiVdxegAH0F6yddEVQ7zdkjcGCgCzUu+BcbhQxw==", + "dev": true, + "requires": { + "@jridgewell/resolve-uri": "^3.1.0", + "@jridgewell/sourcemap-codec": "^1.4.14" + } + }, + "@nodelib/fs.scandir": { + "version": "2.1.5", + "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", + "integrity": "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==", + "dev": true, + "requires": { + "@nodelib/fs.stat": "2.0.5", + "run-parallel": "^1.1.9" + } + }, + "@nodelib/fs.stat": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz", + "integrity": "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==", + "dev": true + }, + "@nodelib/fs.walk": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/@nodelib/fs.walk/-/fs.walk-1.2.8.tgz", + "integrity": "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==", + "dev": true, + "requires": { + "@nodelib/fs.scandir": "2.1.5", + "fastq": "^1.6.0" + } + }, + "@pkgjs/parseargs": { + "version": "0.11.0", + "resolved": "https://registry.npmjs.org/@pkgjs/parseargs/-/parseargs-0.11.0.tgz", + "integrity": "sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==", + "dev": true, + "optional": true + }, + "@pkgr/utils": { + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/@pkgr/utils/-/utils-2.4.2.tgz", + "integrity": "sha512-POgTXhjrTfbTV63DiFXav4lBHiICLKKwDeaKn9Nphwj7WH6m0hMMCaJkMyRWjgtPFyRKRVoMXXjczsTQRDEhYw==", + "dev": true, + "requires": { + "cross-spawn": "^7.0.3", + "fast-glob": "^3.3.0", + "is-glob": "^4.0.3", + "open": "^9.1.0", + "picocolors": "^1.0.0", + "tslib": "^2.6.0" + }, + "dependencies": { + "tslib": { + "version": "2.6.2", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.6.2.tgz", + "integrity": "sha512-AEYxH93jGFPn/a2iVAwW87VuUIkR1FVUKB77NwMF7nBTDkDrrT/Hpt/IrCJ0QXhW27jTBDcf5ZY7w6RiqTMw2Q==", + "dev": true + } + } + }, + "@sinclair/typebox": { + "version": "0.27.8", + "resolved": "https://registry.npmjs.org/@sinclair/typebox/-/typebox-0.27.8.tgz", + "integrity": "sha512-+Fj43pSMwJs4KRrH/938Uf+uAELIgVBmQzg/q1YG10djyfA3TnrU8N8XzqCh/okZdszqBQTZf96idMfE5lnwTA==", + "dev": true + }, + "@sinonjs/commons": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/@sinonjs/commons/-/commons-3.0.0.tgz", + "integrity": "sha512-jXBtWAF4vmdNmZgD5FoKsVLv3rPgDnLgPbU84LIJ3otV44vJlDRokVng5v8NFJdCf/da9legHcKaRuZs4L7faA==", + "dev": true, + "requires": { + "type-detect": "4.0.8" + } + }, + "@sinonjs/fake-timers": { + "version": "10.3.0", + "resolved": "https://registry.npmjs.org/@sinonjs/fake-timers/-/fake-timers-10.3.0.tgz", + "integrity": "sha512-V4BG07kuYSUkTCSBHG8G8TNhM+F19jXFWnQtzj+we8DrkpSBCee9Z3Ms8yiGer/dlmhe35/Xdgyo3/0rQKg7YA==", + "dev": true, + "requires": { + "@sinonjs/commons": "^3.0.0" + } + }, + "@tsconfig/node10": { + "version": "1.0.9", + "resolved": "https://registry.npmjs.org/@tsconfig/node10/-/node10-1.0.9.tgz", + "integrity": "sha512-jNsYVVxU8v5g43Erja32laIDHXeoNvFEpX33OK4d6hljo3jDhCBDhx5dhCCTMWUojscpAagGiRkBKxpdl9fxqA==" + }, + "@tsconfig/node12": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/@tsconfig/node12/-/node12-1.0.11.tgz", + "integrity": "sha512-cqefuRsh12pWyGsIoBKJA9luFu3mRxCA+ORZvA4ktLSzIuCUtWVxGIuXigEwO5/ywWFMZ2QEGKWvkZG1zDMTag==" + }, + "@tsconfig/node14": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@tsconfig/node14/-/node14-1.0.3.tgz", + "integrity": "sha512-ysT8mhdixWK6Hw3i1V2AeRqZ5WfXg1G43mqoYlM2nc6388Fq5jcXyr5mRsqViLx/GJYdoL0bfXD8nmF+Zn/Iow==" + }, + "@tsconfig/node16": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@tsconfig/node16/-/node16-1.0.3.tgz", + "integrity": "sha512-yOlFc+7UtL/89t2ZhjPvvB/DeAr3r+Dq58IgzsFkOAvVC6NMJXmCGjbptdXdR9qsX7pKcTL+s87FtYREi2dEEQ==" + }, + "@types/babel__core": { + "version": "7.20.1", + "resolved": "https://registry.npmjs.org/@types/babel__core/-/babel__core-7.20.1.tgz", + "integrity": "sha512-aACu/U/omhdk15O4Nfb+fHgH/z3QsfQzpnvRZhYhThms83ZnAOZz7zZAWO7mn2yyNQaA4xTO8GLK3uqFU4bYYw==", + "dev": true, + "requires": { + "@babel/parser": "^7.20.7", + "@babel/types": "^7.20.7", + "@types/babel__generator": "*", + "@types/babel__template": "*", + "@types/babel__traverse": "*" + } + }, + "@types/babel__generator": { + "version": "7.6.4", + "resolved": "https://registry.npmjs.org/@types/babel__generator/-/babel__generator-7.6.4.tgz", + "integrity": "sha512-tFkciB9j2K755yrTALxD44McOrk+gfpIpvC3sxHjRawj6PfnQxrse4Clq5y/Rq+G3mrBurMax/lG8Qn2t9mSsg==", + "dev": true, + "requires": { + "@babel/types": "^7.0.0" + } + }, + "@types/babel__template": { + "version": "7.4.1", + "resolved": "https://registry.npmjs.org/@types/babel__template/-/babel__template-7.4.1.tgz", + "integrity": "sha512-azBFKemX6kMg5Io+/rdGT0dkGreboUVR0Cdm3fz9QJWpaQGJRQXl7C+6hOTCZcMll7KFyEQpgbYI2lHdsS4U7g==", + "dev": true, + "requires": { + "@babel/parser": "^7.1.0", + "@babel/types": "^7.0.0" + } + }, + "@types/babel__traverse": { + "version": "7.20.1", + "resolved": "https://registry.npmjs.org/@types/babel__traverse/-/babel__traverse-7.20.1.tgz", + "integrity": "sha512-MitHFXnhtgwsGZWtT68URpOvLN4EREih1u3QtQiN4VdAxWKRVvGCSvw/Qth0M0Qq3pJpnGOu5JaM/ydK7OGbqg==", + "dev": true, + "requires": { + "@babel/types": "^7.20.7" + } + }, + "@types/body-parser": { + "version": "1.19.2", + "resolved": "https://registry.npmjs.org/@types/body-parser/-/body-parser-1.19.2.tgz", + "integrity": "sha512-ALYone6pm6QmwZoAgeyNksccT9Q4AWZQ6PvfwR37GT6r6FWUPguq6sUmNGSMV2Wr761oQoBxwGGa6DR5o1DC9g==", + "dev": true, + "requires": { + "@types/connect": "*", + "@types/node": "*" + } + }, + "@types/compression": { + "version": "1.7.4", + "resolved": "https://registry.npmjs.org/@types/compression/-/compression-1.7.4.tgz", + "integrity": "sha512-sdFVnQJRkQBX83ydsLCBm4A39p45y0QkxdAR689yOtAFNbbS9Acrp86RZWJj6BHRXyZH9tX4t1dU7XDiGdY3nA==", + "dev": true, + "requires": { + "@types/express": "*" + } + }, + "@types/config": { + "version": "3.3.2", + "resolved": "https://registry.npmjs.org/@types/config/-/config-3.3.2.tgz", + "integrity": "sha512-NAxsgr73BakGnfpHq5eKmtba/x+JyPcZmSa0DzCTdR4JCxeSC+KpwSQuOMGvACs7axkjGqi44/zYRkhiX7aDew==", + "dev": true + }, + "@types/connect": { + "version": "3.4.35", + "resolved": "https://registry.npmjs.org/@types/connect/-/connect-3.4.35.tgz", + "integrity": "sha512-cdeYyv4KWoEgpBISTxWvqYsVy444DOqehiF3fM3ne10AmJ62RSyNkUnxMJXHQWRQQX2eR94m5y1IZyDwBjV9FQ==", + "dev": true, + "requires": { + "@types/node": "*" + } + }, + "@types/express": { + "version": "4.17.20", + "resolved": "https://registry.npmjs.org/@types/express/-/express-4.17.20.tgz", + "integrity": "sha512-rOaqlkgEvOW495xErXMsmyX3WKBInbhG5eqojXYi3cGUaLoRDlXa5d52fkfWZT963AZ3v2eZ4MbKE6WpDAGVsw==", + "dev": true, + "requires": { + "@types/body-parser": "*", + "@types/express-serve-static-core": "^4.17.33", + "@types/qs": "*", + "@types/serve-static": "*" + } + }, + "@types/express-serve-static-core": { + "version": "4.17.33", + "resolved": "https://registry.npmjs.org/@types/express-serve-static-core/-/express-serve-static-core-4.17.33.tgz", + "integrity": "sha512-TPBqmR/HRYI3eC2E5hmiivIzv+bidAfXofM+sbonAGvyDhySGw9/PQZFt2BLOrjUUR++4eJVpx6KnLQK1Fk9tA==", + "dev": true, + "requires": { + "@types/node": "*", + "@types/qs": "*", + "@types/range-parser": "*" + } + }, + "@types/graceful-fs": { + "version": "4.1.8", + "resolved": "https://registry.npmjs.org/@types/graceful-fs/-/graceful-fs-4.1.8.tgz", + "integrity": "sha512-NhRH7YzWq8WiNKVavKPBmtLYZHxNY19Hh+az28O/phfp68CF45pMFud+ZzJ8ewnxnC5smIdF3dqFeiSUQ5I+pw==", + "dev": true, + "requires": { + "@types/node": "*" + } + }, + "@types/istanbul-lib-coverage": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/@types/istanbul-lib-coverage/-/istanbul-lib-coverage-2.0.4.tgz", + "integrity": "sha512-z/QT1XN4K4KYuslS23k62yDIDLwLFkzxOuMplDtObz0+y7VqJCaO2o+SPwHCvLFZh7xazvvoor2tA/hPz9ee7g==", + "dev": true + }, + "@types/istanbul-lib-report": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/@types/istanbul-lib-report/-/istanbul-lib-report-3.0.0.tgz", + "integrity": "sha512-plGgXAPfVKFoYfa9NpYDAkseG+g6Jr294RqeqcqDixSbU34MZVJRi/P+7Y8GDpzkEwLaGZZOpKIEmeVZNtKsrg==", + "dev": true, + "requires": { + "@types/istanbul-lib-coverage": "*" + } + }, + "@types/istanbul-reports": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/@types/istanbul-reports/-/istanbul-reports-3.0.1.tgz", + "integrity": "sha512-c3mAZEuK0lvBp8tmuL74XRKn1+y2dcwOUpH7x4WrF6gk1GIgiluDRgMYQtw2OFcBvAJWlt6ASU3tSqxp0Uu0Aw==", + "dev": true, + "requires": { + "@types/istanbul-lib-report": "*" + } + }, + "@types/jest": { + "version": "29.5.7", + "resolved": "https://registry.npmjs.org/@types/jest/-/jest-29.5.7.tgz", + "integrity": "sha512-HLyetab6KVPSiF+7pFcUyMeLsx25LDNDemw9mGsJBkai/oouwrjTycocSDYopMEwFhN2Y4s9oPyOCZNofgSt2g==", + "dev": true, + "requires": { + "expect": "^29.0.0", + "pretty-format": "^29.0.0" + } + }, + "@types/json-schema": { + "version": "7.0.14", + "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.14.tgz", + "integrity": "sha512-U3PUjAudAdJBeC2pgN8uTIKgxrb4nlDF3SF0++EldXQvQBGkpFZMSnwQiIoDU77tv45VgNkl/L4ouD+rEomujw==", + "dev": true + }, + "@types/json5": { + "version": "0.0.29", + "resolved": "https://registry.npmjs.org/@types/json5/-/json5-0.0.29.tgz", + "integrity": "sha512-dRLjCWHYg4oaA77cxO64oO+7JwCwnIzkZPdrrC71jQmQtlhM556pwKo5bUzqvZndkVbeFLIIi+9TC40JNF5hNQ==", + "dev": true + }, + "@types/mime": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/@types/mime/-/mime-3.0.1.tgz", + "integrity": "sha512-Y4XFY5VJAuw0FgAqPNd6NNoV44jbq9Bz2L7Rh/J6jLTiHBSBJa9fxqQIvkIld4GsoDOcCbvzOUAbLPsSKKg+uA==", + "dev": true + }, + "@types/node": { + "version": "20.8.10", + "resolved": "https://registry.npmjs.org/@types/node/-/node-20.8.10.tgz", + "integrity": "sha512-TlgT8JntpcbmKUFzjhsyhGfP2fsiz1Mv56im6enJ905xG1DAYesxJaeSbGqQmAw8OWPdhyJGhGSQGKRNJ45u9w==", + "dev": true, + "requires": { + "undici-types": "~5.26.4" + } + }, + "@types/qs": { + "version": "6.9.7", + "resolved": "https://registry.npmjs.org/@types/qs/-/qs-6.9.7.tgz", + "integrity": "sha512-FGa1F62FT09qcrueBA6qYTrJPVDzah9a+493+o2PCXsesWHIn27G98TsSMs3WPNbZIEj4+VJf6saSFpvD+3Zsw==", + "dev": true + }, + "@types/range-parser": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@types/range-parser/-/range-parser-1.2.4.tgz", + "integrity": "sha512-EEhsLsD6UsDM1yFhAvy0Cjr6VwmpMWqFBCb9w07wVugF7w9nfajxLuVmngTIpgS6svCnm6Vaw+MZhoDCKnOfsw==", + "dev": true + }, + "@types/semver": { + "version": "7.5.4", + "resolved": "https://registry.npmjs.org/@types/semver/-/semver-7.5.4.tgz", + "integrity": "sha512-MMzuxN3GdFwskAnb6fz0orFvhfqi752yjaXylr0Rp4oDg5H0Zn1IuyRhDVvYOwAXoJirx2xuS16I3WjxnAIHiQ==", + "dev": true + }, + "@types/serve-static": { + "version": "1.15.0", + "resolved": "https://registry.npmjs.org/@types/serve-static/-/serve-static-1.15.0.tgz", + "integrity": "sha512-z5xyF6uh8CbjAu9760KDKsH2FcDxZ2tFCsA4HIMWE6IkiYMXfVoa+4f9KX+FN0ZLsaMw1WNG2ETLA6N+/YA+cg==", + "dev": true, + "requires": { + "@types/mime": "*", + "@types/node": "*" + } + }, + "@types/stack-utils": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/@types/stack-utils/-/stack-utils-2.0.1.tgz", + "integrity": "sha512-Hl219/BT5fLAaz6NDkSuhzasy49dwQS/DSdu4MdggFB8zcXv7vflBI3xp7FEmkmdDkBUI2bPUNeMttp2knYdxw==", + "dev": true + }, + "@types/strip-bom": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/@types/strip-bom/-/strip-bom-3.0.0.tgz", + "integrity": "sha512-xevGOReSYGM7g/kUBZzPqCrR/KYAo+F0yiPc85WFTJa0MSLtyFTVTU6cJu/aV4mid7IffDIWqo69THF2o4JiEQ==", + "dev": true + }, + "@types/strip-json-comments": { + "version": "0.0.30", + "resolved": "https://registry.npmjs.org/@types/strip-json-comments/-/strip-json-comments-0.0.30.tgz", + "integrity": "sha512-7NQmHra/JILCd1QqpSzl8+mJRc8ZHz3uDm8YV1Ks9IhK0epEiTw8aIErbvH9PI+6XbqhyIQy3462nEsn7UVzjQ==", + "dev": true + }, + "@types/yargs": { + "version": "17.0.13", + "resolved": "https://registry.npmjs.org/@types/yargs/-/yargs-17.0.13.tgz", + "integrity": "sha512-9sWaruZk2JGxIQU+IhI1fhPYRcQ0UuTNuKuCW9bR5fp7qi2Llf7WDzNa17Cy7TKnh3cdxDOiyTu6gaLS0eDatg==", + "dev": true, + "requires": { + "@types/yargs-parser": "*" + } + }, + "@types/yargs-parser": { + "version": "21.0.0", + "resolved": "https://registry.npmjs.org/@types/yargs-parser/-/yargs-parser-21.0.0.tgz", + "integrity": "sha512-iO9ZQHkZxHn4mSakYV0vFHAVDyEOIJQrV2uZ06HxEPcx+mt8swXoZHIbaaJ2crJYFfErySgktuTZ3BeLz+XmFA==", + "dev": true + }, + "@typescript-eslint/eslint-plugin": { + "version": "6.9.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-6.9.1.tgz", + "integrity": "sha512-w0tiiRc9I4S5XSXXrMHOWgHgxbrBn1Ro+PmiYhSg2ZVdxrAJtQgzU5o2m1BfP6UOn7Vxcc6152vFjQfmZR4xEg==", + "dev": true, + "requires": { + "@eslint-community/regexpp": "^4.5.1", + "@typescript-eslint/scope-manager": "6.9.1", + "@typescript-eslint/type-utils": "6.9.1", + "@typescript-eslint/utils": "6.9.1", + "@typescript-eslint/visitor-keys": "6.9.1", + "debug": "^4.3.4", + "graphemer": "^1.4.0", + "ignore": "^5.2.4", + "natural-compare": "^1.4.0", + "semver": "^7.5.4", + "ts-api-utils": "^1.0.1" + }, + "dependencies": { + "debug": { + "version": "4.3.4", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", + "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==", + "dev": true, + "requires": { + "ms": "2.1.2" + } + }, + "ms": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", + "dev": true + }, + "semver": { + "version": "7.5.4", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.5.4.tgz", + "integrity": "sha512-1bCSESV6Pv+i21Hvpxp3Dx+pSD8lIPt8uVjRrxAUt/nbswYc+tK6Y2btiULjd4+fnq15PX+nqQDC7Oft7WkwcA==", + "dev": true, + "requires": { + "lru-cache": "^6.0.0" + } + } + } + }, + "@typescript-eslint/parser": { + "version": "6.9.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-6.9.1.tgz", + "integrity": "sha512-C7AK2wn43GSaCUZ9do6Ksgi2g3mwFkMO3Cis96kzmgudoVaKyt62yNzJOktP0HDLb/iO2O0n2lBOzJgr6Q/cyg==", + "dev": true, + "requires": { + "@typescript-eslint/scope-manager": "6.9.1", + "@typescript-eslint/types": "6.9.1", + "@typescript-eslint/typescript-estree": "6.9.1", + "@typescript-eslint/visitor-keys": "6.9.1", + "debug": "^4.3.4" + }, + "dependencies": { + "debug": { + "version": "4.3.4", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", + "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==", + "dev": true, + "requires": { + "ms": "2.1.2" + } + }, + "ms": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", + "dev": true + } + } + }, + "@typescript-eslint/scope-manager": { + "version": "6.9.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-6.9.1.tgz", + "integrity": "sha512-38IxvKB6NAne3g/+MyXMs2Cda/Sz+CEpmm+KLGEM8hx/CvnSRuw51i8ukfwB/B/sESdeTGet1NH1Wj7I0YXswg==", + "dev": true, + "requires": { + "@typescript-eslint/types": "6.9.1", + "@typescript-eslint/visitor-keys": "6.9.1" + } + }, + "@typescript-eslint/type-utils": { + "version": "6.9.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-6.9.1.tgz", + "integrity": "sha512-eh2oHaUKCK58qIeYp19F5V5TbpM52680sB4zNSz29VBQPTWIlE/hCj5P5B1AChxECe/fmZlspAWFuRniep1Skg==", + "dev": true, + "requires": { + "@typescript-eslint/typescript-estree": "6.9.1", + "@typescript-eslint/utils": "6.9.1", + "debug": "^4.3.4", + "ts-api-utils": "^1.0.1" + }, + "dependencies": { + "debug": { + "version": "4.3.4", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", + "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==", + "dev": true, + "requires": { + "ms": "2.1.2" + } + }, + "ms": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", + "dev": true + } + } + }, + "@typescript-eslint/types": { + "version": "6.9.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-6.9.1.tgz", + "integrity": "sha512-BUGslGOb14zUHOUmDB2FfT6SI1CcZEJYfF3qFwBeUrU6srJfzANonwRYHDpLBuzbq3HaoF2XL2hcr01c8f8OaQ==", + "dev": true + }, + "@typescript-eslint/typescript-estree": { + "version": "6.9.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-6.9.1.tgz", + "integrity": "sha512-U+mUylTHfcqeO7mLWVQ5W/tMLXqVpRv61wm9ZtfE5egz7gtnmqVIw9ryh0mgIlkKk9rZLY3UHygsBSdB9/ftyw==", + "dev": true, + "requires": { + "@typescript-eslint/types": "6.9.1", + "@typescript-eslint/visitor-keys": "6.9.1", + "debug": "^4.3.4", + "globby": "^11.1.0", + "is-glob": "^4.0.3", + "semver": "^7.5.4", + "ts-api-utils": "^1.0.1" + }, + "dependencies": { + "debug": { + "version": "4.3.4", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", + "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==", + "dev": true, + "requires": { + "ms": "2.1.2" + } + }, + "ms": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", + "dev": true + }, + "semver": { + "version": "7.5.4", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.5.4.tgz", + "integrity": "sha512-1bCSESV6Pv+i21Hvpxp3Dx+pSD8lIPt8uVjRrxAUt/nbswYc+tK6Y2btiULjd4+fnq15PX+nqQDC7Oft7WkwcA==", + "dev": true, + "requires": { + "lru-cache": "^6.0.0" + } + } + } + }, + "@typescript-eslint/utils": { + "version": "6.9.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-6.9.1.tgz", + "integrity": "sha512-L1T0A5nFdQrMVunpZgzqPL6y2wVreSyHhKGZryS6jrEN7bD9NplVAyMryUhXsQ4TWLnZmxc2ekar/lSGIlprCA==", + "dev": true, + "requires": { + "@eslint-community/eslint-utils": "^4.4.0", + "@types/json-schema": "^7.0.12", + "@types/semver": "^7.5.0", + "@typescript-eslint/scope-manager": "6.9.1", + "@typescript-eslint/types": "6.9.1", + "@typescript-eslint/typescript-estree": "6.9.1", + "semver": "^7.5.4" + }, + "dependencies": { + "semver": { + "version": "7.5.4", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.5.4.tgz", + "integrity": "sha512-1bCSESV6Pv+i21Hvpxp3Dx+pSD8lIPt8uVjRrxAUt/nbswYc+tK6Y2btiULjd4+fnq15PX+nqQDC7Oft7WkwcA==", + "dev": true, + "requires": { + "lru-cache": "^6.0.0" + } + } + } + }, + "@typescript-eslint/visitor-keys": { + "version": "6.9.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-6.9.1.tgz", + "integrity": "sha512-MUaPUe/QRLEffARsmNfmpghuQkW436DvESW+h+M52w0coICHRfD6Np9/K6PdACwnrq1HmuLl+cSPZaJmeVPkSw==", + "dev": true, + "requires": { + "@typescript-eslint/types": "6.9.1", + "eslint-visitor-keys": "^3.4.1" + } + }, + "@ungap/structured-clone": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/@ungap/structured-clone/-/structured-clone-1.2.0.tgz", + "integrity": "sha512-zuVdFrMJiuCDQUMCzQaD6KL28MjnqqN8XnAqiEq9PNm/hCPTSGfrXCOfwj1ow4LFb/tNymJPwsNbVePc1xFqrQ==", + "dev": true + }, + "accepts": { + "version": "1.3.8", + "resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.8.tgz", + "integrity": "sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw==", + "requires": { + "mime-types": "~2.1.34", + "negotiator": "0.6.3" + } + }, + "acorn": { + "version": "8.10.0", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.10.0.tgz", + "integrity": "sha512-F0SAmZ8iUtS//m8DmCTA0jlh6TDKkHQyK6xc6V4KDTyZKA9dnvX9/3sRTVQrWm79glUAZbnmmNcdYwUIHWVybw==" + }, + "acorn-jsx": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.3.2.tgz", + "integrity": "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==", + "dev": true + }, + "acorn-walk": { + "version": "8.2.0", + "resolved": "https://registry.npmjs.org/acorn-walk/-/acorn-walk-8.2.0.tgz", + "integrity": "sha512-k+iyHEuPgSw6SbuDpGQM+06HQUa04DZ3o+F6CSzXMvvI5KMvnaEqXe+YVe555R9nn6GPt404fos4wcgpw12SDA==" + }, + "ajv": { + "version": "6.12.6", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", + "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", + "dev": true, + "requires": { + "fast-deep-equal": "^3.1.1", + "fast-json-stable-stringify": "^2.0.0", + "json-schema-traverse": "^0.4.1", + "uri-js": "^4.2.2" + } + }, + "ansi-escapes": { + "version": "4.3.2", + "resolved": "https://registry.npmjs.org/ansi-escapes/-/ansi-escapes-4.3.2.tgz", + "integrity": "sha512-gKXj5ALrKWQLsYG9jlTRmR/xKluxHV+Z9QEwNIgCfM1/uwPMCuzVVnh5mwTd+OuBZcwSIMbqssNWRm1lE51QaQ==", + "dev": true, + "requires": { + "type-fest": "^0.21.3" + } + }, + "ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "dev": true + }, + "ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "requires": { + "color-convert": "^2.0.1" + } + }, + "anymatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.2.tgz", + "integrity": "sha512-P43ePfOAIupkguHUycrc4qJ9kz8ZiuOUijaETwX7THt0Y/GNK7v0aa8rY816xWjZ7rJdA5XdMcpVFTKMq+RvWg==", + "dev": true, + "requires": { + "normalize-path": "^3.0.0", + "picomatch": "^2.0.4" + } + }, + "api-problem": { + "version": "9.0.2", + "resolved": "https://registry.npmjs.org/api-problem/-/api-problem-9.0.2.tgz", + "integrity": "sha512-Xr4TyFkyvTEkgL8zUhyoqeK2Oxx2GQaFIPNiuhVRfop34gNl5r5gk1jYMsQhtPWSpwavROVweNIyL677ibE4rw==" + }, + "arg": { + "version": "4.1.3", + "resolved": "https://registry.npmjs.org/arg/-/arg-4.1.3.tgz", + "integrity": "sha512-58S9QDqG0Xx27YwPSt9fJxivjYl432YCwfDMfZ+71RAqUrZef7LrKQZ3LHLOwCS4FLNBplP533Zx895SeOCHvA==" + }, + "argparse": { + "version": "1.0.10", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-1.0.10.tgz", + "integrity": "sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==", + "dev": true, + "requires": { + "sprintf-js": "~1.0.2" + } + }, + "array-flatten": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-1.1.1.tgz", + "integrity": "sha512-PCVAQswWemu6UdxsDFFX/+gVeYqKAod3D3UVm91jHwynguOwAvYPhx8nNlM++NqRcK6CxxpUafjmhIdKiHibqg==" + }, + "array-includes": { + "version": "3.1.5", + "resolved": "https://registry.npmjs.org/array-includes/-/array-includes-3.1.5.tgz", + "integrity": "sha512-iSDYZMMyTPkiFasVqfuAQnWAYcvO/SeBSCGKePoEthjp4LEMTe4uLc7b025o4jAZpHhihh8xPo99TNWUWWkGDQ==", + "dev": true, + "requires": { + "call-bind": "^1.0.2", + "define-properties": "^1.1.4", + "es-abstract": "^1.19.5", + "get-intrinsic": "^1.1.1", + "is-string": "^1.0.7" + } + }, + "array-union": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/array-union/-/array-union-2.1.0.tgz", + "integrity": "sha512-HGyxoOTYUyCM6stUe6EJgnd4EoewAI7zMdfqO+kGjnlZmBDz/cR5pf8r/cR4Wq60sL/p0IkcjUEEPwS3GFrIyw==", + "dev": true + }, + "array.prototype.flat": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/array.prototype.flat/-/array.prototype.flat-1.3.0.tgz", + "integrity": "sha512-12IUEkHsAhA4DY5s0FPgNXIdc8VRSqD9Zp78a5au9abH/SOBrsp082JOWFNTjkMozh8mqcdiKuaLGhPeYztxSw==", + "dev": true, + "requires": { + "call-bind": "^1.0.2", + "define-properties": "^1.1.3", + "es-abstract": "^1.19.2", + "es-shim-unscopables": "^1.0.0" + } + }, + "array.prototype.flatmap": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/array.prototype.flatmap/-/array.prototype.flatmap-1.3.0.tgz", + "integrity": "sha512-PZC9/8TKAIxcWKdyeb77EzULHPrIX/tIZebLJUQOMR1OwYosT8yggdfWScfTBCDj5utONvOuPQQumYsU2ULbkg==", + "dev": true, + "requires": { + "call-bind": "^1.0.2", + "define-properties": "^1.1.3", + "es-abstract": "^1.19.2", + "es-shim-unscopables": "^1.0.0" + } + }, + "astral-regex": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/astral-regex/-/astral-regex-1.0.0.tgz", + "integrity": "sha512-+Ryf6g3BKoRc7jfp7ad8tM4TtMiaWvbF/1/sQcZPkkS7ag3D5nMBCe2UfOTONtAkaG0tO0ij3C5Lwmf1EiyjHg==", + "dev": true + }, + "async": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/async/-/async-3.2.4.tgz", + "integrity": "sha512-iAB+JbDEGXhyIUavoDl9WP/Jj106Kz9DEn1DPgYw5ruDn0e3Wgi3sKFm55sASdGBNOQB8F59d9qQ7deqrHA8wQ==" + }, + "asynckit": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", + "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==" + }, + "axios": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/axios/-/axios-1.6.0.tgz", + "integrity": "sha512-EZ1DYihju9pwVB+jg67ogm+Tmqc6JmhamRN6I4Zt8DfZu5lbcQGw3ozH9lFejSJgs/ibaef3A9PMXPLeefFGJg==", + "requires": { + "follow-redirects": "^1.15.0", + "form-data": "^4.0.0", + "proxy-from-env": "^1.1.0" + } + }, + "babel-eslint": { + "version": "10.1.0", + "resolved": "https://registry.npmjs.org/babel-eslint/-/babel-eslint-10.1.0.tgz", + "integrity": "sha512-ifWaTHQ0ce+448CYop8AdrQiBsGrnC+bMgfyKFdi6EsPLTAWG+QfyDeM6OH+FmWnKvEq5NnBMLvlBUPKQZoDSg==", + "dev": true, + "requires": { + "@babel/code-frame": "^7.0.0", + "@babel/parser": "^7.7.0", + "@babel/traverse": "^7.7.0", + "@babel/types": "^7.7.0", + "eslint-visitor-keys": "^1.0.0", + "resolve": "^1.12.0" + }, + "dependencies": { + "eslint-visitor-keys": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-1.3.0.tgz", + "integrity": "sha512-6J72N8UNa462wa/KFODt/PJ3IU60SDpC3QXC1Hjc1BXXpfL2C9R5+AU7jhe0F6GREqVMh4Juu+NY7xn+6dipUQ==", + "dev": true + } + } + }, + "babel-jest": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/babel-jest/-/babel-jest-29.7.0.tgz", + "integrity": "sha512-BrvGY3xZSwEcCzKvKsCi2GgHqDqsYkOP4/by5xCgIwGXQxIEh+8ew3gmrE1y7XRR6LHZIj6yLYnUi/mm2KXKBg==", + "dev": true, + "requires": { + "@jest/transform": "^29.7.0", + "@types/babel__core": "^7.1.14", + "babel-plugin-istanbul": "^6.1.1", + "babel-preset-jest": "^29.6.3", + "chalk": "^4.0.0", + "graceful-fs": "^4.2.9", + "slash": "^3.0.0" + } + }, + "babel-plugin-istanbul": { + "version": "6.1.1", + "resolved": "https://registry.npmjs.org/babel-plugin-istanbul/-/babel-plugin-istanbul-6.1.1.tgz", + "integrity": "sha512-Y1IQok9821cC9onCx5otgFfRm7Lm+I+wwxOx738M/WLPZ9Q42m4IG5W0FNX8WLL2gYMZo3JkuXIH2DOpWM+qwA==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.0.0", + "@istanbuljs/load-nyc-config": "^1.0.0", + "@istanbuljs/schema": "^0.1.2", + "istanbul-lib-instrument": "^5.0.4", + "test-exclude": "^6.0.0" + } + }, + "babel-plugin-jest-hoist": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/babel-plugin-jest-hoist/-/babel-plugin-jest-hoist-29.6.3.tgz", + "integrity": "sha512-ESAc/RJvGTFEzRwOTT4+lNDk/GNHMkKbNzsvT0qKRfDyyYTskxB5rnU2njIDYVxXCBHHEI1c0YwHob3WaYujOg==", + "dev": true, + "requires": { + "@babel/template": "^7.3.3", + "@babel/types": "^7.3.3", + "@types/babel__core": "^7.1.14", + "@types/babel__traverse": "^7.0.6" + } + }, + "babel-preset-current-node-syntax": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/babel-preset-current-node-syntax/-/babel-preset-current-node-syntax-1.0.1.tgz", + "integrity": "sha512-M7LQ0bxarkxQoN+vz5aJPsLBn77n8QgTFmo8WK0/44auK2xlCXrYcUxHFxgU7qW5Yzw/CjmLRK2uJzaCd7LvqQ==", + "dev": true, + "requires": { + "@babel/plugin-syntax-async-generators": "^7.8.4", + "@babel/plugin-syntax-bigint": "^7.8.3", + "@babel/plugin-syntax-class-properties": "^7.8.3", + "@babel/plugin-syntax-import-meta": "^7.8.3", + "@babel/plugin-syntax-json-strings": "^7.8.3", + "@babel/plugin-syntax-logical-assignment-operators": "^7.8.3", + "@babel/plugin-syntax-nullish-coalescing-operator": "^7.8.3", + "@babel/plugin-syntax-numeric-separator": "^7.8.3", + "@babel/plugin-syntax-object-rest-spread": "^7.8.3", + "@babel/plugin-syntax-optional-catch-binding": "^7.8.3", + "@babel/plugin-syntax-optional-chaining": "^7.8.3", + "@babel/plugin-syntax-top-level-await": "^7.8.3" + } + }, + "babel-preset-jest": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/babel-preset-jest/-/babel-preset-jest-29.6.3.tgz", + "integrity": "sha512-0B3bhxR6snWXJZtR/RliHTDPRgn1sNHOR0yVtq/IiQFyuOVjFS+wuio/R4gSNkyYmKmJB4wGZv2NZanmKmTnNA==", + "dev": true, + "requires": { + "babel-plugin-jest-hoist": "^29.6.3", + "babel-preset-current-node-syntax": "^1.0.0" + } + }, + "balanced-match": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", + "dev": true + }, + "big-integer": { + "version": "1.6.51", + "resolved": "https://registry.npmjs.org/big-integer/-/big-integer-1.6.51.tgz", + "integrity": "sha512-GPEid2Y9QU1Exl1rpO9B2IPJGHPSupF5GnVIP0blYvNOMer2bTvSWs1jGOUg04hTmu67nmLsQ9TBo1puaotBHg==", + "dev": true + }, + "binary-extensions": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.2.0.tgz", + "integrity": "sha512-jDctJ/IVQbZoJykoeHbhXpOlNBqGNcwXJKJog42E5HDPUwQTSdjCHdihjj0DlnheQ7blbT6dHOafNAiS8ooQKA==", + "dev": true + }, + "body-parser": { + "version": "1.20.1", + "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.1.tgz", + "integrity": "sha512-jWi7abTbYwajOytWCQc37VulmWiRae5RyTpaCyDcS5/lMdtwSz5lOpDE67srw/HYe35f1z3fDQw+3txg7gNtWw==", + "requires": { + "bytes": "3.1.2", + "content-type": "~1.0.4", + "debug": "2.6.9", + "depd": "2.0.0", + "destroy": "1.2.0", + "http-errors": "2.0.0", + "iconv-lite": "0.4.24", + "on-finished": "2.4.1", + "qs": "6.11.0", + "raw-body": "2.5.1", + "type-is": "~1.6.18", + "unpipe": "1.0.0" + } + }, + "bplist-parser": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/bplist-parser/-/bplist-parser-0.2.0.tgz", + "integrity": "sha512-z0M+byMThzQmD9NILRniCUXYsYpjwnlO8N5uCFaCqIOpqRsJCrQL9NK3JsD67CN5a08nF5oIL2bD6loTdHOuKw==", + "dev": true, + "requires": { + "big-integer": "^1.6.44" + } + }, + "brace-expansion": { + "version": "1.1.11", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", + "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "dev": true, + "requires": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "braces": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.2.tgz", + "integrity": "sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A==", + "dev": true, + "requires": { + "fill-range": "^7.0.1" + } + }, + "browserslist": { + "version": "4.21.4", + "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.21.4.tgz", + "integrity": "sha512-CBHJJdDmgjl3daYjN5Cp5kbTf1mUhZoS+beLklHIvkOWscs83YAhLlF3Wsh/lciQYAcbBJgTOD44VtG31ZM4Hw==", + "dev": true, + "requires": { + "caniuse-lite": "^1.0.30001400", + "electron-to-chromium": "^1.4.251", + "node-releases": "^2.0.6", + "update-browserslist-db": "^1.0.9" + } + }, + "bs-logger": { + "version": "0.2.6", + "resolved": "https://registry.npmjs.org/bs-logger/-/bs-logger-0.2.6.tgz", + "integrity": "sha512-pd8DCoxmbgc7hyPKOvxtqNcjYoOsABPQdcCUjGp3d42VR2CX1ORhk2A87oqqu5R1kk+76nsxZupkmyd+MVtCog==", + "dev": true, + "requires": { + "fast-json-stable-stringify": "2.x" + } + }, + "bser": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/bser/-/bser-2.1.1.tgz", + "integrity": "sha512-gQxTNE/GAfIIrmHLUE3oJyp5FO6HRBfhjnw4/wMmA63ZGDJnWBmgY/lyQBpnDUkGmAhbSe39tx2d/iTOAfglwQ==", + "dev": true, + "requires": { + "node-int64": "^0.4.0" + } + }, + "buffer-from": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz", + "integrity": "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==", + "dev": true + }, + "bundle-name": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/bundle-name/-/bundle-name-3.0.0.tgz", + "integrity": "sha512-PKA4BeSvBpQKQ8iPOGCSiell+N8P+Tf1DlwqmYhpe2gAhKPHn8EYOxVT+ShuGmhg8lN8XiSlS80yiExKXrURlw==", + "dev": true, + "requires": { + "run-applescript": "^5.0.0" + } + }, + "bytes": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz", + "integrity": "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==" + }, + "call-bind": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.2.tgz", + "integrity": "sha512-7O+FbCihrB5WGbFYesctwmTKae6rOiIzmz1icreWJ+0aA7LJfuqhEso2T9ncpcFtzMQtzXf2QGGueWJGTYsqrA==", + "requires": { + "function-bind": "^1.1.1", + "get-intrinsic": "^1.0.2" + } + }, + "callsites": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz", + "integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==", + "dev": true + }, + "camelcase": { + "version": "5.3.1", + "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-5.3.1.tgz", + "integrity": "sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg==", + "dev": true + }, + "caniuse-lite": { + "version": "1.0.30001429", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001429.tgz", + "integrity": "sha512-511ThLu1hF+5RRRt0zYCf2U2yRr9GPF6m5y90SBCWsvSoYoW7yAGlv/elyPaNfvGCkp6kj/KFZWU0BMA69Prsg==", + "dev": true + }, + "chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, + "requires": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + } + }, + "char-regex": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/char-regex/-/char-regex-1.0.2.tgz", + "integrity": "sha512-kWWXztvZ5SBQV+eRgKFeh8q5sLuZY2+8WUIzlxWVTg+oGwY14qylx1KbKzHd8P6ZYkAg0xyIDU9JMHhyJMZ1jw==", + "dev": true + }, + "chardet": { + "version": "0.7.0", + "resolved": "https://registry.npmjs.org/chardet/-/chardet-0.7.0.tgz", + "integrity": "sha512-mT8iDcrh03qDGRRmoA2hmBJnxpllMR+0/0qlzjqZES6NdiWDcZkCNAk4rPFZ9Q85r27unkiNNg8ZOiwZXBHwcA==", + "dev": true + }, + "chokidar": { + "version": "3.5.3", + "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.5.3.tgz", + "integrity": "sha512-Dr3sfKRP6oTcjf2JmUmFJfeVMvXBdegxB0iVQ5eb2V10uFJUCAS8OByZdVAyVb8xXNz3GjjTgj9kLWsZTqE6kw==", + "dev": true, + "requires": { + "anymatch": "~3.1.2", + "braces": "~3.0.2", + "fsevents": "~2.3.2", + "glob-parent": "~5.1.2", + "is-binary-path": "~2.1.0", + "is-glob": "~4.0.1", + "normalize-path": "~3.0.0", + "readdirp": "~3.6.0" + } + }, + "ci-info": { + "version": "3.5.0", + "resolved": "https://registry.npmjs.org/ci-info/-/ci-info-3.5.0.tgz", + "integrity": "sha512-yH4RezKOGlOhxkmhbeNuC4eYZKAUsEaGtBuBzDDP1eFUKiccDWzBABxBfOx31IDwDIXMTxWuwAxUGModvkbuVw==", + "dev": true + }, + "cjs-module-lexer": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/cjs-module-lexer/-/cjs-module-lexer-1.2.3.tgz", + "integrity": "sha512-0TNiGstbQmCFwt4akjjBg5pLRTSyj/PkWQ1ZoO2zntmg9yLqSRxwEa4iCfQLGjqhiqBfOJa7W/E8wfGrTDmlZQ==", + "dev": true + }, + "cli-cursor": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/cli-cursor/-/cli-cursor-3.1.0.tgz", + "integrity": "sha512-I/zHAwsKf9FqGoXM4WWRACob9+SNukZTd94DWF57E4toouRulbCxcUh6RKUEOQlYTHJnzkPMySvPNaaSLNfLZw==", + "dev": true, + "requires": { + "restore-cursor": "^3.1.0" + } + }, + "cli-width": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/cli-width/-/cli-width-3.0.0.tgz", + "integrity": "sha512-FxqpkPPwu1HjuN93Omfm4h8uIanXofW0RxVEW3k5RKx+mJJYSthzNhp32Kzxxy3YAEZ/Dc/EWN1vZRY0+kOhbw==", + "dev": true + }, + "cliui": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-8.0.1.tgz", + "integrity": "sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==", + "dev": true, + "requires": { + "string-width": "^4.2.0", + "strip-ansi": "^6.0.1", + "wrap-ansi": "^7.0.0" + } + }, + "co": { + "version": "4.6.0", + "resolved": "https://registry.npmjs.org/co/-/co-4.6.0.tgz", + "integrity": "sha512-QVb0dM5HvG+uaxitm8wONl7jltx8dqhfU33DcqtOZcLSVIKSDDLDi7+0LbAKiyI8hD9u42m2YxXSkMGWThaecQ==", + "dev": true + }, + "collect-v8-coverage": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/collect-v8-coverage/-/collect-v8-coverage-1.0.2.tgz", + "integrity": "sha512-lHl4d5/ONEbLlJvaJNtsF/Lz+WvB07u2ycqTYbdrq7UypDXailES4valYb2eWiJFxZlVmpGekfqoxQhzyFdT4Q==", + "dev": true + }, + "color": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/color/-/color-3.2.1.tgz", + "integrity": "sha512-aBl7dZI9ENN6fUGC7mWpMTPNHmWUSNan9tuWN6ahh5ZLNk9baLJOnSMlrQkHcrfFgz2/RigjUVAjdx36VcemKA==", + "requires": { + "color-convert": "^1.9.3", + "color-string": "^1.6.0" + }, + "dependencies": { + "color-convert": { + "version": "1.9.3", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz", + "integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==", + "requires": { + "color-name": "1.1.3" + } + }, + "color-name": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz", + "integrity": "sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw==" + } + } + }, + "color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "requires": { + "color-name": "~1.1.4" + } + }, + "color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==" + }, + "color-string": { + "version": "1.9.1", + "resolved": "https://registry.npmjs.org/color-string/-/color-string-1.9.1.tgz", + "integrity": "sha512-shrVawQFojnZv6xM40anx4CkoDP+fZsw/ZerEMsW/pyzsRbElpsL/DBVW7q3ExxwusdNXI3lXpuhEZkzs8p5Eg==", + "requires": { + "color-name": "^1.0.0", + "simple-swizzle": "^0.2.2" + } + }, + "colorspace": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/colorspace/-/colorspace-1.1.4.tgz", + "integrity": "sha512-BgvKJiuVu1igBUF2kEjRCZXol6wiiGbY5ipL/oVPwm0BL9sIpMIzM8IK7vwuxIIzOXMV3Ey5w+vxhm0rR/TN8w==", + "requires": { + "color": "^3.1.3", + "text-hex": "1.0.x" + } + }, + "combined-stream": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", + "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", + "requires": { + "delayed-stream": "~1.0.0" + } + }, + "compressible": { + "version": "2.0.18", + "resolved": "https://registry.npmjs.org/compressible/-/compressible-2.0.18.tgz", + "integrity": "sha512-AF3r7P5dWxL8MxyITRMlORQNaOA2IkAFaTr4k7BUumjPtRpGDTZpl0Pb1XCO6JeDCBdp126Cgs9sMxqSjgYyRg==", + "requires": { + "mime-db": ">= 1.43.0 < 2" + } + }, + "compression": { + "version": "1.7.4", + "resolved": "https://registry.npmjs.org/compression/-/compression-1.7.4.tgz", + "integrity": "sha512-jaSIDzP9pZVS4ZfQ+TzvtiWhdpFhE2RDHz8QJkpX9SIpLq88VueF5jJw6t+6CUQcAoA6t+x89MLrWAqpfDE8iQ==", + "requires": { + "accepts": "~1.3.5", + "bytes": "3.0.0", + "compressible": "~2.0.16", + "debug": "2.6.9", + "on-headers": "~1.0.2", + "safe-buffer": "5.1.2", + "vary": "~1.1.2" + }, + "dependencies": { + "bytes": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.0.0.tgz", + "integrity": "sha512-pMhOfFDPiv9t5jjIXkHosWmkSyQbvsgEVNkz0ERHbuLh2T/7j4Mqqpz523Fe8MVY89KC6Sh/QfS2sM+SjgFDcw==" + }, + "safe-buffer": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", + "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==" + } + } + }, + "concat-map": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", + "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==", + "dev": true + }, + "config": { + "version": "3.3.9", + "resolved": "https://registry.npmjs.org/config/-/config-3.3.9.tgz", + "integrity": "sha512-G17nfe+cY7kR0wVpc49NCYvNtelm/pPy8czHoFkAgtV1lkmcp7DHtWCdDu+C9Z7gb2WVqa9Tm3uF9aKaPbCfhg==", + "requires": { + "json5": "^2.2.3" + } + }, + "content-disposition": { + "version": "0.5.4", + "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.4.tgz", + "integrity": "sha512-FveZTNuGw04cxlAiWbzi6zTAL/lhehaWbTtgluJh4/E95DqMwTmha3KZN1aAWA8cFIhHzMZUvLevkw5Rqk+tSQ==", + "requires": { + "safe-buffer": "5.2.1" + } + }, + "content-type": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.4.tgz", + "integrity": "sha512-hIP3EEPs8tB9AT1L+NUqtwOAps4mk2Zob89MWXMHjHWg9milF/j4osnnQLXBCBFBk/tvIG/tUc9mOUJiPBhPXA==" + }, + "convert-source-map": { + "version": "1.9.0", + "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-1.9.0.tgz", + "integrity": "sha512-ASFBup0Mz1uyiIjANan1jzLQami9z1PoYSZCiiYW2FczPbenXc45FZdBZLzOT+r6+iciuEModtmCti+hjaAk0A==", + "dev": true + }, + "cookie": { + "version": "0.5.0", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.5.0.tgz", + "integrity": "sha512-YZ3GUyn/o8gfKJlnlX7g7xq4gyO6OSuhGPKaaGssGB2qgDUS0gPgtTvoyZLTt9Ab6dC4hfc9dV5arkvc/OCmrw==" + }, + "cookie-signature": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.6.tgz", + "integrity": "sha512-QADzlaHc8icV8I7vbaJXJwod9HWYp8uCqf1xa4OfNu1T7JVxQIrUgOWtHdNDtPiywmFbiS12VjotIXLrKM3orQ==" + }, + "create-jest": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/create-jest/-/create-jest-29.7.0.tgz", + "integrity": "sha512-Adz2bdH0Vq3F53KEMJOoftQFutWCukm6J24wbPWRO4k1kMY7gS7ds/uoJkNuV8wDCtWWnuwGcJwpWcih+zEW1Q==", + "dev": true, + "requires": { + "@jest/types": "^29.6.3", + "chalk": "^4.0.0", + "exit": "^0.1.2", + "graceful-fs": "^4.2.9", + "jest-config": "^29.7.0", + "jest-util": "^29.7.0", + "prompts": "^2.0.1" + } + }, + "create-require": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/create-require/-/create-require-1.1.1.tgz", + "integrity": "sha512-dcKFX3jn0MpIaXjisoRvexIJVEKzaq7z2rZKxf+MSr9TkdmHmsU4m2lcLojrj/FHl8mk5VxMmYA+ftRkP/3oKQ==" + }, + "cross-spawn": { + "version": "7.0.3", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.3.tgz", + "integrity": "sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w==", + "dev": true, + "requires": { + "path-key": "^3.1.0", + "shebang-command": "^2.0.0", + "which": "^2.0.1" + } + }, + "debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "requires": { + "ms": "2.0.0" + } + }, + "dedent": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/dedent/-/dedent-1.5.1.tgz", + "integrity": "sha512-+LxW+KLWxu3HW3M2w2ympwtqPrqYRzU8fqi6Fhd18fBALe15blJPI/I4+UHveMVG6lJqB4JNd4UG0S5cnVHwIg==", + "dev": true + }, + "deep-is": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz", + "integrity": "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==", + "dev": true + }, + "deepmerge": { + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/deepmerge/-/deepmerge-4.3.1.tgz", + "integrity": "sha512-3sUqbMEc77XqpdNO7FRyRog+eW3ph+GYCbj+rK+uYyRMuwsVy0rMiVtPn+QJlKFvWP/1PYpapqYn0Me2knFn+A==", + "dev": true + }, + "default-browser": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/default-browser/-/default-browser-4.0.0.tgz", + "integrity": "sha512-wX5pXO1+BrhMkSbROFsyxUm0i/cJEScyNhA4PPxc41ICuv05ZZB/MX28s8aZx6xjmatvebIapF6hLEKEcpneUA==", + "dev": true, + "requires": { + "bundle-name": "^3.0.0", + "default-browser-id": "^3.0.0", + "execa": "^7.1.1", + "titleize": "^3.0.0" + }, + "dependencies": { + "execa": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/execa/-/execa-7.2.0.tgz", + "integrity": "sha512-UduyVP7TLB5IcAQl+OzLyLcS/l32W/GLg+AhHJ+ow40FOk2U3SAllPwR44v4vmdFwIWqpdwxxpQbF1n5ta9seA==", + "dev": true, + "requires": { + "cross-spawn": "^7.0.3", + "get-stream": "^6.0.1", + "human-signals": "^4.3.0", + "is-stream": "^3.0.0", + "merge-stream": "^2.0.0", + "npm-run-path": "^5.1.0", + "onetime": "^6.0.0", + "signal-exit": "^3.0.7", + "strip-final-newline": "^3.0.0" + } + }, + "human-signals": { + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/human-signals/-/human-signals-4.3.1.tgz", + "integrity": "sha512-nZXjEF2nbo7lIw3mgYjItAfgQXog3OjJogSbKa2CQIIvSGWcKgeJnQlNXip6NglNzYH45nSRiEVimMvYL8DDqQ==", + "dev": true + }, + "is-stream": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-3.0.0.tgz", + "integrity": "sha512-LnQR4bZ9IADDRSkvpqMGvt/tEJWclzklNgSw48V5EAaAeDd6qGvN8ei6k5p0tvxSR171VmGyHuTiAOfxAbr8kA==", + "dev": true + }, + "mimic-fn": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-4.0.0.tgz", + "integrity": "sha512-vqiC06CuhBTUdZH+RYl8sFrL096vA45Ok5ISO6sE/Mr1jRbGH4Csnhi8f3wKVl7x8mO4Au7Ir9D3Oyv1VYMFJw==", + "dev": true + }, + "npm-run-path": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-5.1.0.tgz", + "integrity": "sha512-sJOdmRGrY2sjNTRMbSvluQqg+8X7ZK61yvzBEIDhz4f8z1TZFYABsqjjCBd/0PUNE9M6QDgHJXQkGUEm7Q+l9Q==", + "dev": true, + "requires": { + "path-key": "^4.0.0" + } + }, + "onetime": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/onetime/-/onetime-6.0.0.tgz", + "integrity": "sha512-1FlR+gjXK7X+AsAHso35MnyN5KqGwJRi/31ft6x0M194ht7S+rWAvd7PHss9xSKMzE0asv1pyIHaJYq+BbacAQ==", + "dev": true, + "requires": { + "mimic-fn": "^4.0.0" + } + }, + "path-key": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-4.0.0.tgz", + "integrity": "sha512-haREypq7xkM7ErfgIyA0z+Bj4AGKlMSdlQE2jvJo6huWD1EdkKYV+G/T4nq0YEF2vgTT8kqMFKo1uHn950r4SQ==", + "dev": true + }, + "strip-final-newline": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/strip-final-newline/-/strip-final-newline-3.0.0.tgz", + "integrity": "sha512-dOESqjYr96iWYylGObzd39EuNTa5VJxyvVAEm5Jnh7KGo75V43Hk1odPQkNDyXNmUR6k+gEiDVXnjB8HJ3crXw==", + "dev": true + } + } + }, + "default-browser-id": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/default-browser-id/-/default-browser-id-3.0.0.tgz", + "integrity": "sha512-OZ1y3y0SqSICtE8DE4S8YOE9UZOJ8wO16fKWVP5J1Qz42kV9jcnMVFrEE/noXb/ss3Q4pZIH79kxofzyNNtUNA==", + "dev": true, + "requires": { + "bplist-parser": "^0.2.0", + "untildify": "^4.0.0" + } + }, + "define-lazy-prop": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/define-lazy-prop/-/define-lazy-prop-3.0.0.tgz", + "integrity": "sha512-N+MeXYoqr3pOgn8xfyRPREN7gHakLYjhsHhWGT3fWAiL4IkAt0iDw14QiiEm2bE30c5XX5q0FtAA3CK5f9/BUg==", + "dev": true + }, + "define-properties": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/define-properties/-/define-properties-1.1.4.tgz", + "integrity": "sha512-uckOqKcfaVvtBdsVkdPv3XjveQJsNQqmhXgRi8uhvWWuPYZCNlzT8qAyblUgNoXdHdjMTzAqeGjAoli8f+bzPA==", + "dev": true, + "requires": { + "has-property-descriptors": "^1.0.0", + "object-keys": "^1.1.1" + } + }, + "delayed-stream": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", + "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==" + }, + "depd": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz", + "integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==" + }, + "destroy": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/destroy/-/destroy-1.2.0.tgz", + "integrity": "sha512-2sJGJTaXIIaR1w4iJSNoN0hnMY7Gpc/n8D4qSCJw8QqFWXf7cuAgnEHxBpweaVcPevC2l3KpjYCx3NypQQgaJg==" + }, + "detect-newline": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/detect-newline/-/detect-newline-3.1.0.tgz", + "integrity": "sha512-TLz+x/vEXm/Y7P7wn1EJFNLxYpUD4TgMosxY6fAVJUnJMbupHBOncxyWUG9OpTaH9EBD7uFI5LfEgmMOc54DsA==", + "dev": true + }, + "diff": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/diff/-/diff-4.0.2.tgz", + "integrity": "sha512-58lmxKSA4BNyLz+HHMUzlOEpg09FV+ev6ZMe3vJihgdxzgcwZ8VoEEPmALCZG9LmqfVoNMMKpttIYTVG6uDY7A==" + }, + "diff-sequences": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/diff-sequences/-/diff-sequences-29.6.3.tgz", + "integrity": "sha512-EjePK1srD3P08o2j4f0ExnylqRs5B9tJjcp9t1krH2qRi8CCdsYfwe9JgSLurFBWwq4uOlipzfk5fHNvwFKr8Q==", + "dev": true + }, + "dir-glob": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/dir-glob/-/dir-glob-3.0.1.tgz", + "integrity": "sha512-WkrWp9GR4KXfKGYzOLmTuGVi1UWFfws377n9cc55/tb6DuqyF6pcQ5AbiHEshaDpY9v6oaSr2XCDidGmMwdzIA==", + "dev": true, + "requires": { + "path-type": "^4.0.0" + } + }, + "doctrine": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/doctrine/-/doctrine-3.0.0.tgz", + "integrity": "sha512-yS+Q5i3hBf7GBkd4KG8a7eBNNWNGLTaEwwYWUijIYM7zrlYDM0BFXHjjPWlWZ1Rg7UaddZeIDmi9jF3HmqiQ2w==", + "dev": true, + "requires": { + "esutils": "^2.0.2" + } + }, + "dynamic-dedupe": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/dynamic-dedupe/-/dynamic-dedupe-0.3.0.tgz", + "integrity": "sha512-ssuANeD+z97meYOqd50e04Ze5qp4bPqo8cCkI4TRjZkzAUgIDTrXV1R8QCdINpiI+hw14+rYazvTRdQrz0/rFQ==", + "dev": true, + "requires": { + "xtend": "^4.0.0" + } + }, + "eastasianwidth": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/eastasianwidth/-/eastasianwidth-0.2.0.tgz", + "integrity": "sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==", + "dev": true + }, + "ee-first": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", + "integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==" + }, + "electron-to-chromium": { + "version": "1.4.284", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.4.284.tgz", + "integrity": "sha512-M8WEXFuKXMYMVr45fo8mq0wUrrJHheiKZf6BArTKk9ZBYCKJEOU5H8cdWgDT+qCVZf7Na4lVUaZsA+h6uA9+PA==", + "dev": true + }, + "emittery": { + "version": "0.13.1", + "resolved": "https://registry.npmjs.org/emittery/-/emittery-0.13.1.tgz", + "integrity": "sha512-DeWwawk6r5yR9jFgnDKYt4sLS0LmHJJi3ZOnb5/JdbYwj3nW+FxQnHIjhBKz8YLC7oRNPVM9NQ47I3CVx34eqQ==", + "dev": true + }, + "emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "dev": true + }, + "enabled": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/enabled/-/enabled-2.0.0.tgz", + "integrity": "sha512-AKrN98kuwOzMIdAizXGI86UFBoo26CL21UM763y1h/GMSJ4/OHU9k2YlsmBpyScFo/wbLzWQJBMCW4+IO3/+OQ==" + }, + "encodeurl": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-1.0.2.tgz", + "integrity": "sha512-TPJXq8JqFaVYm2CWmPvnP2Iyo4ZSM7/QKcSmuMLDObfpH5fi7RUGmd/rTDf+rut/saiDiQEeVTNgAmJEdAOx0w==" + }, + "error-ex": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/error-ex/-/error-ex-1.3.2.tgz", + "integrity": "sha512-7dFHNmqeFSEt2ZBsCriorKnn3Z2pj+fd9kmI6QoWw4//DL+icEBfc0U7qJCisqrTsKTjw4fNFy2pW9OqStD84g==", + "dev": true, + "requires": { + "is-arrayish": "^0.2.1" + } + }, + "es-abstract": { + "version": "1.20.4", + "resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.20.4.tgz", + "integrity": "sha512-0UtvRN79eMe2L+UNEF1BwRe364sj/DXhQ/k5FmivgoSdpM90b8Jc0mDzKMGo7QS0BVbOP/bTwBKNnDc9rNzaPA==", + "dev": true, + "requires": { + "call-bind": "^1.0.2", + "es-to-primitive": "^1.2.1", + "function-bind": "^1.1.1", + "function.prototype.name": "^1.1.5", + "get-intrinsic": "^1.1.3", + "get-symbol-description": "^1.0.0", + "has": "^1.0.3", + "has-property-descriptors": "^1.0.0", + "has-symbols": "^1.0.3", + "internal-slot": "^1.0.3", + "is-callable": "^1.2.7", + "is-negative-zero": "^2.0.2", + "is-regex": "^1.1.4", + "is-shared-array-buffer": "^1.0.2", + "is-string": "^1.0.7", + "is-weakref": "^1.0.2", + "object-inspect": "^1.12.2", + "object-keys": "^1.1.1", + "object.assign": "^4.1.4", + "regexp.prototype.flags": "^1.4.3", + "safe-regex-test": "^1.0.0", + "string.prototype.trimend": "^1.0.5", + "string.prototype.trimstart": "^1.0.5", + "unbox-primitive": "^1.0.2" + } + }, + "es-shim-unscopables": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/es-shim-unscopables/-/es-shim-unscopables-1.0.0.tgz", + "integrity": "sha512-Jm6GPcCdC30eMLbZ2x8z2WuRwAws3zTBBKuusffYVUrNj/GVSUAZ+xKMaUpfNDR5IbyNA5LJbaecoUVbmUcB1w==", + "dev": true, + "requires": { + "has": "^1.0.3" + } + }, + "es-to-primitive": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/es-to-primitive/-/es-to-primitive-1.2.1.tgz", + "integrity": "sha512-QCOllgZJtaUo9miYBcLChTUaHNjJF3PYs1VidD7AwiEj1kYxKeQTctLAezAOH5ZKRH0g2IgPn6KwB4IT8iRpvA==", + "dev": true, + "requires": { + "is-callable": "^1.1.4", + "is-date-object": "^1.0.1", + "is-symbol": "^1.0.2" + } + }, + "escalade": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.1.1.tgz", + "integrity": "sha512-k0er2gUkLf8O0zKJiAhmkTnJlTvINGv7ygDNPbeIsX/TJjGJZHuh9B2UxbsaEkmlEo9MfhrSzmhIlhRlI2GXnw==", + "dev": true + }, + "escape-html": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz", + "integrity": "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==" + }, + "escape-string-regexp": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-2.0.0.tgz", + "integrity": "sha512-UpzcLCXolUWcNu5HtVMHYdXJjArjsF9C0aNnquZYY4uW/Vu0miy5YoWvbV345HauVvcAUnpRuhMMcqTcGOY2+w==", + "dev": true + }, + "eslint": { + "version": "8.52.0", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-8.52.0.tgz", + "integrity": "sha512-zh/JHnaixqHZsolRB/w9/02akBk9EPrOs9JwcTP2ek7yL5bVvXuRariiaAjjoJ5DvuwQ1WAE/HsMz+w17YgBCg==", + "dev": true, + "requires": { + "@eslint-community/eslint-utils": "^4.2.0", + "@eslint-community/regexpp": "^4.6.1", + "@eslint/eslintrc": "^2.1.2", + "@eslint/js": "8.52.0", + "@humanwhocodes/config-array": "^0.11.13", + "@humanwhocodes/module-importer": "^1.0.1", + "@nodelib/fs.walk": "^1.2.8", + "@ungap/structured-clone": "^1.2.0", + "ajv": "^6.12.4", + "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.2.2", + "eslint-visitor-keys": "^3.4.3", + "espree": "^9.6.1", + "esquery": "^1.4.2", + "esutils": "^2.0.2", + "fast-deep-equal": "^3.1.3", + "file-entry-cache": "^6.0.1", + "find-up": "^5.0.0", + "glob-parent": "^6.0.2", + "globals": "^13.19.0", + "graphemer": "^1.4.0", + "ignore": "^5.2.0", + "imurmurhash": "^0.1.4", + "is-glob": "^4.0.0", + "is-path-inside": "^3.0.3", + "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.3", + "strip-ansi": "^6.0.1", + "text-table": "^0.2.0" + }, + "dependencies": { + "argparse": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", + "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", + "dev": true + }, + "debug": { + "version": "4.3.4", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", + "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==", + "dev": true, + "requires": { + "ms": "2.1.2" + } + }, + "escape-string-regexp": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", + "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==", + "dev": true + }, + "eslint-scope": { + "version": "7.2.2", + "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-7.2.2.tgz", + "integrity": "sha512-dOt21O7lTMhDM+X9mB4GX+DZrZtCUJPL/wlcTqxyrx5IvO0IYtILdtrQGQp+8n5S0gwSVmOf9NQrjMOgfQZlIg==", + "dev": true, + "requires": { + "esrecurse": "^4.3.0", + "estraverse": "^5.2.0" + } + }, + "find-up": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz", + "integrity": "sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==", + "dev": true, + "requires": { + "locate-path": "^6.0.0", + "path-exists": "^4.0.0" + } + }, + "glob-parent": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz", + "integrity": "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==", + "dev": true, + "requires": { + "is-glob": "^4.0.3" + } + }, + "globals": { + "version": "13.20.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-13.20.0.tgz", + "integrity": "sha512-Qg5QtVkCy/kv3FUSlu4ukeZDVf9ee0iXLAUYX13gbR17bnejFTzr4iS9bY7kwCf1NztRNm1t91fjOiyx4CSwPQ==", + "dev": true, + "requires": { + "type-fest": "^0.20.2" + } + }, + "js-yaml": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz", + "integrity": "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==", + "dev": true, + "requires": { + "argparse": "^2.0.1" + } + }, + "locate-path": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz", + "integrity": "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==", + "dev": true, + "requires": { + "p-locate": "^5.0.0" + } + }, + "ms": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", + "dev": true + }, + "p-locate": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-5.0.0.tgz", + "integrity": "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==", + "dev": true, + "requires": { + "p-limit": "^3.0.2" + } + }, + "type-fest": { + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.20.2.tgz", + "integrity": "sha512-Ne+eE4r0/iWnpAxD852z3A+N0Bt5RN//NjJwRd2VFHEmrywxf5vsZlh4R6lixl6B+wz/8d+maTSAkN1FIkI3LQ==", + "dev": true + } + } + }, + "eslint-config-esnext": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/eslint-config-esnext/-/eslint-config-esnext-4.1.0.tgz", + "integrity": "sha512-GhfVEXdqYKEIIj7j+Fw2SQdL9qyZMekgXfq6PyXM66cQw0B435ddjz3P3kxOBVihMRJ0xGYjosaveQz5Y6z0uA==", + "dev": true, + "requires": { + "babel-eslint": "^10.0.1", + "eslint": "^6.8.0", + "eslint-plugin-babel": "^5.2.1", + "eslint-plugin-import": "^2.14.0" + }, + "dependencies": { + "acorn": { + "version": "7.4.1", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-7.4.1.tgz", + "integrity": "sha512-nQyp0o1/mNdbTO1PO6kHkwSrmgZ0MT/jCCpNiwbUjGoRN4dlBhqJtoQuCnEOKzgTVwg0ZWiCoQy6SxMebQVh8A==", + "dev": true + }, + "ansi-regex": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-4.1.1.tgz", + "integrity": "sha512-ILlv4k/3f6vfQ4OoP2AGvirOktlQ98ZEL1k9FaQjxa3L1abBgbuTDAdPOpvbGncC0BTVQrl+OM8xZGK6tWXt7g==", + "dev": true + }, + "ansi-styles": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz", + "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==", + "dev": true, + "requires": { + "color-convert": "^1.9.0" + } + }, + "chalk": { + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", + "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==", + "dev": true, + "requires": { + "ansi-styles": "^3.2.1", + "escape-string-regexp": "^1.0.5", + "supports-color": "^5.3.0" + } + }, + "color-convert": { + "version": "1.9.3", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz", + "integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==", + "dev": true, + "requires": { + "color-name": "1.1.3" + } + }, + "color-name": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz", + "integrity": "sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw==", + "dev": true + }, + "cross-spawn": { + "version": "6.0.5", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-6.0.5.tgz", + "integrity": "sha512-eTVLrBSt7fjbDygz805pMnstIs2VTBNkRm0qxZd+M7A5XDdxVRWO5MxGBXZhjY4cqLYLdtrGqRf8mBPmzwSpWQ==", + "dev": true, + "requires": { + "nice-try": "^1.0.4", + "path-key": "^2.0.1", + "semver": "^5.5.0", + "shebang-command": "^1.2.0", + "which": "^1.2.9" + }, + "dependencies": { + "semver": { + "version": "5.7.2", + "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.2.tgz", + "integrity": "sha512-cBznnQ9KjJqU67B52RMC65CMarK2600WFnbkcaiwWq3xy/5haFJlshgnpjovMVJ+Hff49d8GEn0b87C5pDQ10g==", + "dev": true + } + } + }, + "debug": { + "version": "4.3.4", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", + "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==", + "dev": true, + "requires": { + "ms": "2.1.2" + } + }, + "escape-string-regexp": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", + "integrity": "sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg==", + "dev": true + }, + "eslint": { + "version": "6.8.0", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-6.8.0.tgz", + "integrity": "sha512-K+Iayyo2LtyYhDSYwz5D5QdWw0hCacNzyq1Y821Xna2xSJj7cijoLLYmLxTQgcgZ9mC61nryMy9S7GRbYpI5Ig==", + "dev": true, + "requires": { + "@babel/code-frame": "^7.0.0", + "ajv": "^6.10.0", + "chalk": "^2.1.0", + "cross-spawn": "^6.0.5", + "debug": "^4.0.1", + "doctrine": "^3.0.0", + "eslint-scope": "^5.0.0", + "eslint-utils": "^1.4.3", + "eslint-visitor-keys": "^1.1.0", + "espree": "^6.1.2", + "esquery": "^1.0.1", + "esutils": "^2.0.2", + "file-entry-cache": "^5.0.1", + "functional-red-black-tree": "^1.0.1", + "glob-parent": "^5.0.0", + "globals": "^12.1.0", + "ignore": "^4.0.6", + "import-fresh": "^3.0.0", + "imurmurhash": "^0.1.4", + "inquirer": "^7.0.0", + "is-glob": "^4.0.0", + "js-yaml": "^3.13.1", + "json-stable-stringify-without-jsonify": "^1.0.1", + "levn": "^0.3.0", + "lodash": "^4.17.14", + "minimatch": "^3.0.4", + "mkdirp": "^0.5.1", + "natural-compare": "^1.4.0", + "optionator": "^0.8.3", + "progress": "^2.0.0", + "regexpp": "^2.0.1", + "semver": "^6.1.2", + "strip-ansi": "^5.2.0", + "strip-json-comments": "^3.0.1", + "table": "^5.2.3", + "text-table": "^0.2.0", + "v8-compile-cache": "^2.0.3" + } + }, + "eslint-utils": { + "version": "1.4.3", + "resolved": "https://registry.npmjs.org/eslint-utils/-/eslint-utils-1.4.3.tgz", + "integrity": "sha512-fbBN5W2xdY45KulGXmLHZ3c3FHfVYmKg0IrAKGOkT/464PQsx2UeIzfz1RmEci+KLm1bBaAzZAh8+/E+XAeZ8Q==", + "dev": true, + "requires": { + "eslint-visitor-keys": "^1.1.0" + } + }, + "eslint-visitor-keys": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-1.3.0.tgz", + "integrity": "sha512-6J72N8UNa462wa/KFODt/PJ3IU60SDpC3QXC1Hjc1BXXpfL2C9R5+AU7jhe0F6GREqVMh4Juu+NY7xn+6dipUQ==", + "dev": true + }, + "espree": { + "version": "6.2.1", + "resolved": "https://registry.npmjs.org/espree/-/espree-6.2.1.tgz", + "integrity": "sha512-ysCxRQY3WaXJz9tdbWOwuWr5Y/XrPTGX9Kiz3yoUXwW0VZ4w30HTkQLaGx/+ttFjF8i+ACbArnB4ce68a9m5hw==", + "dev": true, + "requires": { + "acorn": "^7.1.1", + "acorn-jsx": "^5.2.0", + "eslint-visitor-keys": "^1.1.0" + } + }, + "file-entry-cache": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-5.0.1.tgz", + "integrity": "sha512-bCg29ictuBaKUwwArK4ouCaqDgLZcysCFLmM/Yn/FDoqndh/9vNuQfXRDvTuXKLxfD/JtZQGKFT8MGcJBK644g==", + "dev": true, + "requires": { + "flat-cache": "^2.0.1" + } + }, + "flat-cache": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-2.0.1.tgz", + "integrity": "sha512-LoQe6yDuUMDzQAEH8sgmh4Md6oZnc/7PjtwjNFSzveXqSHt6ka9fPBuso7IGf9Rz4uqnSnWiFH2B/zj24a5ReA==", + "dev": true, + "requires": { + "flatted": "^2.0.0", + "rimraf": "2.6.3", + "write": "1.0.3" + } + }, + "flatted": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/flatted/-/flatted-2.0.2.tgz", + "integrity": "sha512-r5wGx7YeOwNWNlCA0wQ86zKyDLMQr+/RB8xy74M4hTphfmjlijTSSXGuH8rnvKZnfT9i+75zmd8jcKdMR4O6jA==", + "dev": true + }, + "globals": { + "version": "12.4.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-12.4.0.tgz", + "integrity": "sha512-BWICuzzDvDoH54NHKCseDanAhE3CeDorgDL5MT6LMXXj2WCnd9UC2szdk4AWLfjdgNBCXLUanXYcpBBKOSWGwg==", + "dev": true, + "requires": { + "type-fest": "^0.8.1" + } + }, + "has-flag": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", + "integrity": "sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==", + "dev": true + }, + "ignore": { + "version": "4.0.6", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-4.0.6.tgz", + "integrity": "sha512-cyFDKrqc/YdcWFniJhzI42+AzS+gNwmUzOSFcRCQYwySuBBBy/KjuxWLZ/FHEH6Moq1NizMOBWyTcv8O4OZIMg==", + "dev": true + }, + "levn": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/levn/-/levn-0.3.0.tgz", + "integrity": "sha512-0OO4y2iOHix2W6ujICbKIaEQXvFQHue65vUG3pb5EUomzPI90z9hsA1VsO/dbIIpC53J8gxM9Q4Oho0jrCM/yA==", + "dev": true, + "requires": { + "prelude-ls": "~1.1.2", + "type-check": "~0.3.2" + } + }, + "ms": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", + "dev": true + }, + "optionator": { + "version": "0.8.3", + "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.8.3.tgz", + "integrity": "sha512-+IW9pACdk3XWmmTXG8m3upGUJst5XRGzxMRjXzAuJ1XnIFNvfhjjIuYkDvysnPQ7qzqVzLt78BCruntqRhWQbA==", + "dev": true, + "requires": { + "deep-is": "~0.1.3", + "fast-levenshtein": "~2.0.6", + "levn": "~0.3.0", + "prelude-ls": "~1.1.2", + "type-check": "~0.3.2", + "word-wrap": "~1.2.3" + } + }, + "path-key": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-2.0.1.tgz", + "integrity": "sha512-fEHGKCSmUSDPv4uoj8AlD+joPlq3peND+HRYyxFz4KPw4z926S/b8rIuFs2FYJg3BwsxJf6A9/3eIdLaYC+9Dw==", + "dev": true + }, + "prelude-ls": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.1.2.tgz", + "integrity": "sha512-ESF23V4SKG6lVSGZgYNpbsiaAkdab6ZgOxe52p7+Kid3W3u3bxR4Vfd/o21dmN7jSt0IwgZ4v5MUd26FEtXE9w==", + "dev": true + }, + "regexpp": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/regexpp/-/regexpp-2.0.1.tgz", + "integrity": "sha512-lv0M6+TkDVniA3aD1Eg0DVpfU/booSu7Eev3TDO/mZKHBfVjgCGTV4t4buppESEYDtkArYFOxTJWv6S5C+iaNw==", + "dev": true + }, + "rimraf": { + "version": "2.6.3", + "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-2.6.3.tgz", + "integrity": "sha512-mwqeW5XsA2qAejG46gYdENaxXjx9onRNCfn7L0duuP4hCuTIi/QO7PDK07KJfp1d+izWPrzEJDcSqBa0OZQriA==", + "dev": true, + "requires": { + "glob": "^7.1.3" + } + }, + "shebang-command": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-1.2.0.tgz", + "integrity": "sha512-EV3L1+UQWGor21OmnvojK36mhg+TyIKDh3iFBKBohr5xeXIhNBcx8oWdgkTEEQ+BEFFYdLRuqMfd5L84N1V5Vg==", + "dev": true, + "requires": { + "shebang-regex": "^1.0.0" + } + }, + "shebang-regex": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-1.0.0.tgz", + "integrity": "sha512-wpoSFAxys6b2a2wHZ1XpDSgD7N9iVjg29Ph9uV/uaP9Ex/KXlkTZTeddxDPSYQpgvzKLGJke2UU0AzoGCjNIvQ==", + "dev": true + }, + "strip-ansi": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-5.2.0.tgz", + "integrity": "sha512-DuRs1gKbBqsMKIZlrffwlug8MHkcnpjs5VPmL1PAh+mA30U0DTotfDZ0d2UUsXpPmPmMMJ6W773MaA3J+lbiWA==", + "dev": true, + "requires": { + "ansi-regex": "^4.1.0" + } + }, + "supports-color": { + "version": "5.5.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", + "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", + "dev": true, + "requires": { + "has-flag": "^3.0.0" + } + }, + "type-check": { + "version": "0.3.2", + "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.3.2.tgz", + "integrity": "sha512-ZCmOJdvOWDBYJlzAoFkC+Q0+bUyEOS1ltgp1MGU03fqHG+dbi9tBFU2Rd9QKiDZFAYrhPh2JUf7rZRIuHRKtOg==", + "dev": true, + "requires": { + "prelude-ls": "~1.1.2" + } + }, + "type-fest": { + "version": "0.8.1", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.8.1.tgz", + "integrity": "sha512-4dbzIzqvjtgiM5rw1k5rEHtBANKmdudhGyBEajN01fEyhaAIhsoKNy6y7+IN93IfpFtwY9iqi7kD+xwKhQsNJA==", + "dev": true + }, + "which": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/which/-/which-1.3.1.tgz", + "integrity": "sha512-HxJdYWq1MTIQbJ3nw0cqssHoTNU267KlrDuGZ1WYlxDStUtKUhOaJmh112/TZmHxxUfuJqPXSOm7tDyas0OSIQ==", + "dev": true, + "requires": { + "isexe": "^2.0.0" + } + } + } + }, + "eslint-config-node": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/eslint-config-node/-/eslint-config-node-4.1.0.tgz", + "integrity": "sha512-Wz17xV5O2WFG8fGdMYEBdbiL6TL7YNJSJvSX9V4sXQownewfYmoqlly7wxqLkOUv/57pq6LnnotMiQQrrPjCqQ==", + "dev": true, + "requires": { + "eslint": "^6.8.0", + "eslint-config-esnext": "^4.1.0" + }, + "dependencies": { + "acorn": { + "version": "7.4.1", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-7.4.1.tgz", + "integrity": "sha512-nQyp0o1/mNdbTO1PO6kHkwSrmgZ0MT/jCCpNiwbUjGoRN4dlBhqJtoQuCnEOKzgTVwg0ZWiCoQy6SxMebQVh8A==", + "dev": true + }, + "ansi-regex": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-4.1.1.tgz", + "integrity": "sha512-ILlv4k/3f6vfQ4OoP2AGvirOktlQ98ZEL1k9FaQjxa3L1abBgbuTDAdPOpvbGncC0BTVQrl+OM8xZGK6tWXt7g==", + "dev": true + }, + "ansi-styles": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz", + "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==", + "dev": true, + "requires": { + "color-convert": "^1.9.0" + } + }, + "chalk": { + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", + "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==", + "dev": true, + "requires": { + "ansi-styles": "^3.2.1", + "escape-string-regexp": "^1.0.5", + "supports-color": "^5.3.0" + } + }, + "color-convert": { + "version": "1.9.3", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz", + "integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==", + "dev": true, + "requires": { + "color-name": "1.1.3" + } + }, + "color-name": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz", + "integrity": "sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw==", + "dev": true + }, + "cross-spawn": { + "version": "6.0.5", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-6.0.5.tgz", + "integrity": "sha512-eTVLrBSt7fjbDygz805pMnstIs2VTBNkRm0qxZd+M7A5XDdxVRWO5MxGBXZhjY4cqLYLdtrGqRf8mBPmzwSpWQ==", + "dev": true, + "requires": { + "nice-try": "^1.0.4", + "path-key": "^2.0.1", + "semver": "^5.5.0", + "shebang-command": "^1.2.0", + "which": "^1.2.9" + }, + "dependencies": { + "semver": { + "version": "5.7.2", + "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.2.tgz", + "integrity": "sha512-cBznnQ9KjJqU67B52RMC65CMarK2600WFnbkcaiwWq3xy/5haFJlshgnpjovMVJ+Hff49d8GEn0b87C5pDQ10g==", + "dev": true + } + } + }, + "debug": { + "version": "4.3.4", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", + "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==", + "dev": true, + "requires": { + "ms": "2.1.2" + } + }, + "escape-string-regexp": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", + "integrity": "sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg==", + "dev": true + }, + "eslint": { + "version": "6.8.0", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-6.8.0.tgz", + "integrity": "sha512-K+Iayyo2LtyYhDSYwz5D5QdWw0hCacNzyq1Y821Xna2xSJj7cijoLLYmLxTQgcgZ9mC61nryMy9S7GRbYpI5Ig==", + "dev": true, + "requires": { + "@babel/code-frame": "^7.0.0", + "ajv": "^6.10.0", + "chalk": "^2.1.0", + "cross-spawn": "^6.0.5", + "debug": "^4.0.1", + "doctrine": "^3.0.0", + "eslint-scope": "^5.0.0", + "eslint-utils": "^1.4.3", + "eslint-visitor-keys": "^1.1.0", + "espree": "^6.1.2", + "esquery": "^1.0.1", + "esutils": "^2.0.2", + "file-entry-cache": "^5.0.1", + "functional-red-black-tree": "^1.0.1", + "glob-parent": "^5.0.0", + "globals": "^12.1.0", + "ignore": "^4.0.6", + "import-fresh": "^3.0.0", + "imurmurhash": "^0.1.4", + "inquirer": "^7.0.0", + "is-glob": "^4.0.0", + "js-yaml": "^3.13.1", + "json-stable-stringify-without-jsonify": "^1.0.1", + "levn": "^0.3.0", + "lodash": "^4.17.14", + "minimatch": "^3.0.4", + "mkdirp": "^0.5.1", + "natural-compare": "^1.4.0", + "optionator": "^0.8.3", + "progress": "^2.0.0", + "regexpp": "^2.0.1", + "semver": "^6.1.2", + "strip-ansi": "^5.2.0", + "strip-json-comments": "^3.0.1", + "table": "^5.2.3", + "text-table": "^0.2.0", + "v8-compile-cache": "^2.0.3" + } + }, + "eslint-utils": { + "version": "1.4.3", + "resolved": "https://registry.npmjs.org/eslint-utils/-/eslint-utils-1.4.3.tgz", + "integrity": "sha512-fbBN5W2xdY45KulGXmLHZ3c3FHfVYmKg0IrAKGOkT/464PQsx2UeIzfz1RmEci+KLm1bBaAzZAh8+/E+XAeZ8Q==", + "dev": true, + "requires": { + "eslint-visitor-keys": "^1.1.0" + } + }, + "eslint-visitor-keys": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-1.3.0.tgz", + "integrity": "sha512-6J72N8UNa462wa/KFODt/PJ3IU60SDpC3QXC1Hjc1BXXpfL2C9R5+AU7jhe0F6GREqVMh4Juu+NY7xn+6dipUQ==", + "dev": true + }, + "espree": { + "version": "6.2.1", + "resolved": "https://registry.npmjs.org/espree/-/espree-6.2.1.tgz", + "integrity": "sha512-ysCxRQY3WaXJz9tdbWOwuWr5Y/XrPTGX9Kiz3yoUXwW0VZ4w30HTkQLaGx/+ttFjF8i+ACbArnB4ce68a9m5hw==", + "dev": true, + "requires": { + "acorn": "^7.1.1", + "acorn-jsx": "^5.2.0", + "eslint-visitor-keys": "^1.1.0" + } + }, + "file-entry-cache": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-5.0.1.tgz", + "integrity": "sha512-bCg29ictuBaKUwwArK4ouCaqDgLZcysCFLmM/Yn/FDoqndh/9vNuQfXRDvTuXKLxfD/JtZQGKFT8MGcJBK644g==", + "dev": true, + "requires": { + "flat-cache": "^2.0.1" + } + }, + "flat-cache": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-2.0.1.tgz", + "integrity": "sha512-LoQe6yDuUMDzQAEH8sgmh4Md6oZnc/7PjtwjNFSzveXqSHt6ka9fPBuso7IGf9Rz4uqnSnWiFH2B/zj24a5ReA==", + "dev": true, + "requires": { + "flatted": "^2.0.0", + "rimraf": "2.6.3", + "write": "1.0.3" + } + }, + "flatted": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/flatted/-/flatted-2.0.2.tgz", + "integrity": "sha512-r5wGx7YeOwNWNlCA0wQ86zKyDLMQr+/RB8xy74M4hTphfmjlijTSSXGuH8rnvKZnfT9i+75zmd8jcKdMR4O6jA==", + "dev": true + }, + "globals": { + "version": "12.4.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-12.4.0.tgz", + "integrity": "sha512-BWICuzzDvDoH54NHKCseDanAhE3CeDorgDL5MT6LMXXj2WCnd9UC2szdk4AWLfjdgNBCXLUanXYcpBBKOSWGwg==", + "dev": true, + "requires": { + "type-fest": "^0.8.1" + } + }, + "has-flag": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", + "integrity": "sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==", + "dev": true + }, + "ignore": { + "version": "4.0.6", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-4.0.6.tgz", + "integrity": "sha512-cyFDKrqc/YdcWFniJhzI42+AzS+gNwmUzOSFcRCQYwySuBBBy/KjuxWLZ/FHEH6Moq1NizMOBWyTcv8O4OZIMg==", + "dev": true + }, + "levn": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/levn/-/levn-0.3.0.tgz", + "integrity": "sha512-0OO4y2iOHix2W6ujICbKIaEQXvFQHue65vUG3pb5EUomzPI90z9hsA1VsO/dbIIpC53J8gxM9Q4Oho0jrCM/yA==", + "dev": true, + "requires": { + "prelude-ls": "~1.1.2", + "type-check": "~0.3.2" + } + }, + "ms": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", + "dev": true + }, + "optionator": { + "version": "0.8.3", + "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.8.3.tgz", + "integrity": "sha512-+IW9pACdk3XWmmTXG8m3upGUJst5XRGzxMRjXzAuJ1XnIFNvfhjjIuYkDvysnPQ7qzqVzLt78BCruntqRhWQbA==", + "dev": true, + "requires": { + "deep-is": "~0.1.3", + "fast-levenshtein": "~2.0.6", + "levn": "~0.3.0", + "prelude-ls": "~1.1.2", + "type-check": "~0.3.2", + "word-wrap": "~1.2.3" + } + }, + "path-key": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-2.0.1.tgz", + "integrity": "sha512-fEHGKCSmUSDPv4uoj8AlD+joPlq3peND+HRYyxFz4KPw4z926S/b8rIuFs2FYJg3BwsxJf6A9/3eIdLaYC+9Dw==", + "dev": true + }, + "prelude-ls": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.1.2.tgz", + "integrity": "sha512-ESF23V4SKG6lVSGZgYNpbsiaAkdab6ZgOxe52p7+Kid3W3u3bxR4Vfd/o21dmN7jSt0IwgZ4v5MUd26FEtXE9w==", + "dev": true + }, + "regexpp": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/regexpp/-/regexpp-2.0.1.tgz", + "integrity": "sha512-lv0M6+TkDVniA3aD1Eg0DVpfU/booSu7Eev3TDO/mZKHBfVjgCGTV4t4buppESEYDtkArYFOxTJWv6S5C+iaNw==", + "dev": true + }, + "rimraf": { + "version": "2.6.3", + "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-2.6.3.tgz", + "integrity": "sha512-mwqeW5XsA2qAejG46gYdENaxXjx9onRNCfn7L0duuP4hCuTIi/QO7PDK07KJfp1d+izWPrzEJDcSqBa0OZQriA==", + "dev": true, + "requires": { + "glob": "^7.1.3" + } + }, + "shebang-command": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-1.2.0.tgz", + "integrity": "sha512-EV3L1+UQWGor21OmnvojK36mhg+TyIKDh3iFBKBohr5xeXIhNBcx8oWdgkTEEQ+BEFFYdLRuqMfd5L84N1V5Vg==", + "dev": true, + "requires": { + "shebang-regex": "^1.0.0" + } + }, + "shebang-regex": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-1.0.0.tgz", + "integrity": "sha512-wpoSFAxys6b2a2wHZ1XpDSgD7N9iVjg29Ph9uV/uaP9Ex/KXlkTZTeddxDPSYQpgvzKLGJke2UU0AzoGCjNIvQ==", + "dev": true + }, + "strip-ansi": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-5.2.0.tgz", + "integrity": "sha512-DuRs1gKbBqsMKIZlrffwlug8MHkcnpjs5VPmL1PAh+mA30U0DTotfDZ0d2UUsXpPmPmMMJ6W773MaA3J+lbiWA==", + "dev": true, + "requires": { + "ansi-regex": "^4.1.0" + } + }, + "supports-color": { + "version": "5.5.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", + "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", + "dev": true, + "requires": { + "has-flag": "^3.0.0" + } + }, + "type-check": { + "version": "0.3.2", + "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.3.2.tgz", + "integrity": "sha512-ZCmOJdvOWDBYJlzAoFkC+Q0+bUyEOS1ltgp1MGU03fqHG+dbi9tBFU2Rd9QKiDZFAYrhPh2JUf7rZRIuHRKtOg==", + "dev": true, + "requires": { + "prelude-ls": "~1.1.2" + } + }, + "type-fest": { + "version": "0.8.1", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.8.1.tgz", + "integrity": "sha512-4dbzIzqvjtgiM5rw1k5rEHtBANKmdudhGyBEajN01fEyhaAIhsoKNy6y7+IN93IfpFtwY9iqi7kD+xwKhQsNJA==", + "dev": true + }, + "which": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/which/-/which-1.3.1.tgz", + "integrity": "sha512-HxJdYWq1MTIQbJ3nw0cqssHoTNU267KlrDuGZ1WYlxDStUtKUhOaJmh112/TZmHxxUfuJqPXSOm7tDyas0OSIQ==", + "dev": true, + "requires": { + "isexe": "^2.0.0" + } + } + } + }, + "eslint-config-prettier": { + "version": "9.0.0", + "resolved": "https://registry.npmjs.org/eslint-config-prettier/-/eslint-config-prettier-9.0.0.tgz", + "integrity": "sha512-IcJsTkJae2S35pRsRAwoCE+925rJJStOdkKnLVgtE+tEpqU0EVVM7OqrwxqgptKdX29NUwC82I5pXsGFIgSevw==", + "dev": true + }, + "eslint-config-react-native": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/eslint-config-react-native/-/eslint-config-react-native-4.1.0.tgz", + "integrity": "sha512-kNND+cs+ztawH7wgajf/K6FfNshjlDsFDAkkFZF9HAXDgH1w1sNMIfTfwzufg0hOcSK7rbiL4qbG/gg/oR507Q==", + "dev": true, + "requires": { + "eslint": "^6.8.0", + "eslint-config-esnext": "^4.1.0", + "eslint-plugin-react": "^7.19.0", + "eslint-plugin-react-native": "^3.8.1" + }, + "dependencies": { + "acorn": { + "version": "7.4.1", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-7.4.1.tgz", + "integrity": "sha512-nQyp0o1/mNdbTO1PO6kHkwSrmgZ0MT/jCCpNiwbUjGoRN4dlBhqJtoQuCnEOKzgTVwg0ZWiCoQy6SxMebQVh8A==", + "dev": true + }, + "ansi-regex": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-4.1.1.tgz", + "integrity": "sha512-ILlv4k/3f6vfQ4OoP2AGvirOktlQ98ZEL1k9FaQjxa3L1abBgbuTDAdPOpvbGncC0BTVQrl+OM8xZGK6tWXt7g==", + "dev": true + }, + "ansi-styles": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz", + "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==", + "dev": true, + "requires": { + "color-convert": "^1.9.0" + } + }, + "chalk": { + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", + "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==", + "dev": true, + "requires": { + "ansi-styles": "^3.2.1", + "escape-string-regexp": "^1.0.5", + "supports-color": "^5.3.0" + } + }, + "color-convert": { + "version": "1.9.3", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz", + "integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==", + "dev": true, + "requires": { + "color-name": "1.1.3" + } + }, + "color-name": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz", + "integrity": "sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw==", + "dev": true + }, + "cross-spawn": { + "version": "6.0.5", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-6.0.5.tgz", + "integrity": "sha512-eTVLrBSt7fjbDygz805pMnstIs2VTBNkRm0qxZd+M7A5XDdxVRWO5MxGBXZhjY4cqLYLdtrGqRf8mBPmzwSpWQ==", + "dev": true, + "requires": { + "nice-try": "^1.0.4", + "path-key": "^2.0.1", + "semver": "^5.5.0", + "shebang-command": "^1.2.0", + "which": "^1.2.9" + }, + "dependencies": { + "semver": { + "version": "5.7.2", + "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.2.tgz", + "integrity": "sha512-cBznnQ9KjJqU67B52RMC65CMarK2600WFnbkcaiwWq3xy/5haFJlshgnpjovMVJ+Hff49d8GEn0b87C5pDQ10g==", + "dev": true + } + } + }, + "debug": { + "version": "4.3.4", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", + "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==", + "dev": true, + "requires": { + "ms": "2.1.2" + } + }, + "escape-string-regexp": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", + "integrity": "sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg==", + "dev": true + }, + "eslint": { + "version": "6.8.0", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-6.8.0.tgz", + "integrity": "sha512-K+Iayyo2LtyYhDSYwz5D5QdWw0hCacNzyq1Y821Xna2xSJj7cijoLLYmLxTQgcgZ9mC61nryMy9S7GRbYpI5Ig==", + "dev": true, + "requires": { + "@babel/code-frame": "^7.0.0", + "ajv": "^6.10.0", + "chalk": "^2.1.0", + "cross-spawn": "^6.0.5", + "debug": "^4.0.1", + "doctrine": "^3.0.0", + "eslint-scope": "^5.0.0", + "eslint-utils": "^1.4.3", + "eslint-visitor-keys": "^1.1.0", + "espree": "^6.1.2", + "esquery": "^1.0.1", + "esutils": "^2.0.2", + "file-entry-cache": "^5.0.1", + "functional-red-black-tree": "^1.0.1", + "glob-parent": "^5.0.0", + "globals": "^12.1.0", + "ignore": "^4.0.6", + "import-fresh": "^3.0.0", + "imurmurhash": "^0.1.4", + "inquirer": "^7.0.0", + "is-glob": "^4.0.0", + "js-yaml": "^3.13.1", + "json-stable-stringify-without-jsonify": "^1.0.1", + "levn": "^0.3.0", + "lodash": "^4.17.14", + "minimatch": "^3.0.4", + "mkdirp": "^0.5.1", + "natural-compare": "^1.4.0", + "optionator": "^0.8.3", + "progress": "^2.0.0", + "regexpp": "^2.0.1", + "semver": "^6.1.2", + "strip-ansi": "^5.2.0", + "strip-json-comments": "^3.0.1", + "table": "^5.2.3", + "text-table": "^0.2.0", + "v8-compile-cache": "^2.0.3" + } + }, + "eslint-plugin-react-native": { + "version": "3.11.0", + "resolved": "https://registry.npmjs.org/eslint-plugin-react-native/-/eslint-plugin-react-native-3.11.0.tgz", + "integrity": "sha512-7F3OTwrtQPfPFd+VygqKA2VZ0f2fz0M4gJmry/TRE18JBb94/OtMxwbL7Oqwu7FGyrdeIOWnXQbBAveMcSTZIA==", + "dev": true, + "requires": { + "@babel/traverse": "^7.7.4", + "eslint-plugin-react-native-globals": "^0.1.1" + } + }, + "eslint-utils": { + "version": "1.4.3", + "resolved": "https://registry.npmjs.org/eslint-utils/-/eslint-utils-1.4.3.tgz", + "integrity": "sha512-fbBN5W2xdY45KulGXmLHZ3c3FHfVYmKg0IrAKGOkT/464PQsx2UeIzfz1RmEci+KLm1bBaAzZAh8+/E+XAeZ8Q==", + "dev": true, + "requires": { + "eslint-visitor-keys": "^1.1.0" + } + }, + "eslint-visitor-keys": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-1.3.0.tgz", + "integrity": "sha512-6J72N8UNa462wa/KFODt/PJ3IU60SDpC3QXC1Hjc1BXXpfL2C9R5+AU7jhe0F6GREqVMh4Juu+NY7xn+6dipUQ==", + "dev": true + }, + "espree": { + "version": "6.2.1", + "resolved": "https://registry.npmjs.org/espree/-/espree-6.2.1.tgz", + "integrity": "sha512-ysCxRQY3WaXJz9tdbWOwuWr5Y/XrPTGX9Kiz3yoUXwW0VZ4w30HTkQLaGx/+ttFjF8i+ACbArnB4ce68a9m5hw==", + "dev": true, + "requires": { + "acorn": "^7.1.1", + "acorn-jsx": "^5.2.0", + "eslint-visitor-keys": "^1.1.0" + } + }, + "file-entry-cache": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-5.0.1.tgz", + "integrity": "sha512-bCg29ictuBaKUwwArK4ouCaqDgLZcysCFLmM/Yn/FDoqndh/9vNuQfXRDvTuXKLxfD/JtZQGKFT8MGcJBK644g==", + "dev": true, + "requires": { + "flat-cache": "^2.0.1" + } + }, + "flat-cache": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-2.0.1.tgz", + "integrity": "sha512-LoQe6yDuUMDzQAEH8sgmh4Md6oZnc/7PjtwjNFSzveXqSHt6ka9fPBuso7IGf9Rz4uqnSnWiFH2B/zj24a5ReA==", + "dev": true, + "requires": { + "flatted": "^2.0.0", + "rimraf": "2.6.3", + "write": "1.0.3" + } + }, + "flatted": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/flatted/-/flatted-2.0.2.tgz", + "integrity": "sha512-r5wGx7YeOwNWNlCA0wQ86zKyDLMQr+/RB8xy74M4hTphfmjlijTSSXGuH8rnvKZnfT9i+75zmd8jcKdMR4O6jA==", + "dev": true + }, + "globals": { + "version": "12.4.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-12.4.0.tgz", + "integrity": "sha512-BWICuzzDvDoH54NHKCseDanAhE3CeDorgDL5MT6LMXXj2WCnd9UC2szdk4AWLfjdgNBCXLUanXYcpBBKOSWGwg==", + "dev": true, + "requires": { + "type-fest": "^0.8.1" + } + }, + "has-flag": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", + "integrity": "sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==", + "dev": true + }, + "ignore": { + "version": "4.0.6", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-4.0.6.tgz", + "integrity": "sha512-cyFDKrqc/YdcWFniJhzI42+AzS+gNwmUzOSFcRCQYwySuBBBy/KjuxWLZ/FHEH6Moq1NizMOBWyTcv8O4OZIMg==", + "dev": true + }, + "levn": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/levn/-/levn-0.3.0.tgz", + "integrity": "sha512-0OO4y2iOHix2W6ujICbKIaEQXvFQHue65vUG3pb5EUomzPI90z9hsA1VsO/dbIIpC53J8gxM9Q4Oho0jrCM/yA==", + "dev": true, + "requires": { + "prelude-ls": "~1.1.2", + "type-check": "~0.3.2" + } + }, + "ms": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", + "dev": true + }, + "optionator": { + "version": "0.8.3", + "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.8.3.tgz", + "integrity": "sha512-+IW9pACdk3XWmmTXG8m3upGUJst5XRGzxMRjXzAuJ1XnIFNvfhjjIuYkDvysnPQ7qzqVzLt78BCruntqRhWQbA==", + "dev": true, + "requires": { + "deep-is": "~0.1.3", + "fast-levenshtein": "~2.0.6", + "levn": "~0.3.0", + "prelude-ls": "~1.1.2", + "type-check": "~0.3.2", + "word-wrap": "~1.2.3" + } + }, + "path-key": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-2.0.1.tgz", + "integrity": "sha512-fEHGKCSmUSDPv4uoj8AlD+joPlq3peND+HRYyxFz4KPw4z926S/b8rIuFs2FYJg3BwsxJf6A9/3eIdLaYC+9Dw==", + "dev": true + }, + "prelude-ls": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.1.2.tgz", + "integrity": "sha512-ESF23V4SKG6lVSGZgYNpbsiaAkdab6ZgOxe52p7+Kid3W3u3bxR4Vfd/o21dmN7jSt0IwgZ4v5MUd26FEtXE9w==", + "dev": true + }, + "regexpp": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/regexpp/-/regexpp-2.0.1.tgz", + "integrity": "sha512-lv0M6+TkDVniA3aD1Eg0DVpfU/booSu7Eev3TDO/mZKHBfVjgCGTV4t4buppESEYDtkArYFOxTJWv6S5C+iaNw==", + "dev": true + }, + "rimraf": { + "version": "2.6.3", + "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-2.6.3.tgz", + "integrity": "sha512-mwqeW5XsA2qAejG46gYdENaxXjx9onRNCfn7L0duuP4hCuTIi/QO7PDK07KJfp1d+izWPrzEJDcSqBa0OZQriA==", + "dev": true, + "requires": { + "glob": "^7.1.3" + } + }, + "shebang-command": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-1.2.0.tgz", + "integrity": "sha512-EV3L1+UQWGor21OmnvojK36mhg+TyIKDh3iFBKBohr5xeXIhNBcx8oWdgkTEEQ+BEFFYdLRuqMfd5L84N1V5Vg==", + "dev": true, + "requires": { + "shebang-regex": "^1.0.0" + } + }, + "shebang-regex": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-1.0.0.tgz", + "integrity": "sha512-wpoSFAxys6b2a2wHZ1XpDSgD7N9iVjg29Ph9uV/uaP9Ex/KXlkTZTeddxDPSYQpgvzKLGJke2UU0AzoGCjNIvQ==", + "dev": true + }, + "strip-ansi": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-5.2.0.tgz", + "integrity": "sha512-DuRs1gKbBqsMKIZlrffwlug8MHkcnpjs5VPmL1PAh+mA30U0DTotfDZ0d2UUsXpPmPmMMJ6W773MaA3J+lbiWA==", + "dev": true, + "requires": { + "ansi-regex": "^4.1.0" + } + }, + "supports-color": { + "version": "5.5.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", + "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", + "dev": true, + "requires": { + "has-flag": "^3.0.0" + } + }, + "type-check": { + "version": "0.3.2", + "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.3.2.tgz", + "integrity": "sha512-ZCmOJdvOWDBYJlzAoFkC+Q0+bUyEOS1ltgp1MGU03fqHG+dbi9tBFU2Rd9QKiDZFAYrhPh2JUf7rZRIuHRKtOg==", + "dev": true, + "requires": { + "prelude-ls": "~1.1.2" + } + }, + "type-fest": { + "version": "0.8.1", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.8.1.tgz", + "integrity": "sha512-4dbzIzqvjtgiM5rw1k5rEHtBANKmdudhGyBEajN01fEyhaAIhsoKNy6y7+IN93IfpFtwY9iqi7kD+xwKhQsNJA==", + "dev": true + }, + "which": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/which/-/which-1.3.1.tgz", + "integrity": "sha512-HxJdYWq1MTIQbJ3nw0cqssHoTNU267KlrDuGZ1WYlxDStUtKUhOaJmh112/TZmHxxUfuJqPXSOm7tDyas0OSIQ==", + "dev": true, + "requires": { + "isexe": "^2.0.0" + } + } + } + }, + "eslint-config-recommended": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/eslint-config-recommended/-/eslint-config-recommended-4.1.0.tgz", + "integrity": "sha512-2evA0SX1VqtyFiExmBI2WAO4XQCKlr7wmNELE8rcT5PyZY2ixsY881ofVZWKuI/dywpgLiES1gR/XUQcnVLRzQ==", + "dev": true, + "requires": { + "eslint": "^6.8.0", + "eslint-config-esnext": "^4.1.0", + "eslint-config-node": "^4.1.0", + "eslint-config-react-native": "^4.1.0" + }, + "dependencies": { + "acorn": { + "version": "7.4.1", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-7.4.1.tgz", + "integrity": "sha512-nQyp0o1/mNdbTO1PO6kHkwSrmgZ0MT/jCCpNiwbUjGoRN4dlBhqJtoQuCnEOKzgTVwg0ZWiCoQy6SxMebQVh8A==", + "dev": true + }, + "ansi-regex": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-4.1.1.tgz", + "integrity": "sha512-ILlv4k/3f6vfQ4OoP2AGvirOktlQ98ZEL1k9FaQjxa3L1abBgbuTDAdPOpvbGncC0BTVQrl+OM8xZGK6tWXt7g==", + "dev": true + }, + "ansi-styles": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz", + "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==", + "dev": true, + "requires": { + "color-convert": "^1.9.0" + } + }, + "chalk": { + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", + "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==", + "dev": true, + "requires": { + "ansi-styles": "^3.2.1", + "escape-string-regexp": "^1.0.5", + "supports-color": "^5.3.0" + } + }, + "color-convert": { + "version": "1.9.3", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz", + "integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==", + "dev": true, + "requires": { + "color-name": "1.1.3" + } + }, + "color-name": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz", + "integrity": "sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw==", + "dev": true + }, + "cross-spawn": { + "version": "6.0.5", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-6.0.5.tgz", + "integrity": "sha512-eTVLrBSt7fjbDygz805pMnstIs2VTBNkRm0qxZd+M7A5XDdxVRWO5MxGBXZhjY4cqLYLdtrGqRf8mBPmzwSpWQ==", + "dev": true, + "requires": { + "nice-try": "^1.0.4", + "path-key": "^2.0.1", + "semver": "^5.5.0", + "shebang-command": "^1.2.0", + "which": "^1.2.9" + }, + "dependencies": { + "semver": { + "version": "5.7.2", + "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.2.tgz", + "integrity": "sha512-cBznnQ9KjJqU67B52RMC65CMarK2600WFnbkcaiwWq3xy/5haFJlshgnpjovMVJ+Hff49d8GEn0b87C5pDQ10g==", + "dev": true + } + } + }, + "debug": { + "version": "4.3.4", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", + "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==", + "dev": true, + "requires": { + "ms": "2.1.2" + } + }, + "escape-string-regexp": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", + "integrity": "sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg==", + "dev": true + }, + "eslint": { + "version": "6.8.0", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-6.8.0.tgz", + "integrity": "sha512-K+Iayyo2LtyYhDSYwz5D5QdWw0hCacNzyq1Y821Xna2xSJj7cijoLLYmLxTQgcgZ9mC61nryMy9S7GRbYpI5Ig==", + "dev": true, + "requires": { + "@babel/code-frame": "^7.0.0", + "ajv": "^6.10.0", + "chalk": "^2.1.0", + "cross-spawn": "^6.0.5", + "debug": "^4.0.1", + "doctrine": "^3.0.0", + "eslint-scope": "^5.0.0", + "eslint-utils": "^1.4.3", + "eslint-visitor-keys": "^1.1.0", + "espree": "^6.1.2", + "esquery": "^1.0.1", + "esutils": "^2.0.2", + "file-entry-cache": "^5.0.1", + "functional-red-black-tree": "^1.0.1", + "glob-parent": "^5.0.0", + "globals": "^12.1.0", + "ignore": "^4.0.6", + "import-fresh": "^3.0.0", + "imurmurhash": "^0.1.4", + "inquirer": "^7.0.0", + "is-glob": "^4.0.0", + "js-yaml": "^3.13.1", + "json-stable-stringify-without-jsonify": "^1.0.1", + "levn": "^0.3.0", + "lodash": "^4.17.14", + "minimatch": "^3.0.4", + "mkdirp": "^0.5.1", + "natural-compare": "^1.4.0", + "optionator": "^0.8.3", + "progress": "^2.0.0", + "regexpp": "^2.0.1", + "semver": "^6.1.2", + "strip-ansi": "^5.2.0", + "strip-json-comments": "^3.0.1", + "table": "^5.2.3", + "text-table": "^0.2.0", + "v8-compile-cache": "^2.0.3" + } + }, + "eslint-utils": { + "version": "1.4.3", + "resolved": "https://registry.npmjs.org/eslint-utils/-/eslint-utils-1.4.3.tgz", + "integrity": "sha512-fbBN5W2xdY45KulGXmLHZ3c3FHfVYmKg0IrAKGOkT/464PQsx2UeIzfz1RmEci+KLm1bBaAzZAh8+/E+XAeZ8Q==", + "dev": true, + "requires": { + "eslint-visitor-keys": "^1.1.0" + } + }, + "eslint-visitor-keys": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-1.3.0.tgz", + "integrity": "sha512-6J72N8UNa462wa/KFODt/PJ3IU60SDpC3QXC1Hjc1BXXpfL2C9R5+AU7jhe0F6GREqVMh4Juu+NY7xn+6dipUQ==", + "dev": true + }, + "espree": { + "version": "6.2.1", + "resolved": "https://registry.npmjs.org/espree/-/espree-6.2.1.tgz", + "integrity": "sha512-ysCxRQY3WaXJz9tdbWOwuWr5Y/XrPTGX9Kiz3yoUXwW0VZ4w30HTkQLaGx/+ttFjF8i+ACbArnB4ce68a9m5hw==", + "dev": true, + "requires": { + "acorn": "^7.1.1", + "acorn-jsx": "^5.2.0", + "eslint-visitor-keys": "^1.1.0" + } + }, + "file-entry-cache": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-5.0.1.tgz", + "integrity": "sha512-bCg29ictuBaKUwwArK4ouCaqDgLZcysCFLmM/Yn/FDoqndh/9vNuQfXRDvTuXKLxfD/JtZQGKFT8MGcJBK644g==", + "dev": true, + "requires": { + "flat-cache": "^2.0.1" + } + }, + "flat-cache": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-2.0.1.tgz", + "integrity": "sha512-LoQe6yDuUMDzQAEH8sgmh4Md6oZnc/7PjtwjNFSzveXqSHt6ka9fPBuso7IGf9Rz4uqnSnWiFH2B/zj24a5ReA==", + "dev": true, + "requires": { + "flatted": "^2.0.0", + "rimraf": "2.6.3", + "write": "1.0.3" + } + }, + "flatted": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/flatted/-/flatted-2.0.2.tgz", + "integrity": "sha512-r5wGx7YeOwNWNlCA0wQ86zKyDLMQr+/RB8xy74M4hTphfmjlijTSSXGuH8rnvKZnfT9i+75zmd8jcKdMR4O6jA==", + "dev": true + }, + "globals": { + "version": "12.4.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-12.4.0.tgz", + "integrity": "sha512-BWICuzzDvDoH54NHKCseDanAhE3CeDorgDL5MT6LMXXj2WCnd9UC2szdk4AWLfjdgNBCXLUanXYcpBBKOSWGwg==", + "dev": true, + "requires": { + "type-fest": "^0.8.1" + } + }, + "has-flag": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", + "integrity": "sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==", + "dev": true + }, + "ignore": { + "version": "4.0.6", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-4.0.6.tgz", + "integrity": "sha512-cyFDKrqc/YdcWFniJhzI42+AzS+gNwmUzOSFcRCQYwySuBBBy/KjuxWLZ/FHEH6Moq1NizMOBWyTcv8O4OZIMg==", + "dev": true + }, + "levn": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/levn/-/levn-0.3.0.tgz", + "integrity": "sha512-0OO4y2iOHix2W6ujICbKIaEQXvFQHue65vUG3pb5EUomzPI90z9hsA1VsO/dbIIpC53J8gxM9Q4Oho0jrCM/yA==", + "dev": true, + "requires": { + "prelude-ls": "~1.1.2", + "type-check": "~0.3.2" + } + }, + "ms": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", + "dev": true + }, + "optionator": { + "version": "0.8.3", + "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.8.3.tgz", + "integrity": "sha512-+IW9pACdk3XWmmTXG8m3upGUJst5XRGzxMRjXzAuJ1XnIFNvfhjjIuYkDvysnPQ7qzqVzLt78BCruntqRhWQbA==", + "dev": true, + "requires": { + "deep-is": "~0.1.3", + "fast-levenshtein": "~2.0.6", + "levn": "~0.3.0", + "prelude-ls": "~1.1.2", + "type-check": "~0.3.2", + "word-wrap": "~1.2.3" + } + }, + "path-key": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-2.0.1.tgz", + "integrity": "sha512-fEHGKCSmUSDPv4uoj8AlD+joPlq3peND+HRYyxFz4KPw4z926S/b8rIuFs2FYJg3BwsxJf6A9/3eIdLaYC+9Dw==", + "dev": true + }, + "prelude-ls": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.1.2.tgz", + "integrity": "sha512-ESF23V4SKG6lVSGZgYNpbsiaAkdab6ZgOxe52p7+Kid3W3u3bxR4Vfd/o21dmN7jSt0IwgZ4v5MUd26FEtXE9w==", + "dev": true + }, + "regexpp": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/regexpp/-/regexpp-2.0.1.tgz", + "integrity": "sha512-lv0M6+TkDVniA3aD1Eg0DVpfU/booSu7Eev3TDO/mZKHBfVjgCGTV4t4buppESEYDtkArYFOxTJWv6S5C+iaNw==", + "dev": true + }, + "rimraf": { + "version": "2.6.3", + "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-2.6.3.tgz", + "integrity": "sha512-mwqeW5XsA2qAejG46gYdENaxXjx9onRNCfn7L0duuP4hCuTIi/QO7PDK07KJfp1d+izWPrzEJDcSqBa0OZQriA==", + "dev": true, + "requires": { + "glob": "^7.1.3" + } + }, + "shebang-command": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-1.2.0.tgz", + "integrity": "sha512-EV3L1+UQWGor21OmnvojK36mhg+TyIKDh3iFBKBohr5xeXIhNBcx8oWdgkTEEQ+BEFFYdLRuqMfd5L84N1V5Vg==", + "dev": true, + "requires": { + "shebang-regex": "^1.0.0" + } + }, + "shebang-regex": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-1.0.0.tgz", + "integrity": "sha512-wpoSFAxys6b2a2wHZ1XpDSgD7N9iVjg29Ph9uV/uaP9Ex/KXlkTZTeddxDPSYQpgvzKLGJke2UU0AzoGCjNIvQ==", + "dev": true + }, + "strip-ansi": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-5.2.0.tgz", + "integrity": "sha512-DuRs1gKbBqsMKIZlrffwlug8MHkcnpjs5VPmL1PAh+mA30U0DTotfDZ0d2UUsXpPmPmMMJ6W773MaA3J+lbiWA==", + "dev": true, + "requires": { + "ansi-regex": "^4.1.0" + } + }, + "supports-color": { + "version": "5.5.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", + "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", + "dev": true, + "requires": { + "has-flag": "^3.0.0" + } + }, + "type-check": { + "version": "0.3.2", + "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.3.2.tgz", + "integrity": "sha512-ZCmOJdvOWDBYJlzAoFkC+Q0+bUyEOS1ltgp1MGU03fqHG+dbi9tBFU2Rd9QKiDZFAYrhPh2JUf7rZRIuHRKtOg==", + "dev": true, + "requires": { + "prelude-ls": "~1.1.2" + } + }, + "type-fest": { + "version": "0.8.1", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.8.1.tgz", + "integrity": "sha512-4dbzIzqvjtgiM5rw1k5rEHtBANKmdudhGyBEajN01fEyhaAIhsoKNy6y7+IN93IfpFtwY9iqi7kD+xwKhQsNJA==", + "dev": true + }, + "which": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/which/-/which-1.3.1.tgz", + "integrity": "sha512-HxJdYWq1MTIQbJ3nw0cqssHoTNU267KlrDuGZ1WYlxDStUtKUhOaJmh112/TZmHxxUfuJqPXSOm7tDyas0OSIQ==", + "dev": true, + "requires": { + "isexe": "^2.0.0" + } + } + } + }, + "eslint-import-resolver-node": { + "version": "0.3.6", + "resolved": "https://registry.npmjs.org/eslint-import-resolver-node/-/eslint-import-resolver-node-0.3.6.tgz", + "integrity": "sha512-0En0w03NRVMn9Uiyn8YRPDKvWjxCWkslUEhGNTdGx15RvPJYQ+lbOlqrlNI2vEAs4pDYK4f/HN2TbDmk5TP0iw==", + "dev": true, + "requires": { + "debug": "^3.2.7", + "resolve": "^1.20.0" + }, + "dependencies": { + "debug": { + "version": "3.2.7", + "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.7.tgz", + "integrity": "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==", + "dev": true, + "requires": { + "ms": "^2.1.1" + } + }, + "ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "dev": true + } + } + }, + "eslint-module-utils": { + "version": "2.7.4", + "resolved": "https://registry.npmjs.org/eslint-module-utils/-/eslint-module-utils-2.7.4.tgz", + "integrity": "sha512-j4GT+rqzCoRKHwURX7pddtIPGySnX9Si/cgMI5ztrcqOPtk5dDEeZ34CQVPphnqkJytlc97Vuk05Um2mJ3gEQA==", + "dev": true, + "requires": { + "debug": "^3.2.7" + }, + "dependencies": { + "debug": { + "version": "3.2.7", + "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.7.tgz", + "integrity": "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==", + "dev": true, + "requires": { + "ms": "^2.1.1" + } + }, + "ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "dev": true + } + } + }, + "eslint-plugin-babel": { + "version": "5.3.1", + "resolved": "https://registry.npmjs.org/eslint-plugin-babel/-/eslint-plugin-babel-5.3.1.tgz", + "integrity": "sha512-VsQEr6NH3dj664+EyxJwO4FCYm/00JhYb3Sk3ft8o+fpKuIfQ9TaW6uVUfvwMXHcf/lsnRIoyFPsLMyiWCSL/g==", + "dev": true, + "requires": { + "eslint-rule-composer": "^0.3.0" + } + }, + "eslint-plugin-import": { + "version": "2.26.0", + "resolved": "https://registry.npmjs.org/eslint-plugin-import/-/eslint-plugin-import-2.26.0.tgz", + "integrity": "sha512-hYfi3FXaM8WPLf4S1cikh/r4IxnO6zrhZbEGz2b660EJRbuxgpDS5gkCuYgGWg2xxh2rBuIr4Pvhve/7c31koA==", + "dev": true, + "requires": { + "array-includes": "^3.1.4", + "array.prototype.flat": "^1.2.5", + "debug": "^2.6.9", + "doctrine": "^2.1.0", + "eslint-import-resolver-node": "^0.3.6", + "eslint-module-utils": "^2.7.3", + "has": "^1.0.3", + "is-core-module": "^2.8.1", + "is-glob": "^4.0.3", + "minimatch": "^3.1.2", + "object.values": "^1.1.5", + "resolve": "^1.22.0", + "tsconfig-paths": "^3.14.1" + }, + "dependencies": { + "doctrine": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/doctrine/-/doctrine-2.1.0.tgz", + "integrity": "sha512-35mSku4ZXK0vfCuHEDAwt55dg2jNajHZ1odvF+8SSr82EsZY4QmXfuWso8oEd8zRhVObSN18aM0CjSdoBX7zIw==", + "dev": true, + "requires": { + "esutils": "^2.0.2" + } + } + } + }, + "eslint-plugin-prettier": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/eslint-plugin-prettier/-/eslint-plugin-prettier-5.0.1.tgz", + "integrity": "sha512-m3u5RnR56asrwV/lDC4GHorlW75DsFfmUcjfCYylTUs85dBRnB7VM6xG8eCMJdeDRnppzmxZVf1GEPJvl1JmNg==", + "dev": true, + "requires": { + "prettier-linter-helpers": "^1.0.0", + "synckit": "^0.8.5" + } + }, + "eslint-plugin-react": { + "version": "7.31.10", + "resolved": "https://registry.npmjs.org/eslint-plugin-react/-/eslint-plugin-react-7.31.10.tgz", + "integrity": "sha512-e4N/nc6AAlg4UKW/mXeYWd3R++qUano5/o+t+wnWxIf+bLsOaH3a4q74kX3nDjYym3VBN4HyO9nEn1GcAqgQOA==", + "dev": true, + "requires": { + "array-includes": "^3.1.5", + "array.prototype.flatmap": "^1.3.0", + "doctrine": "^2.1.0", + "estraverse": "^5.3.0", + "jsx-ast-utils": "^2.4.1 || ^3.0.0", + "minimatch": "^3.1.2", + "object.entries": "^1.1.5", + "object.fromentries": "^2.0.5", + "object.hasown": "^1.1.1", + "object.values": "^1.1.5", + "prop-types": "^15.8.1", + "resolve": "^2.0.0-next.3", + "semver": "^6.3.0", + "string.prototype.matchall": "^4.0.7" + }, + "dependencies": { + "doctrine": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/doctrine/-/doctrine-2.1.0.tgz", + "integrity": "sha512-35mSku4ZXK0vfCuHEDAwt55dg2jNajHZ1odvF+8SSr82EsZY4QmXfuWso8oEd8zRhVObSN18aM0CjSdoBX7zIw==", + "dev": true, + "requires": { + "esutils": "^2.0.2" + } + }, + "resolve": { + "version": "2.0.0-next.4", + "resolved": "https://registry.npmjs.org/resolve/-/resolve-2.0.0-next.4.tgz", + "integrity": "sha512-iMDbmAWtfU+MHpxt/I5iWI7cY6YVEZUQ3MBgPQ++XD1PELuJHIl82xBmObyP2KyQmkNB2dsqF7seoQQiAn5yDQ==", + "dev": true, + "requires": { + "is-core-module": "^2.9.0", + "path-parse": "^1.0.7", + "supports-preserve-symlinks-flag": "^1.0.0" + } + } + } + }, + "eslint-plugin-react-native-globals": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/eslint-plugin-react-native-globals/-/eslint-plugin-react-native-globals-0.1.2.tgz", + "integrity": "sha512-9aEPf1JEpiTjcFAmmyw8eiIXmcNZOqaZyHO77wgm0/dWfT/oxC1SrIq8ET38pMxHYrcB6Uew+TzUVsBeczF88g==", + "dev": true + }, + "eslint-rule-composer": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/eslint-rule-composer/-/eslint-rule-composer-0.3.0.tgz", + "integrity": "sha512-bt+Sh8CtDmn2OajxvNO+BX7Wn4CIWMpTRm3MaiKPCQcnnlm0CS2mhui6QaoeQugs+3Kj2ESKEEGJUdVafwhiCg==", + "dev": true + }, + "eslint-scope": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-5.1.1.tgz", + "integrity": "sha512-2NxwbF/hZ0KpepYN0cNbo+FN6XoK7GaHlQhgx/hIZl6Va0bF45RQOOwhLIy8lQDbuCiadSLCBnH2CFYquit5bw==", + "dev": true, + "requires": { + "esrecurse": "^4.3.0", + "estraverse": "^4.1.1" + }, + "dependencies": { + "estraverse": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-4.3.0.tgz", + "integrity": "sha512-39nnKffWz8xN1BU/2c79n9nB9HDzo0niYUqx6xyqUnyoAnQyyWpOTdZEeiCch8BBu515t4wp9ZmgVfVhn9EBpw==", + "dev": true + } + } + }, + "eslint-visitor-keys": { + "version": "3.4.3", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.4.3.tgz", + "integrity": "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==", + "dev": true + }, + "espree": { + "version": "9.6.1", + "resolved": "https://registry.npmjs.org/espree/-/espree-9.6.1.tgz", + "integrity": "sha512-oruZaFkjorTpF32kDSI5/75ViwGeZginGGy2NoOSg3Q9bnwlnmDm4HLnkl0RE3n+njDXR037aY1+x58Z/zFdwQ==", + "dev": true, + "requires": { + "acorn": "^8.9.0", + "acorn-jsx": "^5.3.2", + "eslint-visitor-keys": "^3.4.1" + } + }, + "esprima": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/esprima/-/esprima-4.0.1.tgz", + "integrity": "sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==", + "dev": true + }, + "esquery": { + "version": "1.4.2", + "resolved": "https://registry.npmjs.org/esquery/-/esquery-1.4.2.tgz", + "integrity": "sha512-JVSoLdTlTDkmjFmab7H/9SL9qGSyjElT3myyKp7krqjVFQCDLmj1QFaCLRFBszBKI0XVZaiiXvuPIX3ZwHe1Ng==", + "dev": true, + "requires": { + "estraverse": "^5.1.0" + } + }, + "esrecurse": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/esrecurse/-/esrecurse-4.3.0.tgz", + "integrity": "sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==", + "dev": true, + "requires": { + "estraverse": "^5.2.0" + } + }, + "estraverse": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz", + "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==", + "dev": true + }, + "esutils": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz", + "integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==", + "dev": true + }, + "etag": { + "version": "1.8.1", + "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz", + "integrity": "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==" + }, + "execa": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/execa/-/execa-5.1.1.tgz", + "integrity": "sha512-8uSpZZocAZRBAPIEINJj3Lo9HyGitllczc27Eh5YYojjMFMn8yHMDMaUHE2Jqfq05D/wucwI4JGURyXt1vchyg==", + "dev": true, + "requires": { + "cross-spawn": "^7.0.3", + "get-stream": "^6.0.0", + "human-signals": "^2.1.0", + "is-stream": "^2.0.0", + "merge-stream": "^2.0.0", + "npm-run-path": "^4.0.1", + "onetime": "^5.1.2", + "signal-exit": "^3.0.3", + "strip-final-newline": "^2.0.0" + } + }, + "exit": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/exit/-/exit-0.1.2.tgz", + "integrity": "sha512-Zk/eNKV2zbjpKzrsQ+n1G6poVbErQxJ0LBOJXaKZ1EViLzH+hrLu9cdXI4zw9dBQJslwBEpbQ2P1oS7nDxs6jQ==", + "dev": true + }, + "expect": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/expect/-/expect-29.7.0.tgz", + "integrity": "sha512-2Zks0hf1VLFYI1kbh0I5jP3KHHyCHpkfyHBzsSXRFgl/Bg9mWYfMW8oD+PdMPlEwy5HNsR9JutYy6pMeOh61nw==", + "dev": true, + "requires": { + "@jest/expect-utils": "^29.7.0", + "jest-get-type": "^29.6.3", + "jest-matcher-utils": "^29.7.0", + "jest-message-util": "^29.7.0", + "jest-util": "^29.7.0" + } + }, + "express": { + "version": "4.18.2", + "resolved": "https://registry.npmjs.org/express/-/express-4.18.2.tgz", + "integrity": "sha512-5/PsL6iGPdfQ/lKM1UuielYgv3BUoJfz1aUwU9vHZ+J7gyvwdQXFEBIEIaxeGf0GIcreATNyBExtalisDbuMqQ==", + "requires": { + "accepts": "~1.3.8", + "array-flatten": "1.1.1", + "body-parser": "1.20.1", + "content-disposition": "0.5.4", + "content-type": "~1.0.4", + "cookie": "0.5.0", + "cookie-signature": "1.0.6", + "debug": "2.6.9", + "depd": "2.0.0", + "encodeurl": "~1.0.2", + "escape-html": "~1.0.3", + "etag": "~1.8.1", + "finalhandler": "1.2.0", + "fresh": "0.5.2", + "http-errors": "2.0.0", + "merge-descriptors": "1.0.1", + "methods": "~1.1.2", + "on-finished": "2.4.1", + "parseurl": "~1.3.3", + "path-to-regexp": "0.1.7", + "proxy-addr": "~2.0.7", + "qs": "6.11.0", + "range-parser": "~1.2.1", + "safe-buffer": "5.2.1", + "send": "0.18.0", + "serve-static": "1.15.0", + "setprototypeof": "1.2.0", + "statuses": "2.0.1", + "type-is": "~1.6.18", + "utils-merge": "1.0.1", + "vary": "~1.1.2" + } + }, + "express-winston": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/express-winston/-/express-winston-4.2.0.tgz", + "integrity": "sha512-EMD74g63nVHi7pFleQw7KHCxiA1pjF5uCwbCfzGqmFxs9KvlDPIVS3cMGpULm6MshExMT9TjC3SqmRGB9kb7yw==", + "requires": { + "chalk": "^2.4.2", + "lodash": "^4.17.21" + }, + "dependencies": { + "ansi-styles": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz", + "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==", + "requires": { + "color-convert": "^1.9.0" + } + }, + "chalk": { + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", + "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==", + "requires": { + "ansi-styles": "^3.2.1", + "escape-string-regexp": "^1.0.5", + "supports-color": "^5.3.0" + } + }, + "color-convert": { + "version": "1.9.3", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz", + "integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==", + "requires": { + "color-name": "1.1.3" + } + }, + "color-name": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz", + "integrity": "sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw==" + }, + "escape-string-regexp": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", + "integrity": "sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg==" + }, + "has-flag": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", + "integrity": "sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==" + }, + "supports-color": { + "version": "5.5.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", + "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", + "requires": { + "has-flag": "^3.0.0" + } + } + } + }, + "external-editor": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/external-editor/-/external-editor-3.1.0.tgz", + "integrity": "sha512-hMQ4CX1p1izmuLYyZqLMO/qGNw10wSv9QDCPfzXfyFrOaCSSoRfqE1Kf1s5an66J5JZC62NewG+mK49jOCtQew==", + "dev": true, + "requires": { + "chardet": "^0.7.0", + "iconv-lite": "^0.4.24", + "tmp": "^0.0.33" + } + }, + "fast-deep-equal": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", + "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", + "dev": true + }, + "fast-diff": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/fast-diff/-/fast-diff-1.2.0.tgz", + "integrity": "sha512-xJuoT5+L99XlZ8twedaRf6Ax2TgQVxvgZOYoPKqZufmJib0tL2tegPBOZb1pVNgIhlqDlA0eO0c3wBvQcmzx4w==", + "dev": true + }, + "fast-glob": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.1.tgz", + "integrity": "sha512-kNFPyjhh5cKjrUltxs+wFx+ZkbRaxxmZ+X0ZU31SOsxCEtP9VPgtq2teZw1DebupL5GmDaNQ6yKMMVcM41iqDg==", + "dev": true, + "requires": { + "@nodelib/fs.stat": "^2.0.2", + "@nodelib/fs.walk": "^1.2.3", + "glob-parent": "^5.1.2", + "merge2": "^1.3.0", + "micromatch": "^4.0.4" + } + }, + "fast-json-stable-stringify": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", + "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==", + "dev": true + }, + "fast-levenshtein": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz", + "integrity": "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==", + "dev": true + }, + "fastq": { + "version": "1.13.0", + "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.13.0.tgz", + "integrity": "sha512-YpkpUnK8od0o1hmeSc7UUs/eB/vIPWJYjKck2QKIzAf71Vm1AAQ3EbuZB3g2JIy+pg+ERD0vqI79KyZiB2e2Nw==", + "dev": true, + "requires": { + "reusify": "^1.0.4" + } + }, + "fb-watchman": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/fb-watchman/-/fb-watchman-2.0.2.tgz", + "integrity": "sha512-p5161BqbuCaSnB8jIbzQHOlpgsPmK5rJVDfDKO91Axs5NC1uu3HRQm6wt9cd9/+GtQQIO53JdGXXoyDpTAsgYA==", + "dev": true, + "requires": { + "bser": "2.1.1" + } + }, + "fecha": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/fecha/-/fecha-4.2.3.tgz", + "integrity": "sha512-OP2IUU6HeYKJi3i0z4A19kHMQoLVs4Hc+DPqqxI2h/DPZHTm/vjsfC6P0b4jCMy14XizLBqvndQ+UilD7707Jw==" + }, + "figures": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/figures/-/figures-3.2.0.tgz", + "integrity": "sha512-yaduQFRKLXYOGgEn6AZau90j3ggSOyiqXU0F9JZfeXYhNa+Jk4X+s45A2zg5jns87GAFa34BBm2kXw4XpNcbdg==", + "dev": true, + "requires": { + "escape-string-regexp": "^1.0.5" + }, + "dependencies": { + "escape-string-regexp": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", + "integrity": "sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg==", + "dev": true + } + } + }, + "file-entry-cache": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-6.0.1.tgz", + "integrity": "sha512-7Gps/XWymbLk2QLYK4NzpMOrYjMhdIxXuIvy2QBsLE6ljuodKvdkWs/cpyJJ3CVIVpH0Oi1Hvg1ovbMzLdFBBg==", + "dev": true, + "requires": { + "flat-cache": "^3.0.4" + } + }, + "fill-range": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.0.1.tgz", + "integrity": "sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ==", + "dev": true, + "requires": { + "to-regex-range": "^5.0.1" + } + }, + "finalhandler": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.2.0.tgz", + "integrity": "sha512-5uXcUVftlQMFnWC9qu/svkWv3GTd2PfUhK/3PLkYNAe7FbqJMt3515HaxE6eRL74GdsriiwujiawdaB1BpEISg==", + "requires": { + "debug": "2.6.9", + "encodeurl": "~1.0.2", + "escape-html": "~1.0.3", + "on-finished": "2.4.1", + "parseurl": "~1.3.3", + "statuses": "2.0.1", + "unpipe": "~1.0.0" + } + }, + "find-up": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-4.1.0.tgz", + "integrity": "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==", + "dev": true, + "requires": { + "locate-path": "^5.0.0", + "path-exists": "^4.0.0" + } + }, + "flat-cache": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-3.0.4.tgz", + "integrity": "sha512-dm9s5Pw7Jc0GvMYbshN6zchCA9RgQlzzEZX3vylR9IqFfS8XciblUXOKfW6SiuJ0e13eDYZoZV5wdrev7P3Nwg==", + "dev": true, + "requires": { + "flatted": "^3.1.0", + "rimraf": "^3.0.2" + }, + "dependencies": { + "rimraf": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz", + "integrity": "sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==", + "dev": true, + "requires": { + "glob": "^7.1.3" + } + } + } + }, + "flatted": { + "version": "3.2.7", + "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.2.7.tgz", + "integrity": "sha512-5nqDSxl8nn5BSNxyR3n4I6eDmbolI6WT+QqR547RwxQapgjQBmtktdP+HTBb/a/zLsbzERTONyUB5pefh5TtjQ==", + "dev": true + }, + "fn.name": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/fn.name/-/fn.name-1.1.0.tgz", + "integrity": "sha512-GRnmB5gPyJpAhTQdSZTSp9uaPSvl09KoYcMQtsB9rQoOmzs9dH6ffeccH+Z+cv6P68Hu5bC6JjRh4Ah/mHSNRw==" + }, + "follow-redirects": { + "version": "1.15.2", + "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.2.tgz", + "integrity": "sha512-VQLG33o04KaQ8uYi2tVNbdrWp1QWxNNea+nmIB4EVM28v0hmP17z7aG1+wAkNzVq4KeXTq3221ye5qTJP91JwA==" + }, + "foreground-child": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/foreground-child/-/foreground-child-3.1.1.tgz", + "integrity": "sha512-TMKDUnIte6bfb5nWv7V/caI169OHgvwjb7V4WkeUvbQQdjr5rWKqHFiKWb/fcOwB+CzBT+qbWjvj+DVwRskpIg==", + "dev": true, + "requires": { + "cross-spawn": "^7.0.0", + "signal-exit": "^4.0.1" + }, + "dependencies": { + "signal-exit": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz", + "integrity": "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==", + "dev": true + } + } + }, + "form-data": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.0.tgz", + "integrity": "sha512-ETEklSGi5t0QMZuiXoA/Q6vcnxcLQP5vdugSpuAyi6SVGi2clPPp+xgEhuMaHC+zGgn31Kd235W35f7Hykkaww==", + "requires": { + "asynckit": "^0.4.0", + "combined-stream": "^1.0.8", + "mime-types": "^2.1.12" + } + }, + "forwarded": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz", + "integrity": "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==" + }, + "fresh": { + "version": "0.5.2", + "resolved": "https://registry.npmjs.org/fresh/-/fresh-0.5.2.tgz", + "integrity": "sha512-zJ2mQYM18rEFOudeV4GShTGIQ7RbzA7ozbU9I/XBpm7kqgMywgmylMwXHxZJmkVoYkna9d2pVXVXPdYTP9ej8Q==" + }, + "fs.realpath": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", + "integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==", + "dev": true + }, + "fsevents": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz", + "integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==", + "dev": true, + "optional": true + }, + "function-bind": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.1.tgz", + "integrity": "sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A==" + }, + "function.prototype.name": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/function.prototype.name/-/function.prototype.name-1.1.5.tgz", + "integrity": "sha512-uN7m/BzVKQnCUF/iW8jYea67v++2u7m5UgENbHRtdDVclOUP+FMPlCNdmk0h/ysGyo2tavMJEDqJAkJdRa1vMA==", + "dev": true, + "requires": { + "call-bind": "^1.0.2", + "define-properties": "^1.1.3", + "es-abstract": "^1.19.0", + "functions-have-names": "^1.2.2" + } + }, + "functional-red-black-tree": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/functional-red-black-tree/-/functional-red-black-tree-1.0.1.tgz", + "integrity": "sha512-dsKNQNdj6xA3T+QlADDA7mOSlX0qiMINjn0cgr+eGHGsbSHzTabcIogz2+p/iqP1Xs6EP/sS2SbqH+brGTbq0g==", + "dev": true + }, + "functions-have-names": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/functions-have-names/-/functions-have-names-1.2.3.tgz", + "integrity": "sha512-xckBUXyTIqT97tq2x2AMb+g163b5JFysYk0x4qxNFwbfQkmNZoiRHb6sPzI9/QV33WeuvVYBUIiD4NzNIyqaRQ==", + "dev": true + }, + "gensync": { + "version": "1.0.0-beta.2", + "resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz", + "integrity": "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==", + "dev": true + }, + "get-caller-file": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz", + "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==", + "dev": true + }, + "get-intrinsic": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.1.3.tgz", + "integrity": "sha512-QJVz1Tj7MS099PevUG5jvnt9tSkXN8K14dxQlikJuPt4uD9hHAHjLyLBiLR5zELelBdD9QNRAXZzsJx0WaDL9A==", + "requires": { + "function-bind": "^1.1.1", + "has": "^1.0.3", + "has-symbols": "^1.0.3" + } + }, + "get-package-type": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/get-package-type/-/get-package-type-0.1.0.tgz", + "integrity": "sha512-pjzuKtY64GYfWizNAJ0fr9VqttZkNiK2iS430LtIHzjBEr6bX8Am2zm4sW4Ro5wjWW5cAlRL1qAMTcXbjNAO2Q==", + "dev": true + }, + "get-stream": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-6.0.1.tgz", + "integrity": "sha512-ts6Wi+2j3jQjqi70w5AlN8DFnkSwC+MqmxEzdEALB2qXZYV3X/b1CTfgPLGJNMeAWxdPfU8FO1ms3NUfaHCPYg==", + "dev": true + }, + "get-symbol-description": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/get-symbol-description/-/get-symbol-description-1.0.0.tgz", + "integrity": "sha512-2EmdH1YvIQiZpltCNgkuiUnyukzxM/R6NDJX31Ke3BG1Nq5b0S2PhX59UKi9vZpPDQVdqn+1IcaAwnzTT5vCjw==", + "dev": true, + "requires": { + "call-bind": "^1.0.2", + "get-intrinsic": "^1.1.1" + } + }, + "glob": { + "version": "7.2.3", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", + "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", + "dev": true, + "requires": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.1.1", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + } + }, + "glob-parent": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", + "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", + "dev": true, + "requires": { + "is-glob": "^4.0.1" + } + }, + "globals": { + "version": "11.12.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-11.12.0.tgz", + "integrity": "sha512-WOBp/EEGUiIsJSp7wcv/y6MO+lV9UoncWqxuFfm8eBwzWNgyfBd6Gz+IeKQ9jCmyhoH99g15M3T+QaVHFjizVA==", + "dev": true + }, + "globby": { + "version": "11.1.0", + "resolved": "https://registry.npmjs.org/globby/-/globby-11.1.0.tgz", + "integrity": "sha512-jhIXaOzy1sb8IyocaruWSn1TjmnBVs8Ayhcy83rmxNJ8q2uWKCAj3CnJY+KpGSXCueAPc0i05kVvVKtP1t9S3g==", + "dev": true, + "requires": { + "array-union": "^2.1.0", + "dir-glob": "^3.0.1", + "fast-glob": "^3.2.9", + "ignore": "^5.2.0", + "merge2": "^1.4.1", + "slash": "^3.0.0" + } + }, + "graceful-fs": { + "version": "4.2.10", + "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.10.tgz", + "integrity": "sha512-9ByhssR2fPVsNZj478qUUbKfmL0+t5BDVyjShtyZZLiK7ZDAArFFfopyOTj0M05wE2tJPisA4iTnnXl2YoPvOA==", + "dev": true + }, + "graphemer": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/graphemer/-/graphemer-1.4.0.tgz", + "integrity": "sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag==", + "dev": true + }, + "has": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/has/-/has-1.0.3.tgz", + "integrity": "sha512-f2dvO0VU6Oej7RkWJGrehjbzMAjFp5/VKPp5tTpWIV4JHHZK1/BxbFRtf/siA2SWTe09caDmVtYYzWEIbBS4zw==", + "requires": { + "function-bind": "^1.1.1" + } + }, + "has-bigints": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/has-bigints/-/has-bigints-1.0.2.tgz", + "integrity": "sha512-tSvCKtBr9lkF0Ex0aQiP9N+OpV4zi2r/Nee5VkRDbaqv35RLYMzbwQfFSZZH0kR+Rd6302UJZ2p/bJCEoR3VoQ==", + "dev": true + }, + "has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true + }, + "has-property-descriptors": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/has-property-descriptors/-/has-property-descriptors-1.0.0.tgz", + "integrity": "sha512-62DVLZGoiEBDHQyqG4w9xCuZ7eJEwNmJRWw2VY84Oedb7WFcA27fiEVe8oUQx9hAUJ4ekurquucTGwsyO1XGdQ==", + "dev": true, + "requires": { + "get-intrinsic": "^1.1.1" + } + }, + "has-symbols": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.0.3.tgz", + "integrity": "sha512-l3LCuF6MgDNwTDKkdYGEihYjt5pRPbEg46rtlmnSPlUbgmB8LOIrKJbYYFBSbnPaJexMKtiPO8hmeRjRz2Td+A==" + }, + "has-tostringtag": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.0.tgz", + "integrity": "sha512-kFjcSNhnlGV1kyoGk7OXKSawH5JOb/LzUc5w9B02hOTO0dfFRjbHQKvg1d6cf3HbeUmtU9VbbV3qzZ2Teh97WQ==", + "dev": true, + "requires": { + "has-symbols": "^1.0.2" + } + }, + "html-escaper": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/html-escaper/-/html-escaper-2.0.2.tgz", + "integrity": "sha512-H2iMtd0I4Mt5eYiapRdIDjp+XzelXQ0tFE4JS7YFwFevXXMmOp9myNrUvCg0D6ws8iqkRPBfKHgbwig1SmlLfg==", + "dev": true + }, + "http-errors": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.0.tgz", + "integrity": "sha512-FtwrG/euBzaEjYeRqOgly7G0qviiXoJWnvEH2Z1plBdXgbyjv34pHTSb9zoeHMyDy33+DWy5Wt9Wo+TURtOYSQ==", + "requires": { + "depd": "2.0.0", + "inherits": "2.0.4", + "setprototypeof": "1.2.0", + "statuses": "2.0.1", + "toidentifier": "1.0.1" + } + }, + "human-signals": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/human-signals/-/human-signals-2.1.0.tgz", + "integrity": "sha512-B4FFZ6q/T2jhhksgkbEW3HBvWIfDW85snkQgawt07S7J5QXTk6BkNV+0yAeZrM5QpMAdYlocGoljn0sJ/WQkFw==", + "dev": true + }, + "iconv-lite": { + "version": "0.4.24", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz", + "integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==", + "requires": { + "safer-buffer": ">= 2.1.2 < 3" + } + }, + "ignore": { + "version": "5.2.4", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.2.4.tgz", + "integrity": "sha512-MAb38BcSbH0eHNBxn7ql2NH/kX33OkB3lZ1BNdh7ENeRChHTYsTvWrMubiIAMNS2llXEEgZ1MUOBtXChP3kaFQ==", + "dev": true + }, + "import-fresh": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.0.tgz", + "integrity": "sha512-veYYhQa+D1QBKznvhUHxb8faxlrwUnxseDAbAp457E0wLNio2bOSKnjYDhMj+YiAq61xrMGhQk9iXVk5FzgQMw==", + "dev": true, + "requires": { + "parent-module": "^1.0.0", + "resolve-from": "^4.0.0" + }, + "dependencies": { + "resolve-from": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz", + "integrity": "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==", + "dev": true + } + } + }, + "import-local": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/import-local/-/import-local-3.1.0.tgz", + "integrity": "sha512-ASB07uLtnDs1o6EHjKpX34BKYDSqnFerfTOJL2HvMqF70LnxpjkzDB8J44oT9pu4AMPkQwf8jl6szgvNd2tRIg==", + "dev": true, + "requires": { + "pkg-dir": "^4.2.0", + "resolve-cwd": "^3.0.0" + } + }, + "imurmurhash": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz", + "integrity": "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==", + "dev": true + }, + "inflight": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", + "integrity": "sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==", + "dev": true, + "requires": { + "once": "^1.3.0", + "wrappy": "1" + } + }, + "inherits": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==" + }, + "inquirer": { + "version": "7.3.3", + "resolved": "https://registry.npmjs.org/inquirer/-/inquirer-7.3.3.tgz", + "integrity": "sha512-JG3eIAj5V9CwcGvuOmoo6LB9kbAYT8HXffUl6memuszlwDC/qvFAJw49XJ5NROSFNPxp3iQg1GqkFhaY/CR0IA==", + "dev": true, + "requires": { + "ansi-escapes": "^4.2.1", + "chalk": "^4.1.0", + "cli-cursor": "^3.1.0", + "cli-width": "^3.0.0", + "external-editor": "^3.0.3", + "figures": "^3.0.0", + "lodash": "^4.17.19", + "mute-stream": "0.0.8", + "run-async": "^2.4.0", + "rxjs": "^6.6.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0", + "through": "^2.3.6" + } + }, + "internal-slot": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/internal-slot/-/internal-slot-1.0.3.tgz", + "integrity": "sha512-O0DB1JC/sPyZl7cIo78n5dR7eUSwwpYPiXRhTzNxZVAMUuB8vlnRFyLxdrVToks6XPLVnFfbzaVd5WLjhgg+vA==", + "dev": true, + "requires": { + "get-intrinsic": "^1.1.0", + "has": "^1.0.3", + "side-channel": "^1.0.4" + } + }, + "ipaddr.js": { + "version": "1.9.1", + "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz", + "integrity": "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==" + }, + "is-arrayish": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.2.1.tgz", + "integrity": "sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg==", + "dev": true + }, + "is-bigint": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/is-bigint/-/is-bigint-1.0.4.tgz", + "integrity": "sha512-zB9CruMamjym81i2JZ3UMn54PKGsQzsJeo6xvN3HJJ4CAsQNB6iRutp2To77OfCNuoxspsIhzaPoO1zyCEhFOg==", + "dev": true, + "requires": { + "has-bigints": "^1.0.1" + } + }, + "is-binary-path": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz", + "integrity": "sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==", + "dev": true, + "requires": { + "binary-extensions": "^2.0.0" + } + }, + "is-boolean-object": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/is-boolean-object/-/is-boolean-object-1.1.2.tgz", + "integrity": "sha512-gDYaKHJmnj4aWxyj6YHyXVpdQawtVLHU5cb+eztPGczf6cjuTdwve5ZIEfgXqH4e57An1D1AKf8CZ3kYrQRqYA==", + "dev": true, + "requires": { + "call-bind": "^1.0.2", + "has-tostringtag": "^1.0.0" + } + }, + "is-callable": { + "version": "1.2.7", + "resolved": "https://registry.npmjs.org/is-callable/-/is-callable-1.2.7.tgz", + "integrity": "sha512-1BC0BVFhS/p0qtw6enp8e+8OD0UrK0oFLztSjNzhcKA3WDuJxxAPXzPuPtKkjEY9UUoEWlX/8fgKeu2S8i9JTA==", + "dev": true + }, + "is-core-module": { + "version": "2.11.0", + "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.11.0.tgz", + "integrity": "sha512-RRjxlvLDkD1YJwDbroBHMb+cukurkDWNyHx7D3oNB5x9rb5ogcksMC5wHCadcXoo67gVr/+3GFySh3134zi6rw==", + "dev": true, + "requires": { + "has": "^1.0.3" + } + }, + "is-date-object": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/is-date-object/-/is-date-object-1.0.5.tgz", + "integrity": "sha512-9YQaSxsAiSwcvS33MBk3wTCVnWK+HhF8VZR2jRxehM16QcVOdHqPn4VPHmRK4lSr38n9JriurInLcP90xsYNfQ==", + "dev": true, + "requires": { + "has-tostringtag": "^1.0.0" + } + }, + "is-docker": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-docker/-/is-docker-3.0.0.tgz", + "integrity": "sha512-eljcgEDlEns/7AXFosB5K/2nCM4P7FQPkGc/DWLy5rmFEWvZayGrik1d9/QIY5nJ4f9YsVvBkA6kJpHn9rISdQ==", + "dev": true + }, + "is-extglob": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", + "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", + "dev": true + }, + "is-fullwidth-code-point": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", + "dev": true + }, + "is-generator-fn": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/is-generator-fn/-/is-generator-fn-2.1.0.tgz", + "integrity": "sha512-cTIB4yPYL/Grw0EaSzASzg6bBy9gqCofvWN8okThAYIxKJZC+udlRAmGbM0XLeniEJSs8uEgHPGuHSe1XsOLSQ==", + "dev": true + }, + "is-glob": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", + "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", + "dev": true, + "requires": { + "is-extglob": "^2.1.1" + } + }, + "is-inside-container": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-inside-container/-/is-inside-container-1.0.0.tgz", + "integrity": "sha512-KIYLCCJghfHZxqjYBE7rEy0OBuTd5xCHS7tHVgvCLkx7StIoaxwNW3hCALgEUjFfeRk+MG/Qxmp/vtETEF3tRA==", + "dev": true, + "requires": { + "is-docker": "^3.0.0" + } + }, + "is-negative-zero": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/is-negative-zero/-/is-negative-zero-2.0.2.tgz", + "integrity": "sha512-dqJvarLawXsFbNDeJW7zAz8ItJ9cd28YufuuFzh0G8pNHjJMnY08Dv7sYX2uF5UpQOwieAeOExEYAWWfu7ZZUA==", + "dev": true + }, + "is-number": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", + "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", + "dev": true + }, + "is-number-object": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/is-number-object/-/is-number-object-1.0.7.tgz", + "integrity": "sha512-k1U0IRzLMo7ZlYIfzRu23Oh6MiIFasgpb9X76eqfFZAqwH44UI4KTBvBYIZ1dSL9ZzChTB9ShHfLkR4pdW5krQ==", + "dev": true, + "requires": { + "has-tostringtag": "^1.0.0" + } + }, + "is-path-inside": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/is-path-inside/-/is-path-inside-3.0.3.tgz", + "integrity": "sha512-Fd4gABb+ycGAmKou8eMftCupSir5lRxqf4aD/vd0cD2qc4HL07OjCeuHMr8Ro4CoMaeCKDB0/ECBOVWjTwUvPQ==", + "dev": true + }, + "is-regex": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/is-regex/-/is-regex-1.1.4.tgz", + "integrity": "sha512-kvRdxDsxZjhzUX07ZnLydzS1TU/TJlTUHHY4YLL87e37oUA49DfkLqgy+VjFocowy29cKvcSiu+kIv728jTTVg==", + "dev": true, + "requires": { + "call-bind": "^1.0.2", + "has-tostringtag": "^1.0.0" + } + }, + "is-shared-array-buffer": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/is-shared-array-buffer/-/is-shared-array-buffer-1.0.2.tgz", + "integrity": "sha512-sqN2UDu1/0y6uvXyStCOzyhAjCSlHceFoMKJW8W9EU9cvic/QdsZ0kEU93HEy3IUEFZIiH/3w+AH/UQbPHNdhA==", + "dev": true, + "requires": { + "call-bind": "^1.0.2" + } + }, + "is-stream": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-2.0.1.tgz", + "integrity": "sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg==" + }, + "is-string": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/is-string/-/is-string-1.0.7.tgz", + "integrity": "sha512-tE2UXzivje6ofPW7l23cjDOMa09gb7xlAqG6jG5ej6uPV32TlWP3NKPigtaGeHNu9fohccRYvIiZMfOOnOYUtg==", + "dev": true, + "requires": { + "has-tostringtag": "^1.0.0" + } + }, + "is-symbol": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/is-symbol/-/is-symbol-1.0.4.tgz", + "integrity": "sha512-C/CPBqKWnvdcxqIARxyOh4v1UUEOCHpgDa0WYgpKDFMszcrPcffg5uhwSgPCLD2WWxmq6isisz87tzT01tuGhg==", + "dev": true, + "requires": { + "has-symbols": "^1.0.2" + } + }, + "is-weakref": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/is-weakref/-/is-weakref-1.0.2.tgz", + "integrity": "sha512-qctsuLZmIQ0+vSSMfoVvyFe2+GSEvnmZ2ezTup1SBse9+twCCeial6EEi3Nc2KFcf6+qz2FBPnjXsk8xhKSaPQ==", + "dev": true, + "requires": { + "call-bind": "^1.0.2" + } + }, + "is-wsl": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/is-wsl/-/is-wsl-2.2.0.tgz", + "integrity": "sha512-fKzAra0rGJUUBwGBgNkHZuToZcn+TtXHpeCgmkMJMMYx1sQDYaCSyjJBSCa2nH1DGm7s3n1oBnohoVTBaN7Lww==", + "dev": true, + "requires": { + "is-docker": "^2.0.0" + }, + "dependencies": { + "is-docker": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/is-docker/-/is-docker-2.2.1.tgz", + "integrity": "sha512-F+i2BKsFrH66iaUFc0woD8sLy8getkwTwtOBjvs56Cx4CgJDeKQeqfz8wAYiSb8JOprWhHH5p77PbmYCvvUuXQ==", + "dev": true + } + } + }, + "isexe": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", + "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", + "dev": true + }, + "istanbul-lib-coverage": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/istanbul-lib-coverage/-/istanbul-lib-coverage-3.2.0.tgz", + "integrity": "sha512-eOeJ5BHCmHYvQK7xt9GkdHuzuCGS1Y6g9Gvnx3Ym33fz/HpLRYxiS0wHNr+m/MBC8B647Xt608vCDEvhl9c6Mw==", + "dev": true + }, + "istanbul-lib-instrument": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/istanbul-lib-instrument/-/istanbul-lib-instrument-5.2.1.tgz", + "integrity": "sha512-pzqtp31nLv/XFOzXGuvhCb8qhjmTVo5vjVk19XE4CRlSWz0KoeJ3bw9XsA7nOp9YBf4qHjwBxkDzKcME/J29Yg==", + "dev": true, + "requires": { + "@babel/core": "^7.12.3", + "@babel/parser": "^7.14.7", + "@istanbuljs/schema": "^0.1.2", + "istanbul-lib-coverage": "^3.2.0", + "semver": "^6.3.0" + } + }, + "istanbul-lib-report": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/istanbul-lib-report/-/istanbul-lib-report-3.0.1.tgz", + "integrity": "sha512-GCfE1mtsHGOELCU8e/Z7YWzpmybrx/+dSTfLrvY8qRmaY6zXTKWn6WQIjaAFw069icm6GVMNkgu0NzI4iPZUNw==", + "dev": true, + "requires": { + "istanbul-lib-coverage": "^3.0.0", + "make-dir": "^4.0.0", + "supports-color": "^7.1.0" + } + }, + "istanbul-lib-source-maps": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/istanbul-lib-source-maps/-/istanbul-lib-source-maps-4.0.1.tgz", + "integrity": "sha512-n3s8EwkdFIJCG3BPKBYvskgXGoy88ARzvegkitk60NxRdwltLOTaH7CUiMRXvwYorl0Q712iEjcWB+fK/MrWVw==", + "dev": true, + "requires": { + "debug": "^4.1.1", + "istanbul-lib-coverage": "^3.0.0", + "source-map": "^0.6.1" + }, + "dependencies": { + "debug": { + "version": "4.3.4", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", + "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==", + "dev": true, + "requires": { + "ms": "2.1.2" + } + }, + "ms": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", + "dev": true + } + } + }, + "istanbul-reports": { + "version": "3.1.6", + "resolved": "https://registry.npmjs.org/istanbul-reports/-/istanbul-reports-3.1.6.tgz", + "integrity": "sha512-TLgnMkKg3iTDsQ9PbPTdpfAK2DzjF9mqUG7RMgcQl8oFjad8ob4laGxv5XV5U9MAfx8D6tSJiUyuAwzLicaxlg==", + "dev": true, + "requires": { + "html-escaper": "^2.0.0", + "istanbul-lib-report": "^3.0.0" + } + }, + "jackspeak": { + "version": "2.3.6", + "resolved": "https://registry.npmjs.org/jackspeak/-/jackspeak-2.3.6.tgz", + "integrity": "sha512-N3yCS/NegsOBokc8GAdM8UcmfsKiSS8cipheD/nivzr700H+nsMOxJjQnvwOcRYVuFkdH0wGUvW2WbXGmrZGbQ==", + "dev": true, + "requires": { + "@isaacs/cliui": "^8.0.2", + "@pkgjs/parseargs": "^0.11.0" + } + }, + "jest": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest/-/jest-29.7.0.tgz", + "integrity": "sha512-NIy3oAFp9shda19hy4HK0HRTWKtPJmGdnvywu01nOqNC2vZg+Z+fvJDxpMQA88eb2I9EcafcdjYgsDthnYTvGw==", + "dev": true, + "requires": { + "@jest/core": "^29.7.0", + "@jest/types": "^29.6.3", + "import-local": "^3.0.2", + "jest-cli": "^29.7.0" + } + }, + "jest-changed-files": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-changed-files/-/jest-changed-files-29.7.0.tgz", + "integrity": "sha512-fEArFiwf1BpQ+4bXSprcDc3/x4HSzL4al2tozwVpDFpsxALjLYdyiIK4e5Vz66GQJIbXJ82+35PtysofptNX2w==", + "dev": true, + "requires": { + "execa": "^5.0.0", + "jest-util": "^29.7.0", + "p-limit": "^3.1.0" + } + }, + "jest-circus": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-circus/-/jest-circus-29.7.0.tgz", + "integrity": "sha512-3E1nCMgipcTkCocFwM90XXQab9bS+GMsjdpmPrlelaxwD93Ad8iVEjX/vvHPdLPnFf+L40u+5+iutRdA1N9myw==", + "dev": true, + "requires": { + "@jest/environment": "^29.7.0", + "@jest/expect": "^29.7.0", + "@jest/test-result": "^29.7.0", + "@jest/types": "^29.6.3", + "@types/node": "*", + "chalk": "^4.0.0", + "co": "^4.6.0", + "dedent": "^1.0.0", + "is-generator-fn": "^2.0.0", + "jest-each": "^29.7.0", + "jest-matcher-utils": "^29.7.0", + "jest-message-util": "^29.7.0", + "jest-runtime": "^29.7.0", + "jest-snapshot": "^29.7.0", + "jest-util": "^29.7.0", + "p-limit": "^3.1.0", + "pretty-format": "^29.7.0", + "pure-rand": "^6.0.0", + "slash": "^3.0.0", + "stack-utils": "^2.0.3" + } + }, + "jest-cli": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-cli/-/jest-cli-29.7.0.tgz", + "integrity": "sha512-OVVobw2IubN/GSYsxETi+gOe7Ka59EFMR/twOU3Jb2GnKKeMGJB5SGUUrEz3SFVmJASUdZUzy83sLNNQ2gZslg==", + "dev": true, + "requires": { + "@jest/core": "^29.7.0", + "@jest/test-result": "^29.7.0", + "@jest/types": "^29.6.3", + "chalk": "^4.0.0", + "create-jest": "^29.7.0", + "exit": "^0.1.2", + "import-local": "^3.0.2", + "jest-config": "^29.7.0", + "jest-util": "^29.7.0", + "jest-validate": "^29.7.0", + "yargs": "^17.3.1" + } + }, + "jest-config": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-config/-/jest-config-29.7.0.tgz", + "integrity": "sha512-uXbpfeQ7R6TZBqI3/TxCU4q4ttk3u0PJeC+E0zbfSoSjq6bJ7buBPxzQPL0ifrkY4DNu4JUdk0ImlBUYi840eQ==", + "dev": true, + "requires": { + "@babel/core": "^7.11.6", + "@jest/test-sequencer": "^29.7.0", + "@jest/types": "^29.6.3", + "babel-jest": "^29.7.0", + "chalk": "^4.0.0", + "ci-info": "^3.2.0", + "deepmerge": "^4.2.2", + "glob": "^7.1.3", + "graceful-fs": "^4.2.9", + "jest-circus": "^29.7.0", + "jest-environment-node": "^29.7.0", + "jest-get-type": "^29.6.3", + "jest-regex-util": "^29.6.3", + "jest-resolve": "^29.7.0", + "jest-runner": "^29.7.0", + "jest-util": "^29.7.0", + "jest-validate": "^29.7.0", + "micromatch": "^4.0.4", + "parse-json": "^5.2.0", + "pretty-format": "^29.7.0", + "slash": "^3.0.0", + "strip-json-comments": "^3.1.1" + } + }, + "jest-diff": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-diff/-/jest-diff-29.7.0.tgz", + "integrity": "sha512-LMIgiIrhigmPrs03JHpxUh2yISK3vLFPkAodPeo0+BuF7wA2FoQbkEg1u8gBYBThncu7e1oEDUfIXVuTqLRUjw==", + "dev": true, + "requires": { + "chalk": "^4.0.0", + "diff-sequences": "^29.6.3", + "jest-get-type": "^29.6.3", + "pretty-format": "^29.7.0" + } + }, + "jest-docblock": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-docblock/-/jest-docblock-29.7.0.tgz", + "integrity": "sha512-q617Auw3A612guyaFgsbFeYpNP5t2aoUNLwBUbc/0kD1R4t9ixDbyFTHd1nok4epoVFpr7PmeWHrhvuV3XaJ4g==", + "dev": true, + "requires": { + "detect-newline": "^3.0.0" + } + }, + "jest-each": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-each/-/jest-each-29.7.0.tgz", + "integrity": "sha512-gns+Er14+ZrEoC5fhOfYCY1LOHHr0TI+rQUHZS8Ttw2l7gl+80eHc/gFf2Ktkw0+SIACDTeWvpFcv3B04VembQ==", + "dev": true, + "requires": { + "@jest/types": "^29.6.3", + "chalk": "^4.0.0", + "jest-get-type": "^29.6.3", + "jest-util": "^29.7.0", + "pretty-format": "^29.7.0" + } + }, + "jest-environment-node": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-environment-node/-/jest-environment-node-29.7.0.tgz", + "integrity": "sha512-DOSwCRqXirTOyheM+4d5YZOrWcdu0LNZ87ewUoywbcb2XR4wKgqiG8vNeYwhjFMbEkfju7wx2GYH0P2gevGvFw==", + "dev": true, + "requires": { + "@jest/environment": "^29.7.0", + "@jest/fake-timers": "^29.7.0", + "@jest/types": "^29.6.3", + "@types/node": "*", + "jest-mock": "^29.7.0", + "jest-util": "^29.7.0" + } + }, + "jest-get-type": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/jest-get-type/-/jest-get-type-29.6.3.tgz", + "integrity": "sha512-zrteXnqYxfQh7l5FHyL38jL39di8H8rHoecLH3JNxH3BwOrBsNeabdap5e0I23lD4HHI8W5VFBZqG4Eaq5LNcw==", + "dev": true + }, + "jest-haste-map": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-haste-map/-/jest-haste-map-29.7.0.tgz", + "integrity": "sha512-fP8u2pyfqx0K1rGn1R9pyE0/KTn+G7PxktWidOBTqFPLYX0b9ksaMFkhK5vrS3DVun09pckLdlx90QthlW7AmA==", + "dev": true, + "requires": { + "@jest/types": "^29.6.3", + "@types/graceful-fs": "^4.1.3", + "@types/node": "*", + "anymatch": "^3.0.3", + "fb-watchman": "^2.0.0", + "fsevents": "^2.3.2", + "graceful-fs": "^4.2.9", + "jest-regex-util": "^29.6.3", + "jest-util": "^29.7.0", + "jest-worker": "^29.7.0", + "micromatch": "^4.0.4", + "walker": "^1.0.8" + } + }, + "jest-leak-detector": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-leak-detector/-/jest-leak-detector-29.7.0.tgz", + "integrity": "sha512-kYA8IJcSYtST2BY9I+SMC32nDpBT3J2NvWJx8+JCuCdl/CR1I4EKUJROiP8XtCcxqgTTBGJNdbB1A8XRKbTetw==", + "dev": true, + "requires": { + "jest-get-type": "^29.6.3", + "pretty-format": "^29.7.0" + } + }, + "jest-matcher-utils": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-matcher-utils/-/jest-matcher-utils-29.7.0.tgz", + "integrity": "sha512-sBkD+Xi9DtcChsI3L3u0+N0opgPYnCRPtGcQYrgXmR+hmt/fYfWAL0xRXYU8eWOdfuLgBe0YCW3AFtnRLagq/g==", + "dev": true, + "requires": { + "chalk": "^4.0.0", + "jest-diff": "^29.7.0", + "jest-get-type": "^29.6.3", + "pretty-format": "^29.7.0" + } + }, + "jest-message-util": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-message-util/-/jest-message-util-29.7.0.tgz", + "integrity": "sha512-GBEV4GRADeP+qtB2+6u61stea8mGcOT4mCtrYISZwfu9/ISHFJ/5zOMXYbpBE9RsS5+Gb63DW4FgmnKJ79Kf6w==", + "dev": true, + "requires": { + "@babel/code-frame": "^7.12.13", + "@jest/types": "^29.6.3", + "@types/stack-utils": "^2.0.0", + "chalk": "^4.0.0", + "graceful-fs": "^4.2.9", + "micromatch": "^4.0.4", + "pretty-format": "^29.7.0", + "slash": "^3.0.0", + "stack-utils": "^2.0.3" + } + }, + "jest-mock": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-mock/-/jest-mock-29.7.0.tgz", + "integrity": "sha512-ITOMZn+UkYS4ZFh83xYAOzWStloNzJFO2s8DWrE4lhtGD+AorgnbkiKERe4wQVBydIGPx059g6riW5Btp6Llnw==", + "dev": true, + "requires": { + "@jest/types": "^29.6.3", + "@types/node": "*", + "jest-util": "^29.7.0" + } + }, + "jest-pnp-resolver": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/jest-pnp-resolver/-/jest-pnp-resolver-1.2.3.tgz", + "integrity": "sha512-+3NpwQEnRoIBtx4fyhblQDPgJI0H1IEIkX7ShLUjPGA7TtUTvI1oiKi3SR4oBR0hQhQR80l4WAe5RrXBwWMA8w==", + "dev": true + }, + "jest-regex-util": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/jest-regex-util/-/jest-regex-util-29.6.3.tgz", + "integrity": "sha512-KJJBsRCyyLNWCNBOvZyRDnAIfUiRJ8v+hOBQYGn8gDyF3UegwiP4gwRR3/SDa42g1YbVycTidUF3rKjyLFDWbg==", + "dev": true + }, + "jest-resolve": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-resolve/-/jest-resolve-29.7.0.tgz", + "integrity": "sha512-IOVhZSrg+UvVAshDSDtHyFCCBUl/Q3AAJv8iZ6ZjnZ74xzvwuzLXid9IIIPgTnY62SJjfuupMKZsZQRsCvxEgA==", + "dev": true, + "requires": { + "chalk": "^4.0.0", + "graceful-fs": "^4.2.9", + "jest-haste-map": "^29.7.0", + "jest-pnp-resolver": "^1.2.2", + "jest-util": "^29.7.0", + "jest-validate": "^29.7.0", + "resolve": "^1.20.0", + "resolve.exports": "^2.0.0", + "slash": "^3.0.0" + } + }, + "jest-resolve-dependencies": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-resolve-dependencies/-/jest-resolve-dependencies-29.7.0.tgz", + "integrity": "sha512-un0zD/6qxJ+S0et7WxeI3H5XSe9lTBBR7bOHCHXkKR6luG5mwDDlIzVQ0V5cZCuoTgEdcdwzTghYkTWfubi+nA==", + "dev": true, + "requires": { + "jest-regex-util": "^29.6.3", + "jest-snapshot": "^29.7.0" + } + }, + "jest-runner": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-runner/-/jest-runner-29.7.0.tgz", + "integrity": "sha512-fsc4N6cPCAahybGBfTRcq5wFR6fpLznMg47sY5aDpsoejOcVYFb07AHuSnR0liMcPTgBsA3ZJL6kFOjPdoNipQ==", + "dev": true, + "requires": { + "@jest/console": "^29.7.0", + "@jest/environment": "^29.7.0", + "@jest/test-result": "^29.7.0", + "@jest/transform": "^29.7.0", + "@jest/types": "^29.6.3", + "@types/node": "*", + "chalk": "^4.0.0", + "emittery": "^0.13.1", + "graceful-fs": "^4.2.9", + "jest-docblock": "^29.7.0", + "jest-environment-node": "^29.7.0", + "jest-haste-map": "^29.7.0", + "jest-leak-detector": "^29.7.0", + "jest-message-util": "^29.7.0", + "jest-resolve": "^29.7.0", + "jest-runtime": "^29.7.0", + "jest-util": "^29.7.0", + "jest-watcher": "^29.7.0", + "jest-worker": "^29.7.0", + "p-limit": "^3.1.0", + "source-map-support": "0.5.13" + } + }, + "jest-runtime": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-runtime/-/jest-runtime-29.7.0.tgz", + "integrity": "sha512-gUnLjgwdGqW7B4LvOIkbKs9WGbn+QLqRQQ9juC6HndeDiezIwhDP+mhMwHWCEcfQ5RUXa6OPnFF8BJh5xegwwQ==", + "dev": true, + "requires": { + "@jest/environment": "^29.7.0", + "@jest/fake-timers": "^29.7.0", + "@jest/globals": "^29.7.0", + "@jest/source-map": "^29.6.3", + "@jest/test-result": "^29.7.0", + "@jest/transform": "^29.7.0", + "@jest/types": "^29.6.3", + "@types/node": "*", + "chalk": "^4.0.0", + "cjs-module-lexer": "^1.0.0", + "collect-v8-coverage": "^1.0.0", + "glob": "^7.1.3", + "graceful-fs": "^4.2.9", + "jest-haste-map": "^29.7.0", + "jest-message-util": "^29.7.0", + "jest-mock": "^29.7.0", + "jest-regex-util": "^29.6.3", + "jest-resolve": "^29.7.0", + "jest-snapshot": "^29.7.0", + "jest-util": "^29.7.0", + "slash": "^3.0.0", + "strip-bom": "^4.0.0" + } + }, + "jest-snapshot": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-snapshot/-/jest-snapshot-29.7.0.tgz", + "integrity": "sha512-Rm0BMWtxBcioHr1/OX5YCP8Uov4riHvKPknOGs804Zg9JGZgmIBkbtlxJC/7Z4msKYVbIJtfU+tKb8xlYNfdkw==", + "dev": true, + "requires": { + "@babel/core": "^7.11.6", + "@babel/generator": "^7.7.2", + "@babel/plugin-syntax-jsx": "^7.7.2", + "@babel/plugin-syntax-typescript": "^7.7.2", + "@babel/types": "^7.3.3", + "@jest/expect-utils": "^29.7.0", + "@jest/transform": "^29.7.0", + "@jest/types": "^29.6.3", + "babel-preset-current-node-syntax": "^1.0.0", + "chalk": "^4.0.0", + "expect": "^29.7.0", + "graceful-fs": "^4.2.9", + "jest-diff": "^29.7.0", + "jest-get-type": "^29.6.3", + "jest-matcher-utils": "^29.7.0", + "jest-message-util": "^29.7.0", + "jest-util": "^29.7.0", + "natural-compare": "^1.4.0", + "pretty-format": "^29.7.0", + "semver": "^7.5.3" + }, + "dependencies": { + "semver": { + "version": "7.5.4", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.5.4.tgz", + "integrity": "sha512-1bCSESV6Pv+i21Hvpxp3Dx+pSD8lIPt8uVjRrxAUt/nbswYc+tK6Y2btiULjd4+fnq15PX+nqQDC7Oft7WkwcA==", + "dev": true, + "requires": { + "lru-cache": "^6.0.0" + } + } + } + }, + "jest-util": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-util/-/jest-util-29.7.0.tgz", + "integrity": "sha512-z6EbKajIpqGKU56y5KBUgy1dt1ihhQJgWzUlZHArA/+X2ad7Cb5iF+AK1EWVL/Bo7Rz9uurpqw6SiBCefUbCGA==", + "dev": true, + "requires": { + "@jest/types": "^29.6.3", + "@types/node": "*", + "chalk": "^4.0.0", + "ci-info": "^3.2.0", + "graceful-fs": "^4.2.9", + "picomatch": "^2.2.3" + } + }, + "jest-validate": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-validate/-/jest-validate-29.7.0.tgz", + "integrity": "sha512-ZB7wHqaRGVw/9hST/OuFUReG7M8vKeq0/J2egIGLdvjHCmYqGARhzXmtgi+gVeZ5uXFF219aOc3Ls2yLg27tkw==", + "dev": true, + "requires": { + "@jest/types": "^29.6.3", + "camelcase": "^6.2.0", + "chalk": "^4.0.0", + "jest-get-type": "^29.6.3", + "leven": "^3.1.0", + "pretty-format": "^29.7.0" + }, + "dependencies": { + "camelcase": { + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-6.3.0.tgz", + "integrity": "sha512-Gmy6FhYlCY7uOElZUSbxo2UCDH8owEk996gkbrpsgGtrJLM3J7jGxl9Ic7Qwwj4ivOE5AWZWRMecDdF7hqGjFA==", + "dev": true + } + } + }, + "jest-watcher": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-watcher/-/jest-watcher-29.7.0.tgz", + "integrity": "sha512-49Fg7WXkU3Vl2h6LbLtMQ/HyB6rXSIX7SqvBLQmssRBGN9I0PNvPmAmCWSOY6SOvrjhI/F7/bGAv9RtnsPA03g==", + "dev": true, + "requires": { + "@jest/test-result": "^29.7.0", + "@jest/types": "^29.6.3", + "@types/node": "*", + "ansi-escapes": "^4.2.1", + "chalk": "^4.0.0", + "emittery": "^0.13.1", + "jest-util": "^29.7.0", + "string-length": "^4.0.1" + } + }, + "jest-worker": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-worker/-/jest-worker-29.7.0.tgz", + "integrity": "sha512-eIz2msL/EzL9UFTFFx7jBTkeZfku0yUAyZZZmJ93H2TYEiroIx2PQjEXcwYtYl8zXCxb+PAmA2hLIt/6ZEkPHw==", + "dev": true, + "requires": { + "@types/node": "*", + "jest-util": "^29.7.0", + "merge-stream": "^2.0.0", + "supports-color": "^8.0.0" + }, + "dependencies": { + "supports-color": { + "version": "8.1.1", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-8.1.1.tgz", + "integrity": "sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==", + "dev": true, + "requires": { + "has-flag": "^4.0.0" + } + } + } + }, + "js-tokens": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", + "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", + "dev": true + }, + "js-yaml": { + "version": "3.14.1", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.14.1.tgz", + "integrity": "sha512-okMH7OXXJ7YrN9Ok3/SXrnu4iX9yOk+25nqX4imS2npuvTYDmo/QEZoqwZkYaIDk3jVvBOTOIEgEhaLOynBS9g==", + "dev": true, + "requires": { + "argparse": "^1.0.7", + "esprima": "^4.0.0" + } + }, + "jsesc": { + "version": "2.5.2", + "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-2.5.2.tgz", + "integrity": "sha512-OYu7XEzjkCQ3C5Ps3QIZsQfNpqoJyZZA99wd9aWd05NCtC5pWOkShK2mkL6HXQR6/Cy2lbNdPlZBpuQHXE63gA==", + "dev": true + }, + "json-parse-even-better-errors": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/json-parse-even-better-errors/-/json-parse-even-better-errors-2.3.1.tgz", + "integrity": "sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w==", + "dev": true + }, + "json-schema-traverse": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", + "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", + "dev": true + }, + "json-stable-stringify-without-jsonify": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz", + "integrity": "sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==", + "dev": true + }, + "json5": { + "version": "2.2.3", + "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz", + "integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==" + }, + "jsx-ast-utils": { + "version": "3.3.3", + "resolved": "https://registry.npmjs.org/jsx-ast-utils/-/jsx-ast-utils-3.3.3.tgz", + "integrity": "sha512-fYQHZTZ8jSfmWZ0iyzfwiU4WDX4HpHbMCZ3gPlWYiCl3BoeOTsqKBqnTVfH2rYT7eP5c3sVbeSPHnnJOaTrWiw==", + "dev": true, + "requires": { + "array-includes": "^3.1.5", + "object.assign": "^4.1.3" + } + }, + "kleur": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/kleur/-/kleur-3.0.3.tgz", + "integrity": "sha512-eTIzlVOSUR+JxdDFepEYcBMtZ9Qqdef+rnzWdRZuMbOywu5tO2w2N7rqjoANZ5k9vywhL6Br1VRjUIgTQx4E8w==", + "dev": true + }, + "kuler": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/kuler/-/kuler-2.0.0.tgz", + "integrity": "sha512-Xq9nH7KlWZmXAtodXDDRE7vs6DU1gTU8zYDHDiWLSip45Egwq3plLHzPn27NgvzL2r1LMPC1vdqh98sQxtqj4A==" + }, + "leven": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/leven/-/leven-3.1.0.tgz", + "integrity": "sha512-qsda+H8jTaUaN/x5vzW2rzc+8Rw4TAQ/4KjB46IwK5VH+IlVeeeje/EoZRpiXvIqjFgK84QffqPztGI3VBLG1A==", + "dev": true + }, + "levn": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/levn/-/levn-0.4.1.tgz", + "integrity": "sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==", + "dev": true, + "requires": { + "prelude-ls": "^1.2.1", + "type-check": "~0.4.0" + } + }, + "lines-and-columns": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-1.2.4.tgz", + "integrity": "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==", + "dev": true + }, + "locate-path": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-5.0.0.tgz", + "integrity": "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==", + "dev": true, + "requires": { + "p-locate": "^4.1.0" + } + }, + "lodash": { + "version": "4.17.21", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz", + "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==" + }, + "lodash.memoize": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/lodash.memoize/-/lodash.memoize-4.1.2.tgz", + "integrity": "sha512-t7j+NzmgnQzTAYXcsHYLgimltOV1MXHtlOWf6GjL9Kj8GK5FInw5JotxvbOs+IvV1/Dzo04/fCGfLVs7aXb4Ag==", + "dev": true + }, + "lodash.merge": { + "version": "4.6.2", + "resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz", + "integrity": "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==", + "dev": true + }, + "logform": { + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/logform/-/logform-2.4.2.tgz", + "integrity": "sha512-W4c9himeAwXEdZ05dQNerhFz2XG80P9Oj0loPUMV23VC2it0orMHQhJm4hdnnor3rd1HsGf6a2lPwBM1zeXHGw==", + "requires": { + "@colors/colors": "1.5.0", + "fecha": "^4.2.0", + "ms": "^2.1.1", + "safe-stable-stringify": "^2.3.1", + "triple-beam": "^1.3.0" + }, + "dependencies": { + "ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==" + } + } + }, + "loose-envify": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz", + "integrity": "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==", + "dev": true, + "requires": { + "js-tokens": "^3.0.0 || ^4.0.0" + } + }, + "lru-cache": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", + "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==", + "dev": true, + "requires": { + "yallist": "^4.0.0" + } + }, + "make-dir": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-4.0.0.tgz", + "integrity": "sha512-hXdUTZYIVOt1Ex//jAQi+wTZZpUpwBj/0QsOzqegb3rGMMeJiSEu5xLHnYfBrRV4RH2+OCSOO95Is/7x1WJ4bw==", + "dev": true, + "requires": { + "semver": "^7.5.3" + }, + "dependencies": { + "semver": { + "version": "7.5.4", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.5.4.tgz", + "integrity": "sha512-1bCSESV6Pv+i21Hvpxp3Dx+pSD8lIPt8uVjRrxAUt/nbswYc+tK6Y2btiULjd4+fnq15PX+nqQDC7Oft7WkwcA==", + "dev": true, + "requires": { + "lru-cache": "^6.0.0" + } + } + } + }, + "make-error": { + "version": "1.3.6", + "resolved": "https://registry.npmjs.org/make-error/-/make-error-1.3.6.tgz", + "integrity": "sha512-s8UhlNe7vPKomQhC1qFelMokr/Sc3AgNbso3n74mVPA5LTZwkB9NlXf4XPamLxJE8h0gh73rM94xvwRT2CVInw==" + }, + "makeerror": { + "version": "1.0.12", + "resolved": "https://registry.npmjs.org/makeerror/-/makeerror-1.0.12.tgz", + "integrity": "sha512-JmqCvUhmt43madlpFzG4BQzG2Z3m6tvQDNKdClZnO3VbIudJYmxsT0FNJMeiB2+JTSlTQTSbU8QdesVmwJcmLg==", + "dev": true, + "requires": { + "tmpl": "1.0.5" + } + }, + "media-typer": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz", + "integrity": "sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ==" + }, + "merge-descriptors": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.1.tgz", + "integrity": "sha512-cCi6g3/Zr1iqQi6ySbseM1Xvooa98N0w31jzUYrXPX2xqObmFGHJ0tQ5u74H3mVh7wLouTseZyYIq39g8cNp1w==" + }, + "merge-stream": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/merge-stream/-/merge-stream-2.0.0.tgz", + "integrity": "sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w==", + "dev": true + }, + "merge2": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz", + "integrity": "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==", + "dev": true + }, + "methods": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/methods/-/methods-1.1.2.tgz", + "integrity": "sha512-iclAHeNqNm68zFtnZ0e+1L2yUIdvzNoauKU4WBA3VvH/vPFieF7qfRlwUZU+DA9P9bPXIS90ulxoUoCH23sV2w==" + }, + "micromatch": { + "version": "4.0.5", + "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.5.tgz", + "integrity": "sha512-DMy+ERcEW2q8Z2Po+WNXuw3c5YaUSFjAO5GsJqfEl7UjvtIuFKO6ZrKvcItdy98dwFI2N1tg3zNIdKaQT+aNdA==", + "dev": true, + "requires": { + "braces": "^3.0.2", + "picomatch": "^2.3.1" + } + }, + "mime": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/mime/-/mime-1.6.0.tgz", + "integrity": "sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==" + }, + "mime-db": { + "version": "1.52.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", + "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==" + }, + "mime-types": { + "version": "2.1.35", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", + "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", + "requires": { + "mime-db": "1.52.0" + } + }, + "mimic-fn": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-2.1.0.tgz", + "integrity": "sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg==", + "dev": true + }, + "minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "dev": true, + "requires": { + "brace-expansion": "^1.1.7" + } + }, + "minimist": { + "version": "1.2.7", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.7.tgz", + "integrity": "sha512-bzfL1YUZsP41gmu/qjrEk0Q6i2ix/cVeAhbCbqH9u3zYutS1cLg00qhrD0M2MVdCcx4Sc0UpP2eBWo9rotpq6g==", + "dev": true + }, + "minipass": { + "version": "7.0.4", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.0.4.tgz", + "integrity": "sha512-jYofLM5Dam9279rdkWzqHozUo4ybjdZmCsDHePy5V/PbBcVMiSZR97gmAy45aqi8CK1lG2ECd356FU86avfwUQ==", + "dev": true + }, + "mkdirp": { + "version": "0.5.6", + "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.6.tgz", + "integrity": "sha512-FP+p8RB8OWpF3YZBCrP5gtADmtXApB5AMLn+vdyA+PyxCjrCs00mjyUozssO33cwDeT3wNGdLxJ5M//YqtHAJw==", + "dev": true, + "requires": { + "minimist": "^1.2.6" + } + }, + "ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==" + }, + "mute-stream": { + "version": "0.0.8", + "resolved": "https://registry.npmjs.org/mute-stream/-/mute-stream-0.0.8.tgz", + "integrity": "sha512-nnbWWOkoWyUsTjKrhgD0dcz22mdkSnpYqbEjIm2nhwhuxlSkpywJmBo8h0ZqJdkp73mb90SssHkN4rsRaBAfAA==", + "dev": true + }, + "natural-compare": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz", + "integrity": "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==", + "dev": true + }, + "negotiator": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.3.tgz", + "integrity": "sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg==" + }, + "nice-try": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/nice-try/-/nice-try-1.0.5.tgz", + "integrity": "sha512-1nh45deeb5olNY7eX82BkPO7SSxR5SSYJiPTrTdFUVYwAl8CKMA5N9PjTYkHiRjisVcxcQ1HXdLhx2qxxJzLNQ==", + "dev": true + }, + "node-int64": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/node-int64/-/node-int64-0.4.0.tgz", + "integrity": "sha512-O5lz91xSOeoXP6DulyHfllpq+Eg00MWitZIbtPfoSEvqIHdl5gfcY6hYzDWnj0qD5tz52PI08u9qUvSVeUBeHw==", + "dev": true + }, + "node-releases": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.6.tgz", + "integrity": "sha512-PiVXnNuFm5+iYkLBNeq5211hvO38y63T0i2KKh2KnUs3RpzJ+JtODFjkD8yjLwnDkTYF1eKXheUwdssR+NRZdg==", + "dev": true + }, + "normalize-path": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", + "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==", + "dev": true + }, + "npm-run-path": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-4.0.1.tgz", + "integrity": "sha512-S48WzZW777zhNIrn7gxOlISNAqi9ZC/uQFnRdbeIHhZhCA6UqpkOT8T1G7BvfdgP4Er8gF4sUbaS0i7QvIfCWw==", + "dev": true, + "requires": { + "path-key": "^3.0.0" + } + }, + "object-assign": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", + "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==", + "dev": true + }, + "object-inspect": { + "version": "1.12.2", + "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.12.2.tgz", + "integrity": "sha512-z+cPxW0QGUp0mcqcsgQyLVRDoXFQbXOwBaqyF7VIgI4TWNQsDHrBpUQslRmIfAoYWdYzs6UlKJtB2XJpTaNSpQ==" + }, + "object-keys": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/object-keys/-/object-keys-1.1.1.tgz", + "integrity": "sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA==", + "dev": true + }, + "object.assign": { + "version": "4.1.4", + "resolved": "https://registry.npmjs.org/object.assign/-/object.assign-4.1.4.tgz", + "integrity": "sha512-1mxKf0e58bvyjSCtKYY4sRe9itRk3PJpquJOjeIkz885CczcI4IvJJDLPS72oowuSh+pBxUFROpX+TU++hxhZQ==", + "dev": true, + "requires": { + "call-bind": "^1.0.2", + "define-properties": "^1.1.4", + "has-symbols": "^1.0.3", + "object-keys": "^1.1.1" + } + }, + "object.entries": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/object.entries/-/object.entries-1.1.5.tgz", + "integrity": "sha512-TyxmjUoZggd4OrrU1W66FMDG6CuqJxsFvymeyXI51+vQLN67zYfZseptRge703kKQdo4uccgAKebXFcRCzk4+g==", + "dev": true, + "requires": { + "call-bind": "^1.0.2", + "define-properties": "^1.1.3", + "es-abstract": "^1.19.1" + } + }, + "object.fromentries": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/object.fromentries/-/object.fromentries-2.0.5.tgz", + "integrity": "sha512-CAyG5mWQRRiBU57Re4FKoTBjXfDoNwdFVH2Y1tS9PqCsfUTymAohOkEMSG3aRNKmv4lV3O7p1et7c187q6bynw==", + "dev": true, + "requires": { + "call-bind": "^1.0.2", + "define-properties": "^1.1.3", + "es-abstract": "^1.19.1" + } + }, + "object.hasown": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/object.hasown/-/object.hasown-1.1.1.tgz", + "integrity": "sha512-LYLe4tivNQzq4JdaWW6WO3HMZZJWzkkH8fnI6EebWl0VZth2wL2Lovm74ep2/gZzlaTdV62JZHEqHQ2yVn8Q/A==", + "dev": true, + "requires": { + "define-properties": "^1.1.4", + "es-abstract": "^1.19.5" + } + }, + "object.values": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/object.values/-/object.values-1.1.5.tgz", + "integrity": "sha512-QUZRW0ilQ3PnPpbNtgdNV1PDbEqLIiSFB3l+EnGtBQ/8SUTLj1PZwtQHABZtLgwpJZTSZhuGLOGk57Drx2IvYg==", + "dev": true, + "requires": { + "call-bind": "^1.0.2", + "define-properties": "^1.1.3", + "es-abstract": "^1.19.1" + } + }, + "on-finished": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz", + "integrity": "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==", + "requires": { + "ee-first": "1.1.1" + } + }, + "on-headers": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/on-headers/-/on-headers-1.0.2.tgz", + "integrity": "sha512-pZAE+FJLoyITytdqK0U5s+FIpjN0JP3OzFi/u8Rx+EV5/W+JTWGXG8xFzevE7AjBfDqHv/8vL8qQsIhHnqRkrA==" + }, + "once": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", + "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", + "dev": true, + "requires": { + "wrappy": "1" + } + }, + "one-time": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/one-time/-/one-time-1.0.0.tgz", + "integrity": "sha512-5DXOiRKwuSEcQ/l0kGCF6Q3jcADFv5tSmRaJck/OqkVFcOzutB134KRSfF0xDrL39MNnqxbHBbUUcjZIhTgb2g==", + "requires": { + "fn.name": "1.x.x" + } + }, + "onetime": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/onetime/-/onetime-5.1.2.tgz", + "integrity": "sha512-kbpaSSGJTWdAY5KPVeMOKXSrPtr8C8C7wodJbcsd51jRnmD+GZu8Y0VoU6Dm5Z4vWr0Ig/1NKuWRKf7j5aaYSg==", + "dev": true, + "requires": { + "mimic-fn": "^2.1.0" + } + }, + "open": { + "version": "9.1.0", + "resolved": "https://registry.npmjs.org/open/-/open-9.1.0.tgz", + "integrity": "sha512-OS+QTnw1/4vrf+9hh1jc1jnYjzSG4ttTBB8UxOwAnInG3Uo4ssetzC1ihqaIHjLJnA5GGlRl6QlZXOTQhRBUvg==", + "dev": true, + "requires": { + "default-browser": "^4.0.0", + "define-lazy-prop": "^3.0.0", + "is-inside-container": "^1.0.0", + "is-wsl": "^2.2.0" + } + }, + "optionator": { + "version": "0.9.3", + "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.3.tgz", + "integrity": "sha512-JjCoypp+jKn1ttEFExxhetCKeJt9zhAgAve5FXHixTvFDW/5aEktX9bufBKLRRMdU7bNtpLfcGu94B3cdEJgjg==", + "dev": true, + "requires": { + "@aashutoshrathi/word-wrap": "^1.2.3", + "deep-is": "^0.1.3", + "fast-levenshtein": "^2.0.6", + "levn": "^0.4.1", + "prelude-ls": "^1.2.1", + "type-check": "^0.4.0" + } + }, + "os-tmpdir": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/os-tmpdir/-/os-tmpdir-1.0.2.tgz", + "integrity": "sha512-D2FR03Vir7FIu45XBY20mTb+/ZSWB00sjU9jdQXt83gDrI4Ztz5Fs7/yy74g2N5SVQY4xY1qDr4rNddwYRVX0g==", + "dev": true + }, + "p-limit": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", + "integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==", + "dev": true, + "requires": { + "yocto-queue": "^0.1.0" + } + }, + "p-locate": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-4.1.0.tgz", + "integrity": "sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==", + "dev": true, + "requires": { + "p-limit": "^2.2.0" + }, + "dependencies": { + "p-limit": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz", + "integrity": "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==", + "dev": true, + "requires": { + "p-try": "^2.0.0" + } + } + } + }, + "p-try": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/p-try/-/p-try-2.2.0.tgz", + "integrity": "sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==", + "dev": true + }, + "parent-module": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz", + "integrity": "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==", + "dev": true, + "requires": { + "callsites": "^3.0.0" + } + }, + "parse-json": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-5.2.0.tgz", + "integrity": "sha512-ayCKvm/phCGxOkYRSCM82iDwct8/EonSEgCSxWxD7ve6jHggsFl4fZVQBPRNgQoKiuV/odhFrGzQXZwbifC8Rg==", + "dev": true, + "requires": { + "@babel/code-frame": "^7.0.0", + "error-ex": "^1.3.1", + "json-parse-even-better-errors": "^2.3.0", + "lines-and-columns": "^1.1.6" + } + }, + "parseurl": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz", + "integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==" + }, + "path-exists": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", + "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", + "dev": true + }, + "path-is-absolute": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", + "integrity": "sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==", + "dev": true + }, + "path-key": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", + "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", + "dev": true + }, + "path-parse": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz", + "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==", + "dev": true + }, + "path-scurry": { + "version": "1.10.1", + "resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-1.10.1.tgz", + "integrity": "sha512-MkhCqzzBEpPvxxQ71Md0b1Kk51W01lrYvlMzSUaIzNsODdd7mqhiimSZlr+VegAz5Z6Vzt9Xg2ttE//XBhH3EQ==", + "dev": true, + "requires": { + "lru-cache": "^9.1.1 || ^10.0.0", + "minipass": "^5.0.0 || ^6.0.2 || ^7.0.0" + }, + "dependencies": { + "lru-cache": { + "version": "10.0.1", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.0.1.tgz", + "integrity": "sha512-IJ4uwUTi2qCccrioU6g9g/5rvvVl13bsdczUUcqbciD9iLr095yj8DQKdObriEvuNSx325N1rV1O0sJFszx75g==", + "dev": true + } + } + }, + "path-to-regexp": { + "version": "0.1.7", + "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.7.tgz", + "integrity": "sha512-5DFkuoqlv1uYQKxy8omFBeJPQcdoE07Kv2sferDCrAq1ohOU+MSDswDIbnx3YAM60qIOnYa53wBhXW0EbMonrQ==" + }, + "path-type": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-type/-/path-type-4.0.0.tgz", + "integrity": "sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==", + "dev": true + }, + "picocolors": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.0.0.tgz", + "integrity": "sha512-1fygroTLlHu66zi26VoTDv8yRgm0Fccecssto+MhsZ0D/DGW2sm8E8AjW7NU5VVTRt5GxbeZ5qBuJr+HyLYkjQ==", + "dev": true + }, + "picomatch": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", + "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", + "dev": true + }, + "pirates": { + "version": "4.0.5", + "resolved": "https://registry.npmjs.org/pirates/-/pirates-4.0.5.tgz", + "integrity": "sha512-8V9+HQPupnaXMA23c5hvl69zXvTwTzyAYasnkb0Tts4XvO4CliqONMOnvlq26rkhLC3nWDFBJf73LU1e1VZLaQ==", + "dev": true + }, + "pkg-dir": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/pkg-dir/-/pkg-dir-4.2.0.tgz", + "integrity": "sha512-HRDzbaKjC+AOWVXxAU/x54COGeIv9eb+6CkDSQoNTt4XyWoIJvuPsXizxu/Fr23EiekbtZwmh1IcIG/l/a10GQ==", + "dev": true, + "requires": { + "find-up": "^4.0.0" + } + }, + "prelude-ls": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz", + "integrity": "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==", + "dev": true + }, + "prettier": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.0.3.tgz", + "integrity": "sha512-L/4pUDMxcNa8R/EthV08Zt42WBO4h1rarVtK0K+QJG0X187OLo7l699jWw0GKuwzkPQ//jMFA/8Xm6Fh3J/DAg==", + "dev": true + }, + "prettier-linter-helpers": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/prettier-linter-helpers/-/prettier-linter-helpers-1.0.0.tgz", + "integrity": "sha512-GbK2cP9nraSSUF9N2XwUwqfzlAFlMNYYl+ShE/V+H8a9uNl/oUqB1w2EL54Jh0OlyRSd8RfWYJ3coVS4TROP2w==", + "dev": true, + "requires": { + "fast-diff": "^1.1.2" + } + }, + "pretty-format": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-29.7.0.tgz", + "integrity": "sha512-Pdlw/oPxN+aXdmM9R00JVC9WVFoCLTKJvDVLgmJ+qAffBMxsV85l/Lu7sNx4zSzPyoL2euImuEwHhOXdEgNFZQ==", + "dev": true, + "requires": { + "@jest/schemas": "^29.6.3", + "ansi-styles": "^5.0.0", + "react-is": "^18.0.0" + }, + "dependencies": { + "ansi-styles": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz", + "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==", + "dev": true + } + } + }, + "progress": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/progress/-/progress-2.0.3.tgz", + "integrity": "sha512-7PiHtLll5LdnKIMw100I+8xJXR5gW2QwWYkT6iJva0bXitZKa/XMrSbdmg3r2Xnaidz9Qumd0VPaMrZlF9V9sA==", + "dev": true + }, + "prompts": { + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/prompts/-/prompts-2.4.2.tgz", + "integrity": "sha512-NxNv/kLguCA7p3jE8oL2aEBsrJWgAakBpgmgK6lpPWV+WuOmY6r2/zbAVnP+T8bQlA0nzHXSJSJW0Hq7ylaD2Q==", + "dev": true, + "requires": { + "kleur": "^3.0.3", + "sisteransi": "^1.0.5" + } + }, + "prop-types": { + "version": "15.8.1", + "resolved": "https://registry.npmjs.org/prop-types/-/prop-types-15.8.1.tgz", + "integrity": "sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg==", + "dev": true, + "requires": { + "loose-envify": "^1.4.0", + "object-assign": "^4.1.1", + "react-is": "^16.13.1" + }, + "dependencies": { + "react-is": { + "version": "16.13.1", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz", + "integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==", + "dev": true + } + } + }, + "proxy-addr": { + "version": "2.0.7", + "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz", + "integrity": "sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==", + "requires": { + "forwarded": "0.2.0", + "ipaddr.js": "1.9.1" + } + }, + "proxy-from-env": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz", + "integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==" + }, + "punycode": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.1.1.tgz", + "integrity": "sha512-XRsRjdf+j5ml+y/6GKHPZbrF/8p2Yga0JPtdqTIY2Xe5ohJPD9saDJJLPvp9+NSBprVvevdXZybnj2cv8OEd0A==", + "dev": true + }, + "pure-rand": { + "version": "6.0.4", + "resolved": "https://registry.npmjs.org/pure-rand/-/pure-rand-6.0.4.tgz", + "integrity": "sha512-LA0Y9kxMYv47GIPJy6MI84fqTd2HmYZI83W/kM/SkKfDlajnZYfmXFTxkbY+xSBPkLJxltMa9hIkmdc29eguMA==", + "dev": true + }, + "qs": { + "version": "6.11.0", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.11.0.tgz", + "integrity": "sha512-MvjoMCJwEarSbUYk5O+nmoSzSutSsTwF85zcHPQ9OrlFoZOYIjaqBAJIqIXjptyD5vThxGq52Xu/MaJzRkIk4Q==", + "requires": { + "side-channel": "^1.0.4" + } + }, + "queue-microtask": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz", + "integrity": "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==", + "dev": true + }, + "range-parser": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz", + "integrity": "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==" + }, + "raw-body": { + "version": "2.5.1", + "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.5.1.tgz", + "integrity": "sha512-qqJBtEyVgS0ZmPGdCFPWJ3FreoqvG4MVQln/kCgF7Olq95IbOp0/BWyMwbdtn4VTvkM8Y7khCQ2Xgk/tcrCXig==", + "requires": { + "bytes": "3.1.2", + "http-errors": "2.0.0", + "iconv-lite": "0.4.24", + "unpipe": "1.0.0" + } + }, + "react-is": { + "version": "18.2.0", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-18.2.0.tgz", + "integrity": "sha512-xWGDIW6x921xtzPkhiULtthJHoJvBbF3q26fzloPCK0hsvxtPVelvftw3zjbHWSkR2km9Z+4uxbDDK/6Zw9B8w==", + "dev": true + }, + "readable-stream": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.0.tgz", + "integrity": "sha512-BViHy7LKeTz4oNnkcLJ+lVSL6vpiFeX6/d3oSH8zCW7UxP2onchk+vTGB143xuFjHS3deTgkKoXXymXqymiIdA==", + "requires": { + "inherits": "^2.0.3", + "string_decoder": "^1.1.1", + "util-deprecate": "^1.0.1" + } + }, + "readdirp": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz", + "integrity": "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==", + "dev": true, + "requires": { + "picomatch": "^2.2.1" + } + }, + "regexp.prototype.flags": { + "version": "1.4.3", + "resolved": "https://registry.npmjs.org/regexp.prototype.flags/-/regexp.prototype.flags-1.4.3.tgz", + "integrity": "sha512-fjggEOO3slI6Wvgjwflkc4NFRCTZAu5CnNfBd5qOMYhWdn67nJBBu34/TkD++eeFmd8C9r9jfXJ27+nSiRkSUA==", + "dev": true, + "requires": { + "call-bind": "^1.0.2", + "define-properties": "^1.1.3", + "functions-have-names": "^1.2.2" + } + }, + "require-directory": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", + "integrity": "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==", + "dev": true + }, + "resolve": { + "version": "1.22.1", + "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.1.tgz", + "integrity": "sha512-nBpuuYuY5jFsli/JIs1oldw6fOQCBioohqWZg/2hiaOybXOft4lonv85uDOKXdf8rhyK159cxU5cDcK/NKk8zw==", + "dev": true, + "requires": { + "is-core-module": "^2.9.0", + "path-parse": "^1.0.7", + "supports-preserve-symlinks-flag": "^1.0.0" + } + }, + "resolve-cwd": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/resolve-cwd/-/resolve-cwd-3.0.0.tgz", + "integrity": "sha512-OrZaX2Mb+rJCpH/6CpSqt9xFVpN++x01XnN2ie9g6P5/3xelLAkXWVADpdz1IHD/KFfEXyE6V0U01OQ3UO2rEg==", + "dev": true, + "requires": { + "resolve-from": "^5.0.0" + } + }, + "resolve-from": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-5.0.0.tgz", + "integrity": "sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw==", + "dev": true + }, + "resolve.exports": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/resolve.exports/-/resolve.exports-2.0.2.tgz", + "integrity": "sha512-X2UW6Nw3n/aMgDVy+0rSqgHlv39WZAlZrXCdnbyEiKm17DSqHX4MmQMaST3FbeWR5FTuRcUwYAziZajji0Y7mg==", + "dev": true + }, + "restore-cursor": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/restore-cursor/-/restore-cursor-3.1.0.tgz", + "integrity": "sha512-l+sSefzHpj5qimhFSE5a8nufZYAM3sBSVMAPtYkmC+4EH2anSGaEMXSD0izRQbu9nfyQ9y5JrVmp7E8oZrUjvA==", + "dev": true, + "requires": { + "onetime": "^5.1.0", + "signal-exit": "^3.0.2" + } + }, + "reusify": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.0.4.tgz", + "integrity": "sha512-U9nH88a3fc/ekCF1l0/UP1IosiuIjyTh7hBvXVMHYgVcfGvt897Xguj2UOLDeI5BG2m7/uwyaLVT6fbtCwTyzw==", + "dev": true + }, + "rimraf": { + "version": "5.0.5", + "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-5.0.5.tgz", + "integrity": "sha512-CqDakW+hMe/Bz202FPEymy68P+G50RfMQK+Qo5YUqc9SPipvbGjCGKd0RSKEelbsfQuw3g5NZDSrlZZAJurH1A==", + "dev": true, + "requires": { + "glob": "^10.3.7" + }, + "dependencies": { + "brace-expansion": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", + "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", + "dev": true, + "requires": { + "balanced-match": "^1.0.0" + } + }, + "glob": { + "version": "10.3.10", + "resolved": "https://registry.npmjs.org/glob/-/glob-10.3.10.tgz", + "integrity": "sha512-fa46+tv1Ak0UPK1TOy/pZrIybNNt4HCv7SDzwyfiOZkvZLEbjsZkJBPtDHVshZjbecAoAGSC20MjLDG/qr679g==", + "dev": true, + "requires": { + "foreground-child": "^3.1.0", + "jackspeak": "^2.3.5", + "minimatch": "^9.0.1", + "minipass": "^5.0.0 || ^6.0.2 || ^7.0.0", + "path-scurry": "^1.10.1" + } + }, + "minimatch": { + "version": "9.0.3", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.3.tgz", + "integrity": "sha512-RHiac9mvaRw0x3AYRgDC1CxAP7HTcNrrECeA8YYJeWnpo+2Q5CegtZjaotWTWxDG3UeGA1coE05iH1mPjT/2mg==", + "dev": true, + "requires": { + "brace-expansion": "^2.0.1" + } + } + } + }, + "run-applescript": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/run-applescript/-/run-applescript-5.0.0.tgz", + "integrity": "sha512-XcT5rBksx1QdIhlFOCtgZkB99ZEouFZ1E2Kc2LHqNW13U3/74YGdkQRmThTwxy4QIyookibDKYZOPqX//6BlAg==", + "dev": true, + "requires": { + "execa": "^5.0.0" + } + }, + "run-async": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/run-async/-/run-async-2.4.1.tgz", + "integrity": "sha512-tvVnVv01b8c1RrA6Ep7JkStj85Guv/YrMcwqYQnwjsAS2cTmmPGBBjAjpCW7RrSodNSoE2/qg9O4bceNvUuDgQ==", + "dev": true + }, + "run-parallel": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz", + "integrity": "sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==", + "dev": true, + "requires": { + "queue-microtask": "^1.2.2" + } + }, + "rxjs": { + "version": "6.6.7", + "resolved": "https://registry.npmjs.org/rxjs/-/rxjs-6.6.7.tgz", + "integrity": "sha512-hTdwr+7yYNIT5n4AMYp85KA6yw2Va0FLa3Rguvbpa4W3I5xynaBZo41cM3XM+4Q6fRMj3sBYIR1VAmZMXYJvRQ==", + "dev": true, + "requires": { + "tslib": "^1.9.0" + } + }, + "safe-buffer": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", + "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==" + }, + "safe-regex-test": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/safe-regex-test/-/safe-regex-test-1.0.0.tgz", + "integrity": "sha512-JBUUzyOgEwXQY1NuPtvcj/qcBDbDmEvWufhlnXZIm75DEHp+afM1r1ujJpJsV/gSM4t59tpDyPi1sd6ZaPFfsA==", + "dev": true, + "requires": { + "call-bind": "^1.0.2", + "get-intrinsic": "^1.1.3", + "is-regex": "^1.1.4" + } + }, + "safe-stable-stringify": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/safe-stable-stringify/-/safe-stable-stringify-2.4.1.tgz", + "integrity": "sha512-dVHE6bMtS/bnL2mwualjc6IxEv1F+OCUpA46pKUj6F8uDbUM0jCCulPqRNPSnWwGNKx5etqMjZYdXtrm5KJZGA==" + }, + "safer-buffer": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", + "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==" + }, + "semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "dev": true + }, + "send": { + "version": "0.18.0", + "resolved": "https://registry.npmjs.org/send/-/send-0.18.0.tgz", + "integrity": "sha512-qqWzuOjSFOuqPjFe4NOsMLafToQQwBSOEpS+FwEt3A2V3vKubTquT3vmLTQpFgMXp8AlFWFuP1qKaJZOtPpVXg==", + "requires": { + "debug": "2.6.9", + "depd": "2.0.0", + "destroy": "1.2.0", + "encodeurl": "~1.0.2", + "escape-html": "~1.0.3", + "etag": "~1.8.1", + "fresh": "0.5.2", + "http-errors": "2.0.0", + "mime": "1.6.0", + "ms": "2.1.3", + "on-finished": "2.4.1", + "range-parser": "~1.2.1", + "statuses": "2.0.1" + }, + "dependencies": { + "ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==" + } + } + }, + "serve-static": { + "version": "1.15.0", + "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.15.0.tgz", + "integrity": "sha512-XGuRDNjXUijsUL0vl6nSD7cwURuzEgglbOaFuZM9g3kwDXOWVTck0jLzjPzGD+TazWbboZYu52/9/XPdUgne9g==", + "requires": { + "encodeurl": "~1.0.2", + "escape-html": "~1.0.3", + "parseurl": "~1.3.3", + "send": "0.18.0" + } + }, + "setprototypeof": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz", + "integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==" + }, + "shebang-command": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", + "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", + "dev": true, + "requires": { + "shebang-regex": "^3.0.0" + } + }, + "shebang-regex": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", + "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", + "dev": true + }, + "side-channel": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.0.4.tgz", + "integrity": "sha512-q5XPytqFEIKHkGdiMIrY10mvLRvnQh42/+GoBlFW3b2LXLE2xxJpZFdm94we0BaoV3RwJyGqg5wS7epxTv0Zvw==", + "requires": { + "call-bind": "^1.0.0", + "get-intrinsic": "^1.0.2", + "object-inspect": "^1.9.0" + } + }, + "signal-exit": { + "version": "3.0.7", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.7.tgz", + "integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==", + "dev": true + }, + "simple-swizzle": { + "version": "0.2.2", + "resolved": "https://registry.npmjs.org/simple-swizzle/-/simple-swizzle-0.2.2.tgz", + "integrity": "sha512-JA//kQgZtbuY83m+xT+tXJkmJncGMTFT+C+g2h2R9uxkYIrE2yy9sgmcLhCnw57/WSD+Eh3J97FPEDFnbXnDUg==", + "requires": { + "is-arrayish": "^0.3.1" + }, + "dependencies": { + "is-arrayish": { + "version": "0.3.2", + "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.3.2.tgz", + "integrity": "sha512-eVRqCvVlZbuw3GrM63ovNSNAeA1K16kaR/LRY/92w0zxQ5/1YzwblUX652i4Xs9RwAGjW9d9y6X88t8OaAJfWQ==" + } + } + }, + "sisteransi": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/sisteransi/-/sisteransi-1.0.5.tgz", + "integrity": "sha512-bLGGlR1QxBcynn2d5YmDX4MGjlZvy2MRBDRNHLJ8VI6l6+9FUiyTFNJ0IveOSP0bcXgVDPRcfGqA0pjaqUpfVg==", + "dev": true + }, + "slash": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/slash/-/slash-3.0.0.tgz", + "integrity": "sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==", + "dev": true + }, + "slice-ansi": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/slice-ansi/-/slice-ansi-2.1.0.tgz", + "integrity": "sha512-Qu+VC3EwYLldKa1fCxuuvULvSJOKEgk9pi8dZeCVK7TqBfUNTH4sFkk4joj8afVSfAYgJoSOetjx9QWOJ5mYoQ==", + "dev": true, + "requires": { + "ansi-styles": "^3.2.0", + "astral-regex": "^1.0.0", + "is-fullwidth-code-point": "^2.0.0" + }, + "dependencies": { + "ansi-styles": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz", + "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==", + "dev": true, + "requires": { + "color-convert": "^1.9.0" + } + }, + "color-convert": { + "version": "1.9.3", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz", + "integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==", + "dev": true, + "requires": { + "color-name": "1.1.3" + } + }, + "color-name": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz", + "integrity": "sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw==", + "dev": true + }, + "is-fullwidth-code-point": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-2.0.0.tgz", + "integrity": "sha512-VHskAKYM8RfSFXwee5t5cbN5PZeq1Wrh6qd5bkyiXIf6UQcN6w/A0eXM9r6t8d+GYOh+o6ZhiEnb88LN/Y8m2w==", + "dev": true + } + } + }, + "source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "dev": true + }, + "source-map-support": { + "version": "0.5.13", + "resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.13.tgz", + "integrity": "sha512-SHSKFHadjVA5oR4PPqhtAVdcBWwRYVd6g6cAXnIbRiIwc2EhPrTuKUBdSLvlEKyIP3GCf89fltvcZiP9MMFA1w==", + "dev": true, + "requires": { + "buffer-from": "^1.0.0", + "source-map": "^0.6.0" + } + }, + "sprintf-js": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.0.3.tgz", + "integrity": "sha512-D9cPgkvLlV3t3IzL0D0YLvGA9Ahk4PcvVwUbN0dSGr1aP0Nrt4AEnTUbuGvquEC0mA64Gqt1fzirlRs5ibXx8g==", + "dev": true + }, + "stack-trace": { + "version": "0.0.10", + "resolved": "https://registry.npmjs.org/stack-trace/-/stack-trace-0.0.10.tgz", + "integrity": "sha512-KGzahc7puUKkzyMt+IqAep+TVNbKP+k2Lmwhub39m1AsTSkaDutx56aDCo+HLDzf/D26BIHTJWNiTG1KAJiQCg==" + }, + "stack-utils": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/stack-utils/-/stack-utils-2.0.5.tgz", + "integrity": "sha512-xrQcmYhOsn/1kX+Vraq+7j4oE2j/6BFscZ0etmYg81xuM8Gq0022Pxb8+IqgOFUIaxHs0KaSb7T1+OegiNrNFA==", + "dev": true, + "requires": { + "escape-string-regexp": "^2.0.0" + } + }, + "statuses": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.1.tgz", + "integrity": "sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ==" + }, + "string-length": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/string-length/-/string-length-4.0.2.tgz", + "integrity": "sha512-+l6rNN5fYHNhZZy41RXsYptCjA2Igmq4EG7kZAYFQI1E1VTXarr6ZPXBg6eq7Y6eK4FEhY6AJlyuFIb/v/S0VQ==", + "dev": true, + "requires": { + "char-regex": "^1.0.2", + "strip-ansi": "^6.0.0" + } + }, + "string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "dev": true, + "requires": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + } + }, + "string.prototype.matchall": { + "version": "4.0.7", + "resolved": "https://registry.npmjs.org/string.prototype.matchall/-/string.prototype.matchall-4.0.7.tgz", + "integrity": "sha512-f48okCX7JiwVi1NXCVWcFnZgADDC/n2vePlQ/KUCNqCikLLilQvwjMO8+BHVKvgzH0JB0J9LEPgxOGT02RoETg==", + "dev": true, + "requires": { + "call-bind": "^1.0.2", + "define-properties": "^1.1.3", + "es-abstract": "^1.19.1", + "get-intrinsic": "^1.1.1", + "has-symbols": "^1.0.3", + "internal-slot": "^1.0.3", + "regexp.prototype.flags": "^1.4.1", + "side-channel": "^1.0.4" + } + }, + "string.prototype.trimend": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/string.prototype.trimend/-/string.prototype.trimend-1.0.5.tgz", + "integrity": "sha512-I7RGvmjV4pJ7O3kdf+LXFpVfdNOxtCW/2C8f6jNiW4+PQchwxkCDzlk1/7p+Wl4bqFIZeF47qAHXLuHHWKAxog==", + "dev": true, + "requires": { + "call-bind": "^1.0.2", + "define-properties": "^1.1.4", + "es-abstract": "^1.19.5" + } + }, + "string.prototype.trimstart": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/string.prototype.trimstart/-/string.prototype.trimstart-1.0.5.tgz", + "integrity": "sha512-THx16TJCGlsN0o6dl2o6ncWUsdgnLRSA23rRE5pyGBw/mLr3Ej/R2LaqCtgP8VNMGZsvMWnf9ooZPyY2bHvUFg==", + "dev": true, + "requires": { + "call-bind": "^1.0.2", + "define-properties": "^1.1.4", + "es-abstract": "^1.19.5" + } + }, + "string_decoder": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz", + "integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==", + "requires": { + "safe-buffer": "~5.2.0" + } + }, + "strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, + "requires": { + "ansi-regex": "^5.0.1" + } + }, + "strip-bom": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-4.0.0.tgz", + "integrity": "sha512-3xurFv5tEgii33Zi8Jtp55wEIILR9eh34FAW00PZf+JnSsTmV/ioewSgQl97JHvgjoRGwPShsWm+IdrxB35d0w==", + "dev": true + }, + "strip-final-newline": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/strip-final-newline/-/strip-final-newline-2.0.0.tgz", + "integrity": "sha512-BrpvfNAE3dcvq7ll3xVumzjKjZQ5tI1sEUIKr3Uoks0XUl45St3FlatVqef9prk4jRDzhW6WZg+3bk93y6pLjA==", + "dev": true + }, + "strip-json-comments": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz", + "integrity": "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==", + "dev": true + }, + "supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "requires": { + "has-flag": "^4.0.0" + } + }, + "supports-preserve-symlinks-flag": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz", + "integrity": "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==", + "dev": true + }, + "synckit": { + "version": "0.8.5", + "resolved": "https://registry.npmjs.org/synckit/-/synckit-0.8.5.tgz", + "integrity": "sha512-L1dapNV6vu2s/4Sputv8xGsCdAVlb5nRDMFU/E27D44l5U6cw1g0dGd45uLc+OXjNMmF4ntiMdCimzcjFKQI8Q==", + "dev": true, + "requires": { + "@pkgr/utils": "^2.3.1", + "tslib": "^2.5.0" + }, + "dependencies": { + "tslib": { + "version": "2.6.2", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.6.2.tgz", + "integrity": "sha512-AEYxH93jGFPn/a2iVAwW87VuUIkR1FVUKB77NwMF7nBTDkDrrT/Hpt/IrCJ0QXhW27jTBDcf5ZY7w6RiqTMw2Q==", + "dev": true + } + } + }, + "table": { + "version": "5.4.6", + "resolved": "https://registry.npmjs.org/table/-/table-5.4.6.tgz", + "integrity": "sha512-wmEc8m4fjnob4gt5riFRtTu/6+4rSe12TpAELNSqHMfF3IqnA+CH37USM6/YR3qRZv7e56kAEAtd6nKZaxe0Ug==", + "dev": true, + "requires": { + "ajv": "^6.10.2", + "lodash": "^4.17.14", + "slice-ansi": "^2.1.0", + "string-width": "^3.0.0" + }, + "dependencies": { + "ansi-regex": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-4.1.1.tgz", + "integrity": "sha512-ILlv4k/3f6vfQ4OoP2AGvirOktlQ98ZEL1k9FaQjxa3L1abBgbuTDAdPOpvbGncC0BTVQrl+OM8xZGK6tWXt7g==", + "dev": true + }, + "emoji-regex": { + "version": "7.0.3", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-7.0.3.tgz", + "integrity": "sha512-CwBLREIQ7LvYFB0WyRvwhq5N5qPhc6PMjD6bYggFlI5YyDgl+0vxq5VHbMOFqLg7hfWzmu8T5Z1QofhmTIhItA==", + "dev": true + }, + "is-fullwidth-code-point": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-2.0.0.tgz", + "integrity": "sha512-VHskAKYM8RfSFXwee5t5cbN5PZeq1Wrh6qd5bkyiXIf6UQcN6w/A0eXM9r6t8d+GYOh+o6ZhiEnb88LN/Y8m2w==", + "dev": true + }, + "string-width": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-3.1.0.tgz", + "integrity": "sha512-vafcv6KjVZKSgz06oM/H6GDBrAtz8vdhQakGjFIvNrHA6y3HCF1CInLy+QLq8dTJPQ1b+KDUqDFctkdRW44e1w==", + "dev": true, + "requires": { + "emoji-regex": "^7.0.1", + "is-fullwidth-code-point": "^2.0.0", + "strip-ansi": "^5.1.0" + } + }, + "strip-ansi": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-5.2.0.tgz", + "integrity": "sha512-DuRs1gKbBqsMKIZlrffwlug8MHkcnpjs5VPmL1PAh+mA30U0DTotfDZ0d2UUsXpPmPmMMJ6W773MaA3J+lbiWA==", + "dev": true, + "requires": { + "ansi-regex": "^4.1.0" + } + } + } + }, + "test-exclude": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/test-exclude/-/test-exclude-6.0.0.tgz", + "integrity": "sha512-cAGWPIyOHU6zlmg88jwm7VRyXnMN7iV68OGAbYDk/Mh/xC/pzVPlQtY6ngoIH/5/tciuhGfvESU8GrHrcxD56w==", + "dev": true, + "requires": { + "@istanbuljs/schema": "^0.1.2", + "glob": "^7.1.4", + "minimatch": "^3.0.4" + } + }, + "text-hex": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/text-hex/-/text-hex-1.0.0.tgz", + "integrity": "sha512-uuVGNWzgJ4yhRaNSiubPY7OjISw4sw4E5Uv0wbjp+OzcbmVU/rsT8ujgcXJhn9ypzsgr5vlzpPqP+MBBKcGvbg==" + }, + "text-table": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/text-table/-/text-table-0.2.0.tgz", + "integrity": "sha512-N+8UisAXDGk8PFXP4HAzVR9nbfmVJ3zYLAWiTIoqC5v5isinhr+r5uaO8+7r3BMfuNIufIsA7RdpVgacC2cSpw==", + "dev": true + }, + "through": { + "version": "2.3.8", + "resolved": "https://registry.npmjs.org/through/-/through-2.3.8.tgz", + "integrity": "sha512-w89qg7PI8wAdvX60bMDP+bFoD5Dvhm9oLheFp5O4a2QF0cSBGsBX4qZmadPMvVqlLJBBci+WqGGOAPvcDeNSVg==", + "dev": true + }, + "titleize": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/titleize/-/titleize-3.0.0.tgz", + "integrity": "sha512-KxVu8EYHDPBdUYdKZdKtU2aj2XfEx9AfjXxE/Aj0vT06w2icA09Vus1rh6eSu1y01akYg6BjIK/hxyLJINoMLQ==", + "dev": true + }, + "tmp": { + "version": "0.0.33", + "resolved": "https://registry.npmjs.org/tmp/-/tmp-0.0.33.tgz", + "integrity": "sha512-jRCJlojKnZ3addtTOjdIqoRuPEKBvNXcGYqzO6zWZX8KfKEpnGY5jfggJQ3EjKuu8D4bJRr0y+cYJFmYbImXGw==", + "dev": true, + "requires": { + "os-tmpdir": "~1.0.2" + } + }, + "tmpl": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/tmpl/-/tmpl-1.0.5.tgz", + "integrity": "sha512-3f0uOEAQwIqGuWW2MVzYg8fV/QNnc/IpuJNG837rLuczAaLVHslWHZQj4IGiEl5Hs3kkbhwL9Ab7Hrsmuj+Smw==", + "dev": true + }, + "to-fast-properties": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/to-fast-properties/-/to-fast-properties-2.0.0.tgz", + "integrity": "sha512-/OaKK0xYrs3DmxRYqL/yDc+FxFUVYhDlXMhRmv3z915w2HF1tnN1omB354j8VUGO/hbRzyD6Y3sA7v7GS/ceog==", + "dev": true + }, + "to-regex-range": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", + "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", + "dev": true, + "requires": { + "is-number": "^7.0.0" + } + }, + "toidentifier": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz", + "integrity": "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==" + }, + "tree-kill": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/tree-kill/-/tree-kill-1.2.2.tgz", + "integrity": "sha512-L0Orpi8qGpRG//Nd+H90vFB+3iHnue1zSSGmNOOCh1GLJ7rUKVwV2HvijphGQS2UmhUZewS9VgvxYIdgr+fG1A==", + "dev": true + }, + "triple-beam": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/triple-beam/-/triple-beam-1.3.0.tgz", + "integrity": "sha512-XrHUvV5HpdLmIj4uVMxHggLbFSZYIn7HEWsqePZcI50pco+MPqJ50wMGY794X7AOOhxOBAjbkqfAbEe/QMp2Lw==" + }, + "ts-api-utils": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-1.0.2.tgz", + "integrity": "sha512-Cbu4nIqnEdd+THNEsBdkolnOXhg0I8XteoHaEKgvsxpsbWda4IsUut2c187HxywQCvveojow0Dgw/amxtSKVkQ==", + "dev": true + }, + "ts-jest": { + "version": "29.1.1", + "resolved": "https://registry.npmjs.org/ts-jest/-/ts-jest-29.1.1.tgz", + "integrity": "sha512-D6xjnnbP17cC85nliwGiL+tpoKN0StpgE0TeOjXQTU6MVCfsB4v7aW05CgQ/1OywGb0x/oy9hHFnN+sczTiRaA==", + "dev": true, + "requires": { + "bs-logger": "0.x", + "fast-json-stable-stringify": "2.x", + "jest-util": "^29.0.0", + "json5": "^2.2.3", + "lodash.memoize": "4.x", + "make-error": "1.x", + "semver": "^7.5.3", + "yargs-parser": "^21.0.1" + }, + "dependencies": { + "semver": { + "version": "7.5.4", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.5.4.tgz", + "integrity": "sha512-1bCSESV6Pv+i21Hvpxp3Dx+pSD8lIPt8uVjRrxAUt/nbswYc+tK6Y2btiULjd4+fnq15PX+nqQDC7Oft7WkwcA==", + "dev": true, + "requires": { + "lru-cache": "^6.0.0" + } + } + } + }, + "ts-node": { + "version": "10.9.1", + "resolved": "https://registry.npmjs.org/ts-node/-/ts-node-10.9.1.tgz", + "integrity": "sha512-NtVysVPkxxrwFGUUxGYhfux8k78pQB3JqYBXlLRZgdGUqTO5wU/UyHop5p70iEbGhB7q5KmiZiU0Y3KlJrScEw==", + "requires": { + "@cspotcode/source-map-support": "^0.8.0", + "@tsconfig/node10": "^1.0.7", + "@tsconfig/node12": "^1.0.7", + "@tsconfig/node14": "^1.0.0", + "@tsconfig/node16": "^1.0.2", + "acorn": "^8.4.1", + "acorn-walk": "^8.1.1", + "arg": "^4.1.0", + "create-require": "^1.1.0", + "diff": "^4.0.1", + "make-error": "^1.1.1", + "v8-compile-cache-lib": "^3.0.1", + "yn": "3.1.1" + } + }, + "ts-node-dev": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ts-node-dev/-/ts-node-dev-2.0.0.tgz", + "integrity": "sha512-ywMrhCfH6M75yftYvrvNarLEY+SUXtUvU8/0Z6llrHQVBx12GiFk5sStF8UdfE/yfzk9IAq7O5EEbTQsxlBI8w==", + "dev": true, + "requires": { + "chokidar": "^3.5.1", + "dynamic-dedupe": "^0.3.0", + "minimist": "^1.2.6", + "mkdirp": "^1.0.4", + "resolve": "^1.0.0", + "rimraf": "^2.6.1", + "source-map-support": "^0.5.12", + "tree-kill": "^1.2.2", + "ts-node": "^10.4.0", + "tsconfig": "^7.0.0" + }, + "dependencies": { + "mkdirp": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-1.0.4.tgz", + "integrity": "sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw==", + "dev": true + }, + "rimraf": { + "version": "2.7.1", + "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-2.7.1.tgz", + "integrity": "sha512-uWjbaKIK3T1OSVptzX7Nl6PvQ3qAGtKEtVRjRuazjfL3Bx5eI409VZSqgND+4UNnmzLVdPj9FqFJNPqBZFve4w==", + "dev": true, + "requires": { + "glob": "^7.1.3" + } + } + } + }, + "tsconfig": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/tsconfig/-/tsconfig-7.0.0.tgz", + "integrity": "sha512-vZXmzPrL+EmC4T/4rVlT2jNVMWCi/O4DIiSj3UHg1OE5kCKbk4mfrXc6dZksLgRM/TZlKnousKH9bbTazUWRRw==", + "dev": true, + "requires": { + "@types/strip-bom": "^3.0.0", + "@types/strip-json-comments": "0.0.30", + "strip-bom": "^3.0.0", + "strip-json-comments": "^2.0.0" + }, + "dependencies": { + "strip-bom": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-3.0.0.tgz", + "integrity": "sha512-vavAMRXOgBVNF6nyEEmL3DBK19iRpDcoIwW+swQ+CbGiu7lju6t+JklA1MHweoWtadgt4ISVUsXLyDq34ddcwA==", + "dev": true + }, + "strip-json-comments": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-2.0.1.tgz", + "integrity": "sha512-4gB8na07fecVVkOI6Rs4e7T6NOTki5EmL7TUduTs6bu3EdnSycntVJ4re8kgZA+wx9IueI2Y11bfbgwtzuE0KQ==", + "dev": true + } + } + }, + "tsconfig-paths": { + "version": "3.14.1", + "resolved": "https://registry.npmjs.org/tsconfig-paths/-/tsconfig-paths-3.14.1.tgz", + "integrity": "sha512-fxDhWnFSLt3VuTwtvJt5fpwxBHg5AdKWMsgcPOOIilyjymcYVZoCQF8fvFRezCNfblEXmi+PcM1eYHeOAgXCOQ==", + "dev": true, + "requires": { + "@types/json5": "^0.0.29", + "json5": "^1.0.1", + "minimist": "^1.2.6", + "strip-bom": "^3.0.0" + }, + "dependencies": { + "json5": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/json5/-/json5-1.0.2.tgz", + "integrity": "sha512-g1MWMLBiz8FKi1e4w0UyVL3w+iJceWAFBAaBnnGKOpNa5f8TLktkbre1+s6oICydWAm+HRUGTmI+//xv2hvXYA==", + "dev": true, + "requires": { + "minimist": "^1.2.0" + } + }, + "strip-bom": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-3.0.0.tgz", + "integrity": "sha512-vavAMRXOgBVNF6nyEEmL3DBK19iRpDcoIwW+swQ+CbGiu7lju6t+JklA1MHweoWtadgt4ISVUsXLyDq34ddcwA==", + "dev": true + } + } + }, + "tslib": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz", + "integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==", + "dev": true + }, + "type-check": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz", + "integrity": "sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==", + "dev": true, + "requires": { + "prelude-ls": "^1.2.1" + } + }, + "type-detect": { + "version": "4.0.8", + "resolved": "https://registry.npmjs.org/type-detect/-/type-detect-4.0.8.tgz", + "integrity": "sha512-0fr/mIH1dlO+x7TlcMy+bIDqKPsw/70tVyeHW787goQjhmqaZe10uwLujubK9q9Lg6Fiho1KUKDYz0Z7k7g5/g==", + "dev": true + }, + "type-fest": { + "version": "0.21.3", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.21.3.tgz", + "integrity": "sha512-t0rzBq87m3fVcduHDUFhKmyyX+9eo6WQjZvf51Ea/M0Q7+T374Jp1aUiyUl0GKxp8M/OETVHSDvmkyPgvX+X2w==", + "dev": true + }, + "type-is": { + "version": "1.6.18", + "resolved": "https://registry.npmjs.org/type-is/-/type-is-1.6.18.tgz", + "integrity": "sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g==", + "requires": { + "media-typer": "0.3.0", + "mime-types": "~2.1.24" + } + }, + "typescript": { + "version": "5.2.2", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.2.2.tgz", + "integrity": "sha512-mI4WrpHsbCIcwT9cF4FZvr80QUeKvsUsUvKDoR+X/7XHQH98xYD8YHZg7ANtz2GtZt/CBq2QJ0thkGJMHfqc1w==", + "dev": true + }, + "unbox-primitive": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/unbox-primitive/-/unbox-primitive-1.0.2.tgz", + "integrity": "sha512-61pPlCD9h51VoreyJ0BReideM3MDKMKnh6+V9L08331ipq6Q8OFXZYiqP6n/tbHx4s5I9uRhcye6BrbkizkBDw==", + "dev": true, + "requires": { + "call-bind": "^1.0.2", + "has-bigints": "^1.0.2", + "has-symbols": "^1.0.3", + "which-boxed-primitive": "^1.0.2" + } + }, + "undici-types": { + "version": "5.26.5", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-5.26.5.tgz", + "integrity": "sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA==", + "dev": true + }, + "unpipe": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz", + "integrity": "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==" + }, + "untildify": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/untildify/-/untildify-4.0.0.tgz", + "integrity": "sha512-KK8xQ1mkzZeg9inewmFVDNkg3l5LUhoq9kN6iWYB/CC9YMG8HA+c1Q8HwDe6dEX7kErrEVNVBO3fWsVq5iDgtw==", + "dev": true + }, + "update-browserslist-db": { + "version": "1.0.10", + "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.0.10.tgz", + "integrity": "sha512-OztqDenkfFkbSG+tRxBeAnCVPckDBcvibKd35yDONx6OU8N7sqgwc7rCbkJ/WcYtVRZ4ba68d6byhC21GFh7sQ==", + "dev": true, + "requires": { + "escalade": "^3.1.1", + "picocolors": "^1.0.0" + } + }, + "uri-js": { + "version": "4.4.1", + "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz", + "integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==", + "dev": true, + "requires": { + "punycode": "^2.1.0" + } + }, + "util-deprecate": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", + "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==" + }, + "utils-merge": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/utils-merge/-/utils-merge-1.0.1.tgz", + "integrity": "sha512-pMZTvIkT1d+TFGvDOqodOclx0QWkkgi6Tdoa8gC8ffGAAqz9pzPTZWAybbsHHoED/ztMtkv/VoYTYyShUn81hA==" + }, + "v8-compile-cache": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/v8-compile-cache/-/v8-compile-cache-2.3.0.tgz", + "integrity": "sha512-l8lCEmLcLYZh4nbunNZvQCJc5pv7+RCwa8q/LdUx8u7lsWvPDKmpodJAJNwkAhJC//dFY48KuIEmjtd4RViDrA==", + "dev": true + }, + "v8-compile-cache-lib": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/v8-compile-cache-lib/-/v8-compile-cache-lib-3.0.1.tgz", + "integrity": "sha512-wa7YjyUGfNZngI/vtK0UHAN+lgDCxBPCylVXGp0zu59Fz5aiGtNXaq3DhIov063MorB+VfufLh3JlF2KdTK3xg==" + }, + "v8-to-istanbul": { + "version": "9.1.3", + "resolved": "https://registry.npmjs.org/v8-to-istanbul/-/v8-to-istanbul-9.1.3.tgz", + "integrity": "sha512-9lDD+EVI2fjFsMWXc6dy5JJzBsVTcQ2fVkfBvncZ6xJWG9wtBhOldG+mHkSL0+V1K/xgZz0JDO5UT5hFwHUghg==", + "dev": true, + "requires": { + "@jridgewell/trace-mapping": "^0.3.12", + "@types/istanbul-lib-coverage": "^2.0.1", + "convert-source-map": "^2.0.0" + }, + "dependencies": { + "convert-source-map": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz", + "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==", + "dev": true + } + } + }, + "vary": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz", + "integrity": "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==" + }, + "walker": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/walker/-/walker-1.0.8.tgz", + "integrity": "sha512-ts/8E8l5b7kY0vlWLewOkDXMmPdLcVV4GmOQLyxuSswIJsweeFZtAsMF7k1Nszz+TYBQrlYRmzOnr398y1JemQ==", + "dev": true, + "requires": { + "makeerror": "1.0.12" + } + }, + "which": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", + "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", + "dev": true, + "requires": { + "isexe": "^2.0.0" + } + }, + "which-boxed-primitive": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/which-boxed-primitive/-/which-boxed-primitive-1.0.2.tgz", + "integrity": "sha512-bwZdv0AKLpplFY2KZRX6TvyuN7ojjr7lwkg6ml0roIy9YeuSr7JS372qlNW18UQYzgYK9ziGcerWqZOmEn9VNg==", + "dev": true, + "requires": { + "is-bigint": "^1.0.1", + "is-boolean-object": "^1.1.0", + "is-number-object": "^1.0.4", + "is-string": "^1.0.5", + "is-symbol": "^1.0.3" + } + }, + "winston": { + "version": "3.11.0", + "resolved": "https://registry.npmjs.org/winston/-/winston-3.11.0.tgz", + "integrity": "sha512-L3yR6/MzZAOl0DsysUXHVjOwv8mKZ71TrA/41EIduGpOOV5LQVodqN+QdQ6BS6PJ/RdIshZhq84P/fStEZkk7g==", + "requires": { + "@colors/colors": "^1.6.0", + "@dabh/diagnostics": "^2.0.2", + "async": "^3.2.3", + "is-stream": "^2.0.0", + "logform": "^2.4.0", + "one-time": "^1.0.0", + "readable-stream": "^3.4.0", + "safe-stable-stringify": "^2.3.1", + "stack-trace": "0.0.x", + "triple-beam": "^1.3.0", + "winston-transport": "^4.5.0" + }, + "dependencies": { + "@colors/colors": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/@colors/colors/-/colors-1.6.0.tgz", + "integrity": "sha512-Ir+AOibqzrIsL6ajt3Rz3LskB7OiMVHqltZmspbW/TJuTVuyOMirVqAkjfY6JISiLHgyNqicAC8AyHHGzNd/dA==" + } + } + }, + "winston-transport": { + "version": "4.6.0", + "resolved": "https://registry.npmjs.org/winston-transport/-/winston-transport-4.6.0.tgz", + "integrity": "sha512-wbBA9PbPAHxKiygo7ub7BYRiKxms0tpfU2ljtWzb3SjRjv5yl6Ozuy/TkXf00HTAt+Uylo3gSkNwzc4ME0wiIg==", + "requires": { + "logform": "^2.3.2", + "readable-stream": "^3.6.0", + "triple-beam": "^1.3.0" + } + }, + "word-wrap": { + "version": "1.2.5", + "resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.5.tgz", + "integrity": "sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==", + "dev": true + }, + "wrap-ansi": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", + "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", + "dev": true, + "requires": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + } + }, + "wrappy": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", + "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", + "dev": true + }, + "write": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/write/-/write-1.0.3.tgz", + "integrity": "sha512-/lg70HAjtkUgWPVZhZcm+T4hkL8Zbtp1nFNOn3lRrxnlv50SRBv7cR7RqR+GMsd3hUXy9hWBo4CHTbFTcOYwig==", + "dev": true, + "requires": { + "mkdirp": "^0.5.1" + } + }, + "write-file-atomic": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/write-file-atomic/-/write-file-atomic-4.0.2.tgz", + "integrity": "sha512-7KxauUdBmSdWnmpaGFg+ppNjKF8uNLry8LyzjauQDOVONfFLNKrKvQOxZ/VuTIcS/gge/YNahf5RIIQWTSarlg==", + "dev": true, + "requires": { + "imurmurhash": "^0.1.4", + "signal-exit": "^3.0.7" + } + }, + "xtend": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.2.tgz", + "integrity": "sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ==", + "dev": true + }, + "y18n": { + "version": "5.0.8", + "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz", + "integrity": "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==", + "dev": true + }, + "yallist": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", + "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", + "dev": true + }, + "yargs": { + "version": "17.7.2", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-17.7.2.tgz", + "integrity": "sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w==", + "dev": true, + "requires": { + "cliui": "^8.0.1", + "escalade": "^3.1.1", + "get-caller-file": "^2.0.5", + "require-directory": "^2.1.1", + "string-width": "^4.2.3", + "y18n": "^5.0.5", + "yargs-parser": "^21.1.1" + } + }, + "yargs-parser": { + "version": "21.1.1", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-21.1.1.tgz", + "integrity": "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==", + "dev": true + }, + "yn": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/yn/-/yn-3.1.1.tgz", + "integrity": "sha512-Ux4ygGWsu2c7isFWe8Yu1YluJmqVhxqK2cLXNQA5AcC3QfbGNpM7fu0Y8b/z16pXLnFxZYvWhd3fhBY9DLmC6Q==" + }, + "yocto-queue": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", + "integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==", + "dev": true + } + } +} diff --git a/bcbox/app/package.json b/bcbox/app/package.json new file mode 100644 index 00000000..ef001916 --- /dev/null +++ b/bcbox/app/package.json @@ -0,0 +1,56 @@ +{ + "name": "bcbox-app", + "version": "0.5.0", + "private": true, + "description": "", + "author": "NR Common Service Showcase ", + "license": "Apache-2.0", + "scripts": { + "build": "ts-node ./frontend-utils.ts", + "clean": "rimraf coverage dist", + "clean:all": "npm run clean && ts-node ./frontend-utils.ts clean", + "debug": "ts-node-dev --debug --respawn --transpile-only --rs --watch bin,config,dist ./bin/www", + "format": "prettier ./src --write", + "lint": "eslint . **/www* --no-fix --ext .js,.jsx,.cjs,.mjs,.ts,.tsx,.cts,.mts --ignore-path .gitignore", + "prelint": "npm run typecheck", + "pretest": "npm run lint", + "posttest": "ts-node ./lcov-fix.ts", + "purge": "rimraf node_modules", + "rebuild": "npm run clean && npm run build", + "reinstall": "npm run purge && npm install", + "serve": "ts-node-dev --respawn --transpile-only --rs --watch bin,config,dist ./bin/www", + "start": "ts-node --transpile-only ./bin/www", + "test": "jest --verbose --forceExit --detectOpenHandles", + "typecheck": "tsc --noEmit" + }, + "dependencies": { + "api-problem": "^9.0.2", + "axios": "^1.6.0", + "compression": "^1.7.4", + "config": "^3.3.9", + "express": "^4.18.2", + "express-winston": "^4.2.0", + "ts-node": "^10.9.1", + "winston": "^3.11.0", + "winston-transport": "^4.6.0" + }, + "devDependencies": { + "@types/compression": "^1.7.4", + "@types/config": "^3.3.2", + "@types/express": "^4.17.20", + "@types/jest": "^29.5.7", + "@types/node": "^20.8.10", + "@typescript-eslint/eslint-plugin": "^6.9.1", + "@typescript-eslint/parser": "^6.9.1", + "eslint": "^8.52.0", + "eslint-config-prettier": "^9.0.0", + "eslint-config-recommended": "^4.1.0", + "eslint-plugin-prettier": "^5.0.1", + "jest": "^29.7.0", + "prettier": "^3.0.3", + "rimraf": "^5.0.5", + "ts-jest": "^29.1.1", + "ts-node-dev": "^2.0.0", + "typescript": "^5.2.2" + } +} diff --git a/bcbox/app/src/components/log.ts b/bcbox/app/src/components/log.ts new file mode 100644 index 00000000..faeb7049 --- /dev/null +++ b/bcbox/app/src/components/log.ts @@ -0,0 +1,106 @@ +import config from 'config'; +import { logger } from 'express-winston'; +// const jwt = require('jsonwebtoken'); +import { parse } from 'path'; +import { createLogger, format, transports } from 'winston'; +import Transport from 'winston-transport'; + +import type { Logger } from 'winston'; + +/** + * Class representing a winston transport writing to null + * @extends Transport + */ +export class NullTransport extends Transport { + /** + * Constructor + * @param {object} opts Winston Transport options + */ + constructor(opts: object) { + super(opts); + } + + /** + * The transport logger + * @param {object} _info Object to log + * @param {function} callback Callback function + */ + log(_info: object, callback: () => void) { + callback(); + } +} + +/** + * Main Winston Logger + * @returns {object} Winston Logger + */ +const log = createLogger({ + exitOnError: false, + format: format.combine( + format.errors({ stack: true }), // Force errors to show stacktrace + format.timestamp(), // Add ISO timestamp to each entry + format.json() // Force output to be in JSON format + ), + level: config.get('server.logLevel') +}); + +if (process.env.NODE_ENV !== 'test') { + log.add(new transports.Console({ handleExceptions: true })); +} else { + log.add(new NullTransport({})); +} + +if (config.has('server.logFile')) { + log.add( + new transports.File({ + filename: config.get('server.logFile'), + handleExceptions: true + }) + ); +} + +/** + * Returns a Winston Logger or Child Winston Logger + * @param {string} [filename] Optional module filename path to annotate logs with + * @returns {object} A child logger with appropriate metadata if `filename` is defined. + * Otherwise returns a standard logger. + */ +export const getLogger = (filename: string | undefined): Logger => { + return filename ? log.child({ component: parse(filename).name }) : log; +}; + +/** + * Returns an express-winston middleware function for http logging + * @returns {function} An express-winston middleware function + */ +export const httpLogger = logger({ + colorize: false, + // Parses express information to insert into log output + // eslint-disable-next-line @typescript-eslint/no-explicit-any + dynamicMeta: (req, res: any) => { + // const token = jwt.decode((req.get('authorization') || '').slice(7)); + return { + // azp: token && token.azp || undefined, + contentLength: res.get('content-length'), + httpVersion: req.httpVersion, + ip: req.ip, + method: req.method, + path: req.path, + query: Object.keys(req.query).length ? req.query : undefined, + responseTime: res.responseTime, + statusCode: res.statusCode, + userAgent: req.get('user-agent') + }; + }, + expressFormat: true, // Use express style message strings + level: 'http', + meta: true, // Must be true for dynamicMeta to execute + metaField: null, // Set to null for all attributes to be at top level object + requestWhitelist: [], // Suppress default value output + responseWhitelist: [], // Suppress default value output + // Skip logging kube-probe requests + skip: (req) => !!req.get('user-agent')?.includes('kube-probe'), + winstonInstance: log +}); + +export default getLogger; diff --git a/bcbox/app/src/components/utils.ts b/bcbox/app/src/components/utils.ts new file mode 100644 index 00000000..477ea9da --- /dev/null +++ b/bcbox/app/src/components/utils.ts @@ -0,0 +1,62 @@ +import { existsSync, readFileSync } from 'fs'; +import { join } from 'path'; + +import { getLogger } from './log'; +const log = getLogger(module.filename); + +/** + * @function getGitRevision + * Gets the current git revision hash + * @see {@link https://stackoverflow.com/a/34518749} + * @returns {string} The git revision hash, or empty string + */ +export function getGitRevision(): string { + try { + const gitDir = (() => { + let dir = '.git', + i = 0; + while (!existsSync(join(__dirname, dir)) && i < 5) { + dir = '../' + dir; + i++; + } + return dir; + })(); + + const head = readFileSync(join(__dirname, `${gitDir}/HEAD`), 'utf8') + .toString() + .trim(); + + if (head.indexOf(':') === -1) { + return head; + } else { + return readFileSync(join(__dirname, `${gitDir}/${head.substring(5)}`), 'utf8') + .toString() + .trim(); + } + // eslint-disable-next-line @typescript-eslint/no-explicit-any + } catch (err: any) { + log.warn(err.message, { function: 'getGitRevision' }); + return ''; + } +} + +/** + * @function readIdpList + * Acquires the list of identity providers to be used + * @returns {object[]} A promise resolving to an array of idp provider objects + */ +export function readIdpList(): object[] { + const configDir = '../../config'; + const defaultFile = 'idplist-default.json'; + const overrideFile = 'idplist-local.json'; + + let idpList = []; + + if (existsSync(join(__dirname, configDir, overrideFile))) { + idpList = JSON.parse(readFileSync(join(__dirname, configDir, overrideFile), 'utf8')); + } else if (existsSync(join(__dirname, configDir, defaultFile))) { + idpList = JSON.parse(readFileSync(join(__dirname, configDir, defaultFile), 'utf8')); + } + + return idpList; +} diff --git a/bcbox/app/tests/common/helper.ts b/bcbox/app/tests/common/helper.ts new file mode 100644 index 00000000..314eb0a2 --- /dev/null +++ b/bcbox/app/tests/common/helper.ts @@ -0,0 +1,51 @@ +import express from 'express'; +// @ts-expect-error api-problem lacks a defined interface; code still works fine +import Problem from 'api-problem'; + +import type { Express, Request, Response } from 'express'; + +/** + * @class helper + * Provides helper utilities that are commonly used in tests + */ +const helper = { + /** + * @function expressHelper + * Creates a stripped-down simple Express server object + * @param {string} basePath The path to mount the `router` on + * @param {object} router An express router object to mount + * @returns {object} A simple express server object with `router` mounted to `basePath` + */ + expressHelper: (basePath: string, router: Express): object => { + const app = express(); + + app.use(express.json()); + app.use( + express.urlencoded({ + extended: false + }) + ); + app.use(basePath, router); + + // Handle 500 + // eslint-disable-next-line no-unused-vars, @typescript-eslint/no-unused-vars + app.use((err: Problem, _req: Request, res: Response, _next: () => void): void => { + if (err instanceof Problem) { + err.send(res); + } else { + new Problem(500, { + details: err.message ? err.message : err + }).send(res); + } + }); + + // Handle 404 + app.use((_req, res) => { + new Problem(404).send(res); + }); + + return app; + } +}; + +export default helper; diff --git a/bcbox/app/tests/unit/components/log.spec.ts b/bcbox/app/tests/unit/components/log.spec.ts new file mode 100644 index 00000000..382e59f1 --- /dev/null +++ b/bcbox/app/tests/unit/components/log.spec.ts @@ -0,0 +1,39 @@ +// TODO: Figure out how to prevent this another way? +// Without this export line you'll see the below error, something to do with global variable clashing in TS +// Cannot redeclare block-scoped variable 'config'.ts(2451) +import config from 'config'; +import type { Logger } from 'winston'; + +import getLogger, { httpLogger } from '../../../src/components/log'; + +describe('getLogger', () => { + const assertLogger = (log: Logger): void => { + expect(log).toBeTruthy(); + expect(typeof log).toBe('object'); + expect(typeof log.pipe).toBe('function'); + expect(log.exitOnError).toBeFalsy(); + expect(log.format).toBeTruthy(); + expect(log.level).toBe(config.get('server.logLevel')); + expect(log.transports).toHaveLength(1); + }; + + it('should return a winston logger', () => { + const result = getLogger(undefined); + assertLogger(result); + }); + + it('should return a child winston logger with metadata overrides', () => { + const result = getLogger('test'); + assertLogger(result); + }); +}); + +describe('httpLogger', () => { + it('should return a winston middleware function', () => { + const result = httpLogger; + + expect(result).toBeTruthy(); + expect(typeof result).toBe('function'); + expect(result.length).toBe(3); + }); +}); diff --git a/bcbox/app/tests/unit/components/utils.spec.ts b/bcbox/app/tests/unit/components/utils.spec.ts new file mode 100644 index 00000000..6e3a11ea --- /dev/null +++ b/bcbox/app/tests/unit/components/utils.spec.ts @@ -0,0 +1,21 @@ +import { getGitRevision, readIdpList } from '../../../src/components/utils'; + +beforeEach(() => { + jest.resetAllMocks(); +}); + +afterAll(() => { + jest.restoreAllMocks(); +}); + +describe('getGitRevision', () => { + it('should return a string', () => { + expect(typeof getGitRevision()).toBe('string'); + }); +}); + +describe('readIdpList', () => { + it('should return an array of objects', () => { + expect(Array.isArray(readIdpList())).toBeTruthy(); + }); +}); diff --git a/bcbox/app/tsconfig.json b/bcbox/app/tsconfig.json new file mode 100644 index 00000000..bbb05680 --- /dev/null +++ b/bcbox/app/tsconfig.json @@ -0,0 +1,99 @@ +{ + "compilerOptions": { + /* Visit https://aka.ms/tsconfig to read more about this file */ + /* Projects */ + // "incremental": true, /* Save .tsbuildinfo files to allow for incremental compilation of projects. */ + // "composite": true, /* Enable constraints that allow a TypeScript project to be used with project references. */ + // "tsBuildInfoFile": "./.tsbuildinfo", /* Specify the path to .tsbuildinfo incremental compilation file. */ + // "disableSourceOfProjectReferenceRedirect": true, /* Disable preferring source files instead of declaration files when referencing composite projects. */ + // "disableSolutionSearching": true, /* Opt a project out of multi-project reference checking when editing. */ + // "disableReferencedProjectLoad": true, /* Reduce the number of projects loaded automatically by TypeScript. */ + /* Language and Environment */ + //"target": "" /* Set the JavaScript language version for emitted JavaScript and include compatible library declarations. */, + // "lib": [], /* Specify a set of bundled library declaration files that describe the target runtime environment. */ + // "jsx": "preserve", /* Specify what JSX code is generated. */ + // "experimentalDecorators": true, /* Enable experimental support for TC39 stage 2 draft decorators. */ + // "emitDecoratorMetadata": true, /* Emit design-type metadata for decorated declarations in source files. */ + // "jsxFactory": "", /* Specify the JSX factory function used when targeting React JSX emit, e.g. 'React.createElement' or 'h'. */ + // "jsxFragmentFactory": "", /* Specify the JSX Fragment reference used for fragments when targeting React JSX emit e.g. 'React.Fragment' or 'Fragment'. */ + // "jsxImportSource": "", /* Specify module specifier used to import the JSX factory functions when using 'jsx: react-jsx*'. */ + // "reactNamespace": "", /* Specify the object invoked for 'createElement'. This only applies when targeting 'react' JSX emit. */ + // "noLib": true, /* Disable including any library files, including the default lib.d.ts. */ + // "useDefineForClassFields": true, /* Emit ECMAScript-standard-compliant class fields. */ + // "moduleDetection": "auto", /* Control what method is used to detect module-format JS files. */ + /* Modules */ + "module": "Node16" /* Specify what module code is generated. */, + // "rootDir": "./", /* Specify the root folder within your source files. */ + // "moduleResolution": "node", /* Specify how TypeScript looks up a file from a given module specifier. */ + // "baseUrl": "./", /* Specify the base directory to resolve non-relative module names. */ + // "paths": {}, /* Specify a set of entries that re-map imports to additional lookup locations. */ + // "rootDirs": [], /* Allow multiple folders to be treated as one when resolving modules. */ + // "typeRoots": [], /* Specify multiple folders that act like './node_modules/@types'. */ + // "types": [], /* Specify type package names to be included without being referenced in a source file. */ + // "allowUmdGlobalAccess": true, /* Allow accessing UMD globals from modules. */ + // "moduleSuffixes": [], /* List of file name suffixes to search when resolving a module. */ + // "resolveJsonModule": true, /* Enable importing .json files. */ + // "noResolve": true, /* Disallow 'import's, 'require's or ''s from expanding the number of files TypeScript should add to a project. */ + /* JavaScript Support */ + // "allowJs": true, /* Allow JavaScript files to be a part of your program. Use the 'checkJS' option to get errors from these files. */ + // "checkJs": true, /* Enable error reporting in type-checked JavaScript files. */ + // "maxNodeModuleJsDepth": 1, /* Specify the maximum folder depth used for checking JavaScript files from 'node_modules'. Only applicable with 'allowJs'. */ + /* Emit */ + // "declaration": true, /* Generate .d.ts files from TypeScript and JavaScript files in your project. */ + // "declarationMap": true, /* Create sourcemaps for d.ts files. */ + // "emitDeclarationOnly": true, /* Only output d.ts files and not JavaScript files. */ + // "sourceMap": true, /* Create source map files for emitted JavaScript files. */ + // "outFile": "./", /* Specify a file that bundles all outputs into one JavaScript file. If 'declaration' is true, also designates a file that bundles all .d.ts output. */ + "outDir": "./dist" /* Specify an output folder for all emitted files. */, + // "removeComments": true, /* Disable emitting comments. */ + // "noEmit": true, /* Disable emitting files from a compilation. */ + // "importHelpers": true, /* Allow importing helper functions from tslib once per project, instead of including them per-file. */ + // "importsNotUsedAsValues": "remove", /* Specify emit/checking behavior for imports that are only used for types. */ + // "downlevelIteration": true, /* Emit more compliant, but verbose and less performant JavaScript for iteration. */ + // "sourceRoot": "", /* Specify the root path for debuggers to find the reference source code. */ + // "mapRoot": "", /* Specify the location where debugger should locate map files instead of generated locations. */ + // "inlineSourceMap": true, /* Include sourcemap files inside the emitted JavaScript. */ + // "inlineSources": true, /* Include source code in the sourcemaps inside the emitted JavaScript. */ + // "emitBOM": true, /* Emit a UTF-8 Byte Order Mark (BOM) in the beginning of output files. */ + // "newLine": "crlf", /* Set the newline character for emitting files. */ + // "stripInternal": true, /* Disable emitting declarations that have '@internal' in their JSDoc comments. */ + // "noEmitHelpers": true, /* Disable generating custom helper functions like '__extends' in compiled output. */ + // "noEmitOnError": true, /* Disable emitting files if any type checking errors are reported. */ + // "preserveConstEnums": true, /* Disable erasing 'const enum' declarations in generated code. */ + // "declarationDir": "./", /* Specify the output directory for generated declaration files. */ + "preserveValueImports": false /* Preserve unused imported values in the JavaScript output that would otherwise be removed. */, + /* Interop Constraints */ + // "isolatedModules": true, /* Ensure that each file can be safely transpiled without relying on other imports. */ + // "allowSyntheticDefaultImports": true, /* Allow 'import x from y' when a module doesn't have a default export. */ + "esModuleInterop": true /* Emit additional JavaScript to ease support for importing CommonJS modules. This enables 'allowSyntheticDefaultImports' for type compatibility. */, + // "preserveSymlinks": true, /* Disable resolving symlinks to their realpath. This correlates to the same flag in node. */ + "forceConsistentCasingInFileNames": true /* Ensure that casing is correct in imports. */, + /* Type Checking */ + "strict": true /* Enable all strict type-checking options. */, + // "noImplicitAny": true, /* Enable error reporting for expressions and declarations with an implied 'any' type. */ + // "strictNullChecks": true, /* When type checking, take into account 'null' and 'undefined'. */ + // "strictFunctionTypes": true, /* When assigning functions, check to ensure parameters and the return values are subtype-compatible. */ + // "strictBindCallApply": true, /* Check that the arguments for 'bind', 'call', and 'apply' methods match the original function. */ + // "strictPropertyInitialization": true, /* Check for class properties that are declared but not set in the constructor. */ + // "noImplicitThis": true, /* Enable error reporting when 'this' is given the type 'any'. */ + // "useUnknownInCatchVariables": true, /* Default catch clause variables as 'unknown' instead of 'any'. */ + // "alwaysStrict": true, /* Ensure 'use strict' is always emitted. */ + // "noUnusedLocals": true, /* Enable error reporting when local variables aren't read. */ + // "noUnusedParameters": true, /* Raise an error when a function parameter isn't read. */ + // "exactOptionalPropertyTypes": true, /* Interpret optional property types as written, rather than adding 'undefined'. */ + // "noImplicitReturns": true, /* Enable error reporting for codepaths that do not explicitly return in a function. */ + // "noFallthroughCasesInSwitch": true, /* Enable error reporting for fallthrough cases in switch statements. */ + // "noUncheckedIndexedAccess": true, /* Add 'undefined' to a type when accessed using an index. */ + // "noImplicitOverride": true, /* Ensure overriding members in derived classes are marked with an override modifier. */ + // "noPropertyAccessFromIndexSignature": true, /* Enforces using indexed accessors for keys declared using an indexed type. */ + // "allowUnusedLabels": true, /* Disable error reporting for unused labels. */ + // "allowUnreachableCode": true, /* Disable error reporting for unreachable code. */ + /* Completeness */ + // "skipDefaultLibCheck": true, /* Skip type checking .d.ts files that are included with TypeScript. */ + "skipLibCheck": false /* Skip type checking all .d.ts files. */ + }, + "exclude": [ + "src/tests", + "node_modules" + ] +} diff --git a/bcbox/bcgovpubcode.yaml b/bcbox/bcgovpubcode.yaml new file mode 100644 index 00000000..2fa25e19 --- /dev/null +++ b/bcbox/bcgovpubcode.yaml @@ -0,0 +1,58 @@ +--- +data_management_roles: + data_custodian: Fraser Marshall + product_owner: Sharolyn Hurley +product_external_dependencies: + common_components: + - Common-Object-Management-Service + identity_authorization: + - IDIR + - BceId + - Business-BceId +product_information: + business_capabilities_standard: + - Manage Object Storage Objects (CRUD) + - Manage Object Storage Objects (Search/Filter) + - Manage Object Storage Objects (Permission Management) + - Manage Object Storage Objects (Share publicly) + ministry: + - Water, Land and Resource Stewardship + product_acronym: BCBox + product_description: >- + BCBox is a hosted user interface that serves as a common front-end consumer + of the hosted COMS API and can be used as a simple dropbox to allow file + sharing and permission management using S3. + product_name: BCBox + product_status: maturing + product_urls: + - https://bcbox.nrs.gov.bc.ca + program_area: >- + Natural Resource Information and Digital Services - Development and Digital + Services +product_technology_information: + backend_frameworks: + - name: Express + version: 4.18.2 + backend_languages_version: + - name: JavaScript + version: ecmaVersion 11 / es2020 + ci_cd_tools: + - GitHub-Actions + - Helm + frontend_frameworks: + - name: Vue + version: 3.2.47 + - name: Other + version: Pinia 2.0.32 + - name: Other + version: Primevue (3.23.0) + - name: Other + version: oidc-client-ts (2.2.1) + frontend_languages: + - name: TypeScript + version: 4.8.4 + - name: JavaScript + version: ecmaVersion 11 / es2020 + hosting_platforms: + - Private-Cloud-Openshift +version: 1 diff --git a/bcbox/charts/bcbox/.helmignore b/bcbox/charts/bcbox/.helmignore new file mode 100644 index 00000000..0e8a0eb3 --- /dev/null +++ b/bcbox/charts/bcbox/.helmignore @@ -0,0 +1,23 @@ +# Patterns to ignore when building packages. +# This supports shell glob matching, relative path matching, and +# negation (prefixed with !). Only one pattern per line. +.DS_Store +# Common VCS dirs +.git/ +.gitignore +.bzr/ +.bzrignore +.hg/ +.hgignore +.svn/ +# Common backup files +*.swp +*.bak +*.tmp +*.orig +*~ +# Various IDEs +.project +.idea/ +*.tmproj +.vscode/ diff --git a/bcbox/charts/bcbox/Chart.yaml b/bcbox/charts/bcbox/Chart.yaml new file mode 100644 index 00000000..16aaa875 --- /dev/null +++ b/bcbox/charts/bcbox/Chart.yaml @@ -0,0 +1,42 @@ +apiVersion: v2 +name: bcbox +# This is the chart version. This version number should be incremented each time you make changes +# to the chart and its templates, including the app version. +# Versions are expected to follow Semantic Versioning (https://semver.org/) +version: 0.0.11 +kubeVersion: ">= 1.13.0" +description: A frontend UI for managing access control to S3 Objects +# A chart can be either an 'application' or a 'library' chart. +# Application charts are a collection of templates that can be packaged into versioned archives +# to be deployed. +# Library charts provide useful utilities or functions for the chart developer. They're included as +# a dependency of application charts to inject those utilities and functions into the rendering +# pipeline. Library charts do not define any templates and therefore cannot be deployed. +type: application +keywords: + - nodejs + - javascript + - typescript + - docker + - microservice + - s3 + - document-management + - access-control + - object-storage + - domo + - vue +home: https://bcgov.github.io/bcbox +sources: + - https://github.com/bcgov/bcbox +dependencies: [] +maintainers: + - name: NR Common Service Showcase Team + email: NR.CommonServiceShowcase@gov.bc.ca + url: https://bcgov.github.io/common-service-showcase/team.html +# This is the version number of the application being deployed. This version number should be +# incremented each time you make changes to the application. Versions are not expected to +# follow Semantic Versioning. They should reflect the version the application is using. +# It is recommended to use it with quotes. +appVersion: "0.5.0" +deprecated: false +annotations: {} diff --git a/bcbox/charts/bcbox/README.md b/bcbox/charts/bcbox/README.md new file mode 100644 index 00000000..249cb445 --- /dev/null +++ b/bcbox/charts/bcbox/README.md @@ -0,0 +1,81 @@ +# bcbox + +![Version: 0.0.11](https://img.shields.io/badge/Version-0.0.11-informational?style=flat-square) ![Type: application](https://img.shields.io/badge/Type-application-informational?style=flat-square) ![AppVersion: 0.5.0](https://img.shields.io/badge/AppVersion-0.5.0-informational?style=flat-square) + +A frontend UI for managing access control to S3 Objects + +**Homepage:** + +## Maintainers + +| Name | Email | Url | +| ---- | ------ | --- | +| NR Common Service Showcase Team | | | + +## Source Code + +* + +## Requirements + +Kubernetes: `>= 1.13.0` + +## Values + +| Key | Type | Default | Description | +|-----|------|---------|-------------| +| autoscaling.behavior.scaleDown.policies[0].periodSeconds | int | `120` | | +| autoscaling.behavior.scaleDown.policies[0].type | string | `"Pods"` | | +| autoscaling.behavior.scaleDown.policies[0].value | int | `1` | | +| autoscaling.behavior.scaleDown.selectPolicy | string | `"Max"` | | +| autoscaling.behavior.scaleDown.stabilizationWindowSeconds | int | `120` | | +| autoscaling.behavior.scaleUp.policies[0].periodSeconds | int | `30` | | +| autoscaling.behavior.scaleUp.policies[0].type | string | `"Pods"` | | +| autoscaling.behavior.scaleUp.policies[0].value | int | `2` | | +| autoscaling.behavior.scaleUp.selectPolicy | string | `"Max"` | | +| autoscaling.behavior.scaleUp.stabilizationWindowSeconds | int | `0` | | +| autoscaling.enabled | bool | `false` | | +| autoscaling.maxReplicas | int | `16` | | +| autoscaling.minReplicas | int | `2` | | +| autoscaling.targetCPUUtilizationPercentage | int | `80` | | +| config.configMap.FRONTEND_APIPATH | string | `"api/v1"` | | +| config.configMap.FRONTEND_COMS_APIPATH | string | `nil` | | +| config.configMap.FRONTEND_OIDC_AUTHORITY | string | `nil` | | +| config.configMap.FRONTEND_OIDC_CLIENTID | string | `nil` | | +| config.configMap.SERVER_APIPATH | string | `"/api/v1"` | | +| config.configMap.SERVER_BODYLIMIT | string | `"30mb"` | | +| config.configMap.SERVER_LOGLEVEL | string | `"http"` | | +| config.configMap.SERVER_PORT | string | `"8080"` | | +| config.enabled | bool | `false` | | +| config.releaseScoped | bool | `false` | | +| failurePolicy | string | `"Retry"` | | +| fullnameOverride | string | `nil` | | +| image.pullPolicy | string | `"IfNotPresent"` | | +| image.repository | string | `"ghcr.io/bcgov"` | | +| image.tag | string | `nil` | | +| imagePullSecrets | list | `[]` | | +| nameOverride | string | `nil` | | +| networkPolicy.enabled | bool | `true` | | +| podAnnotations | object | `{}` | | +| podSecurityContext | object | `{}` | | +| replicaCount | int | `2` | | +| resources.limits.cpu | string | `"200m"` | | +| resources.limits.memory | string | `"256Mi"` | | +| resources.requests.cpu | string | `"10m"` | | +| resources.requests.memory | string | `"128Mi"` | | +| route.annotations | object | `{}` | | +| route.enabled | bool | `true` | | +| route.host | string | `"chart-example.local"` | | +| route.tls.insecureEdgeTerminationPolicy | string | `"Redirect"` | | +| route.tls.termination | string | `"edge"` | | +| route.wildcardPolicy | string | `"None"` | | +| securityContext | object | `{}` | | +| service.port | int | `8080` | | +| service.portName | string | `"http"` | | +| service.type | string | `"ClusterIP"` | | +| serviceAccount.annotations | object | `{}` | | +| serviceAccount.enabled | bool | `false` | | +| serviceAccount.name | string | `nil` | | + +---------------------------------------------- +Autogenerated from chart metadata using [helm-docs v1.11.3](https://github.com/norwoodj/helm-docs/releases/v1.11.3) diff --git a/bcbox/charts/bcbox/templates/NOTES.txt b/bcbox/charts/bcbox/templates/NOTES.txt new file mode 100644 index 00000000..f05141ca --- /dev/null +++ b/bcbox/charts/bcbox/templates/NOTES.txt @@ -0,0 +1,24 @@ +{{- $configMapName := printf "%s-%s" (include "bcbox.configname" .) "config" }} +{{- $configMap := (lookup "v1" "ConfigMap" .Release.Namespace $configMapName ) }} +Get the application URL by running these commands: +{{- if .Values.route.enabled }} + http{{ if $.Values.route.tls }}s{{ end }}://{{ .Values.route.host }}{{ .Values.route.path }} +{{- else if contains "NodePort" .Values.service.type }} + export NODE_PORT=$(kubectl get --namespace {{ .Release.Namespace }} -o jsonpath="{.spec.ports[0].nodePort}" services {{ include "bcbox.fullname" . }}) + export NODE_IP=$(kubectl get nodes --namespace {{ .Release.Namespace }} -o jsonpath="{.items[0].status.addresses[0].address}") + echo http://$NODE_IP:$NODE_PORT +{{- else if contains "LoadBalancer" .Values.service.type }} + NOTE: It may take a few minutes for the LoadBalancer IP to be available. + You can watch the status of by running 'kubectl get --namespace {{ .Release.Namespace }} svc -w {{ include "bcbox.fullname" . }}' + export SERVICE_IP=$(kubectl get svc --namespace {{ .Release.Namespace }} {{ include "bcbox.fullname" . }} --template "{{"{{ range (index .status.loadBalancer.ingress 0) }}{{.}}{{ end }}"}}") + echo http://$SERVICE_IP:{{ .Values.service.port }} +{{- else if contains "ClusterIP" .Values.service.type }} + export POD_NAME=$(kubectl get pods --namespace {{ .Release.Namespace }} -l "app.kubernetes.io/name={{ include "bcbox.name" . }},app.kubernetes.io/instance={{ .Release.Name }}" -o jsonpath="{.items[0].metadata.name}") + export CONTAINER_PORT=$(kubectl get pod --namespace {{ .Release.Namespace }} $POD_NAME -o jsonpath="{.spec.containers[0].ports[0].containerPort}") + echo "Visit http://127.0.0.1:8080 to use your application" + kubectl --namespace {{ .Release.Namespace }} port-forward $POD_NAME 8080:$CONTAINER_PORT +{{- end }} +{{- if not $configMap }} + +Make sure that ConfigMap "{{ $configMapName }}" is defined in the namespace; the deployment will fail to run without it! +{{- end }} diff --git a/bcbox/charts/bcbox/templates/_helpers.tpl b/bcbox/charts/bcbox/templates/_helpers.tpl new file mode 100644 index 00000000..c4569773 --- /dev/null +++ b/bcbox/charts/bcbox/templates/_helpers.tpl @@ -0,0 +1,77 @@ +{{/* +Expand the name of the chart. +*/}} +{{- define "bcbox.name" -}} +{{- default .Chart.Name .Values.nameOverride | trunc 63 | trimSuffix "-" }} +{{- end }} + +{{/* +Create a default fully qualified app name. +We truncate at 63 chars because some Kubernetes name fields are limited to this (by the DNS naming spec). +If release name contains chart name it will be used as a full name. +*/}} +{{- define "bcbox.fullname" -}} +{{- if .Values.fullnameOverride }} +{{- .Values.fullnameOverride | trunc 63 | trimSuffix "-" }} +{{- else }} +{{- $name := default .Chart.Name .Values.nameOverride }} +{{- if contains $name .Release.Name }} +{{- .Release.Name | trunc 63 | trimSuffix "-" }} +{{- else }} +{{- printf "%s-%s" $name .Release.Name | trunc 63 | trimSuffix "-" }} +{{- end }} +{{- end }} +{{- end }} + +{{/* +Define the config pattern of the chart based on options. +*/}} +{{- define "bcbox.configname" -}} +{{- if .Values.config.releaseScoped }} +{{- include "bcbox.fullname" . }} +{{- else }} +{{- include "bcbox.name" . }} +{{- end }} +{{- end }} + +{{/* +Create chart name and version as used by the chart label. +*/}} +{{- define "bcbox.chart" -}} +{{- printf "%s-%s" .Chart.Name .Chart.Version | replace "+" "_" | trunc 63 | trimSuffix "-" }} +{{- end }} + +{{/* +Common labels +*/}} +{{- define "bcbox.labels" -}} +helm.sh/chart: {{ include "bcbox.chart" . }} +app: {{ include "bcbox.fullname" . }} +{{ include "bcbox.selectorLabels" . }} +{{- if .Chart.AppVersion }} +app.kubernetes.io/version: {{ .Chart.AppVersion | quote }} +{{- end }} +app.kubernetes.io/component: backend +app.kubernetes.io/managed-by: {{ .Release.Service }} +app.kubernetes.io/part-of: {{ .Release.Name }} +app.openshift.io/runtime: nodejs +{{- end }} + +{{/* +Selector labels +*/}} +{{- define "bcbox.selectorLabels" -}} +app.kubernetes.io/name: {{ include "bcbox.name" . }} +app.kubernetes.io/instance: {{ .Release.Name }} +{{- end }} + +{{/* +Create the name of the service account to use +*/}} +{{- define "bcbox.serviceAccountName" -}} +{{- if .Values.serviceAccount.create }} +{{- default (include "bcbox.fullname" .) .Values.serviceAccount.name }} +{{- else }} +{{- default "default" .Values.serviceAccount.name }} +{{- end }} +{{- end }} diff --git a/bcbox/charts/bcbox/templates/configmap.yaml b/bcbox/charts/bcbox/templates/configmap.yaml new file mode 100644 index 00000000..4c542f17 --- /dev/null +++ b/bcbox/charts/bcbox/templates/configmap.yaml @@ -0,0 +1,12 @@ +{{- if .Values.config.enabled }} +--- +apiVersion: v1 +kind: ConfigMap +metadata: + name: {{ include "bcbox.configname" . }}-config + {{- if not .Values.config.releaseScoped }} + annotations: + "helm.sh/resource-policy": keep + {{- end }} +data: {{ toYaml .Values.config.configMap | nindent 2 }} +{{- end }} diff --git a/bcbox/charts/bcbox/templates/deploymentconfig.yaml b/bcbox/charts/bcbox/templates/deploymentconfig.yaml new file mode 100644 index 00000000..132dcb4d --- /dev/null +++ b/bcbox/charts/bcbox/templates/deploymentconfig.yaml @@ -0,0 +1,69 @@ +--- +apiVersion: apps.openshift.io/v1 +kind: DeploymentConfig +metadata: + name: {{ include "bcbox.fullname" . }} + labels: + {{- include "bcbox.labels" . | nindent 4 }} +spec: + replicas: {{ .Values.replicaCount }} + revisionHistoryLimit: 10 + selector: + {{- include "bcbox.selectorLabels" . | nindent 4 }} + strategy: + resources: + {{- toYaml .Values.resources | nindent 6 }} + rollingParams: + timeoutSeconds: 600 + type: Rolling + template: + metadata: + labels: {{ include "bcbox.selectorLabels" . | nindent 8 }} + spec: + {{- with .Values.imagePullSecrets }} + imagePullSecrets: {{ toYaml . | nindent 8 }} + {{- end }} + {{- if .Values.serviceAccount.create }} + serviceAccountName: {{ include "bcbox.serviceAccountName" . }} + {{- end }} + {{- with .Values.podSecurityContext }} + securityContext: {{ toYaml . | nindent 8 }} + {{- end }} + containers: + - name: app + {{- with .Values.securityContext }} + securityContext: {{ toYaml . | nindent 12 }} + {{- end }} + image: "{{ .Values.image.repository }}/{{ .Chart.Name }}:{{ .Values.image.tag | default .Chart.AppVersion }}" + imagePullPolicy: {{ .Values.image.pullPolicy }} + ports: + - containerPort: {{ .Values.service.port }} + protocol: TCP + livenessProbe: + failureThreshold: 3 + httpGet: + path: {{ .Values.route.path }} + port: {{ .Values.service.port }} + scheme: HTTP + initialDelaySeconds: 10 + timeoutSeconds: 1 + readinessProbe: + failureThreshold: 3 + httpGet: + path: {{ .Values.route.path }} + port: {{ .Values.service.port }} + scheme: HTTP + initialDelaySeconds: 10 + timeoutSeconds: 1 + resources: {{ toYaml .Values.resources | nindent 12 }} + env: + - name: NODE_ENV + value: production + envFrom: + - configMapRef: + name: {{ include "bcbox.configname" . }}-config + restartPolicy: Always + terminationGracePeriodSeconds: 30 + test: false + triggers: + - type: ConfigChange diff --git a/bcbox/charts/bcbox/templates/hpa.yaml b/bcbox/charts/bcbox/templates/hpa.yaml new file mode 100644 index 00000000..559ecb5b --- /dev/null +++ b/bcbox/charts/bcbox/templates/hpa.yaml @@ -0,0 +1,37 @@ +{{- if .Values.autoscaling.enabled }} +--- +apiVersion: autoscaling/v2 +kind: HorizontalPodAutoscaler +metadata: + name: {{ include "bcbox.fullname" . }} + labels: + {{- include "bcbox.labels" . | nindent 4 }} +spec: + scaleTargetRef: + apiVersion: apps.openshift.io/v1 + kind: DeploymentConfig + name: {{ include "bcbox.fullname" . }} + minReplicas: {{ .Values.autoscaling.minReplicas }} + maxReplicas: {{ .Values.autoscaling.maxReplicas }} + metrics: + {{- if .Values.autoscaling.targetCPUUtilizationPercentage }} + - type: Resource + resource: + name: cpu + target: + type: Utilization + averageUtilization: {{ .Values.autoscaling.targetCPUUtilizationPercentage }} + {{- end }} + {{- if .Values.autoscaling.targetMemoryUtilizationPercentage }} + - type: Resource + resource: + name: memory + target: + type: Utilization + averageUtilization: {{ .Values.autoscaling.targetMemoryUtilizationPercentage }} + {{- end }} + {{- with .Values.autoscaling.behavior }} + behavior: + {{- toYaml . | nindent 4 }} + {{- end }} +{{- end }} diff --git a/bcbox/charts/bcbox/templates/networkpolicy.yaml b/bcbox/charts/bcbox/templates/networkpolicy.yaml new file mode 100644 index 00000000..ab1bd2cd --- /dev/null +++ b/bcbox/charts/bcbox/templates/networkpolicy.yaml @@ -0,0 +1,22 @@ +{{- if .Values.networkPolicy.enabled }} +--- +apiVersion: networking.k8s.io/v1 +kind: NetworkPolicy +metadata: + name: allow-openshift-ingress-to-{{ include "bcbox.fullname" . }}-app + labels: + {{- include "bcbox.labels" . | nindent 4 }} +spec: + ingress: + - from: + - namespaceSelector: + matchLabels: + network.openshift.io/policy-group: ingress + - podSelector: + matchLabels: {{ include "bcbox.selectorLabels" . | nindent 14 }} + ports: + - port: {{ default "8080" .Values.config.configMap.SERVER_PORT | atoi }} + protocol: TCP + podSelector: + matchLabels: {{- include "bcbox.selectorLabels" . | nindent 6 }} +{{- end }} diff --git a/bcbox/charts/bcbox/templates/route.yaml b/bcbox/charts/bcbox/templates/route.yaml new file mode 100644 index 00000000..a1b56f51 --- /dev/null +++ b/bcbox/charts/bcbox/templates/route.yaml @@ -0,0 +1,28 @@ +{{- if .Values.route.enabled -}} +--- +apiVersion: route.openshift.io/v1 +kind: Route +metadata: + name: {{ include "bcbox.fullname" . }} + labels: + {{- include "bcbox.labels" . | nindent 4 }} + {{- with .Values.route.annotations }} + annotations: + {{- toYaml . | nindent 4 }} + {{- end }} +spec: + host: {{ .Values.route.host | quote }} + {{- if .Values.route.path }} + path: {{ .Values.route.path }} + {{- end }} + port: + targetPort: {{ .Values.service.portName }} + tls: + insecureEdgeTerminationPolicy: {{ .Values.route.tls.insecureEdgeTerminationPolicy }} + termination: {{ .Values.route.tls.termination }} + to: + kind: Service + name: {{ include "bcbox.fullname" . }} + weight: 100 + wildcardPolicy: {{ .Values.route.wildcardPolicy }} +{{- end }} diff --git a/bcbox/charts/bcbox/templates/service.yaml b/bcbox/charts/bcbox/templates/service.yaml new file mode 100644 index 00000000..765b9231 --- /dev/null +++ b/bcbox/charts/bcbox/templates/service.yaml @@ -0,0 +1,16 @@ +--- +apiVersion: v1 +kind: Service +metadata: + name: {{ include "bcbox.fullname" . }} + labels: + {{- include "bcbox.labels" . | nindent 4 }} +spec: + type: {{ .Values.service.type }} + ports: + - name: {{ .Values.service.portName }} + port: {{ .Values.service.port }} + protocol: TCP + targetPort: {{ .Values.service.port }} + selector: + {{- include "bcbox.selectorLabels" . | nindent 4 }} diff --git a/bcbox/charts/bcbox/templates/serviceaccount.yaml b/bcbox/charts/bcbox/templates/serviceaccount.yaml new file mode 100644 index 00000000..f8c092f8 --- /dev/null +++ b/bcbox/charts/bcbox/templates/serviceaccount.yaml @@ -0,0 +1,13 @@ +{{- if .Values.serviceAccount.enabled -}} +--- +apiVersion: v1 +kind: ServiceAccount +metadata: + name: {{ include "bcbox.serviceAccountName" . }} + labels: + {{- include "bcbox.labels" . | nindent 4 }} + {{- with .Values.serviceAccount.annotations }} + annotations: + {{- toYaml . | nindent 4 }} + {{- end }} +{{- end }} diff --git a/bcbox/charts/bcbox/values.yaml b/bcbox/charts/bcbox/values.yaml new file mode 100644 index 00000000..ff1e6490 --- /dev/null +++ b/bcbox/charts/bcbox/values.yaml @@ -0,0 +1,126 @@ +# Default values for bcbox. +# This is a YAML-formatted file. +# Declare variables to be passed into your templates. + +replicaCount: 2 + +image: + repository: ghcr.io/bcgov + pullPolicy: IfNotPresent + # Overrides the image tag whose default is the chart appVersion. + tag: ~ + +imagePullSecrets: [] +nameOverride: ~ +fullnameOverride: ~ + +# DeploymentConfig pre-hook failure behavior +failurePolicy: Retry + +podAnnotations: {} + +podSecurityContext: {} + # fsGroup: 2000 + +securityContext: {} + # capabilities: + # drop: + # - ALL + # readOnlyRootFilesystem: true + # runAsNonRoot: true + # runAsUser: 1000 + +autoscaling: + enabled: false + + # Directly modify scaling behavior and frequency + behavior: + scaleDown: + stabilizationWindowSeconds: 120 + selectPolicy: Max + policies: + - type: Pods + value: 1 + periodSeconds: 120 + scaleUp: + stabilizationWindowSeconds: 0 + selectPolicy: Max + policies: + - type: Pods + value: 2 + periodSeconds: 30 + minReplicas: 2 + maxReplicas: 16 + targetCPUUtilizationPercentage: 80 + # targetMemoryUtilizationPercentage: 80 + +serviceAccount: + # Specifies whether a service account should be created + enabled: false + # Annotations to add to the service account + annotations: {} + # The name of the service account to use. + # If not set and create is true, a name is generated using the fullname template + name: ~ + +networkPolicy: + enabled: true + +service: + type: ClusterIP + port: 8080 + portName: http + +route: + enabled: true + annotations: {} + # kubernetes.io/ingress.class: nginx + # kubernetes.io/tls-acme: "true" + host: chart-example.local + # path: / + tls: + insecureEdgeTerminationPolicy: Redirect + termination: edge + wildcardPolicy: None + +resources: + # We usually recommend not to specify default resources and to leave this as a conscious + # choice for the user. This also increases chances charts run on environments with little + # resources, such as Minikube. If you do want to specify resources, uncomment the following + # lines, adjust them as necessary, and remove the curly braces after 'resources:'. + limits: + cpu: 200m + memory: 256Mi + requests: + cpu: 10m + memory: 128Mi + +config: + # Set to true if you want to let Helm manage and overwrite your configmaps. + enabled: false + + # This should be set to true if and only if you require configmaps and secrets to be release + # scoped. In the event you want all instances in the same namespace to share a similar + # configuration, this should be set to false + releaseScoped: false + + # These values will be wholesale added to the configmap as is; refer to the bcbox + # documentation for what each of these values mean and whether you need them defined. + # Ensure that all values are represented explicitly as strings, as non-string values will + # not translate over as expected into container environment variables. + # For configuration keys named `*_ENABLED`, either leave them commented/undefined, or set them + # to string value "true". + configMap: + FRONTEND_APIPATH: "api/v1" + FRONTEND_COMS_APIPATH: ~ + # FRONTEND_EXCLUDE_METADATA: ~ + # FRONTEND_EXCLUDE_TAGSET: ~ + # FRONTEND_NOTIFICATION_BANNER: ~ + FRONTEND_OIDC_AUTHORITY: ~ + FRONTEND_OIDC_CLIENTID: ~ + SERVER_APIPATH: "/api/v1" + SERVER_BODYLIMIT: "30mb" + # SERVER_STATICFILES: ~ + # SERVER_LOGFILE: ~ + SERVER_LOGLEVEL: "http" + SERVER_PORT: "8080" diff --git a/bcbox/frontend/.eslintrc.js b/bcbox/frontend/.eslintrc.js new file mode 100644 index 00000000..b31e8ad2 --- /dev/null +++ b/bcbox/frontend/.eslintrc.js @@ -0,0 +1,56 @@ +module.exports = { + root: true, + env: { + browser: true, + es2020: true + }, + extends: [ + 'prettier', + 'eslint:recommended', + '@vue/eslint-config-typescript', + 'plugin:vue/vue3-recommended', + 'plugin:vitest-globals/recommended' + ], + overrides: [ + { + files: ['**/*.{test,spec}.{js,mjs,cjs,ts,mts,cts,jsx,tsx,vue}'], + env: { + 'vitest-globals/env': true + } + } + ], + plugins: [], + rules: { + 'eol-last': ['error', 'always'], + indent: [ + 'error', + 2, + { + SwitchCase: 1 + } + ], + 'linebreak-style': ['error', 'unix'], + 'max-len': ['warn', { code: 120, comments: 120, ignoreUrls: true }], + 'no-console': process.env.NODE_ENV === 'production' ? 'error' : 'warn', + 'no-debugger': process.env.NODE_ENV === 'production' ? 'error' : 'warn', + quotes: ['error', 'single'], + semi: ['error', 'always'], + 'vue/component-tags-order': [ + 'error', + { + order: ['script', 'template', 'style'] + } + ], + 'vue/html-self-closing': [ + 'error', + { + html: { + void: 'any' + } + } + ], + 'vue/html-indent': 'off', + 'vue/multi-word-component-names': process.env.NODE_ENV === 'production' ? 'warn' : 'off', + 'vue/singleline-html-element-content-newline': 'off' + } +}; diff --git a/bcbox/frontend/.gitignore b/bcbox/frontend/.gitignore new file mode 100644 index 00000000..38adffa6 --- /dev/null +++ b/bcbox/frontend/.gitignore @@ -0,0 +1,28 @@ +# Logs +logs +*.log +npm-debug.log* +yarn-debug.log* +yarn-error.log* +pnpm-debug.log* +lerna-debug.log* + +node_modules +.DS_Store +dist +dist-ssr +coverage +*.local + +/cypress/videos/ +/cypress/screenshots/ + +# Editor directories and files +.vscode/* +!.vscode/extensions.json +.idea +*.suo +*.ntvs* +*.njsproj +*.sln +*.sw? diff --git a/bcbox/frontend/.prettierignore b/bcbox/frontend/.prettierignore new file mode 100644 index 00000000..53d7d115 --- /dev/null +++ b/bcbox/frontend/.prettierignore @@ -0,0 +1,29 @@ +# Logs +logs +*.log +npm-debug.log* +yarn-debug.log* +yarn-error.log* +pnpm-debug.log* +lerna-debug.log* + +build +node_modules +.DS_Store +dist +dist-ssr +coverage +*.local + +/cypress/videos/ +/cypress/screenshots/ + +# Editor directories and files +.vscode/* +!.vscode/extensions.json +.idea +*.suo +*.ntvs* +*.njsproj +*.sln +*.sw? diff --git a/bcbox/frontend/.prettierrc b/bcbox/frontend/.prettierrc new file mode 100644 index 00000000..e5f42d76 --- /dev/null +++ b/bcbox/frontend/.prettierrc @@ -0,0 +1,13 @@ +{ + "arrowParens": "always", + "bracketSameLine": false, + "bracketSpacing": true, + "htmlWhitespaceSensitivity": "ignore", + "printWidth": 120, + "semi": true, + "singleAttributePerLine": true, + "singleQuote": true, + "tabWidth": 2, + "trailingComma": "none", + "useTabs": false +} diff --git a/bcbox/frontend/README.md b/bcbox/frontend/README.md new file mode 100644 index 00000000..3a7949ef --- /dev/null +++ b/bcbox/frontend/README.md @@ -0,0 +1,46 @@ +# app + +This template should help get you started developing with Vue 3 in Vite. + +## Recommended IDE Setup + +[VSCode](https://code.visualstudio.com/) + [Volar](https://marketplace.visualstudio.com/items?itemName=Vue.volar) (and disable Vetur) + [TypeScript Vue Plugin (Volar)](https://marketplace.visualstudio.com/items?itemName=Vue.vscode-typescript-vue-plugin). + +## Type Support for `.vue` Imports in TS + +TypeScript cannot handle type information for `.vue` imports by default, so we replace the `tsc` CLI with `vue-tsc` for type checking. In editors, we need [TypeScript Vue Plugin (Volar)](https://marketplace.visualstudio.com/items?itemName=Vue.vscode-typescript-vue-plugin) to make the TypeScript language service aware of `.vue` types. + +If the standalone TypeScript plugin doesn't feel fast enough to you, Volar has also implemented a [Take Over Mode](https://github.com/johnsoncodehk/volar/discussions/471#discussioncomment-1361669) that is more performant. You can enable it by the following steps: + +1. Disable the built-in TypeScript Extension + 1) Run `Extensions: Show Built-in Extensions` from VSCode's command palette + 2) Find `TypeScript and JavaScript Language Features`, right click and select `Disable (Workspace)` +2. Reload the VSCode window by running `Developer: Reload Window` from the command palette. + +## Customize configuration + +See [Vite Configuration Reference](https://vitejs.dev/config/). + +## Project Setup + +```sh +npm install +``` + +### Compile and Hot-Reload for Development + +```sh +npm run dev +``` + +### Type-Check, Compile and Minify for Production + +```sh +npm run build +``` + +### Lint with [ESLint](https://eslint.org/) + +```sh +npm run lint +``` diff --git a/bcbox/frontend/env.d.ts b/bcbox/frontend/env.d.ts new file mode 100644 index 00000000..11f02fe2 --- /dev/null +++ b/bcbox/frontend/env.d.ts @@ -0,0 +1 @@ +/// diff --git a/bcbox/frontend/index.html b/bcbox/frontend/index.html new file mode 100644 index 00000000..c1b9063c --- /dev/null +++ b/bcbox/frontend/index.html @@ -0,0 +1,13 @@ + + + + + + + BCBox + + +
+ + + diff --git a/bcbox/frontend/package-lock.json b/bcbox/frontend/package-lock.json new file mode 100644 index 00000000..22cb29e3 --- /dev/null +++ b/bcbox/frontend/package-lock.json @@ -0,0 +1,12111 @@ +{ + "name": "bcbox-frontend", + "version": "0.5.0", + "lockfileVersion": 2, + "requires": true, + "packages": { + "": { + "name": "bcbox-frontend", + "version": "0.5.0", + "license": "Apache-2.0", + "dependencies": { + "@bcgov/bc-sans": "^2.1.0", + "@fortawesome/fontawesome-svg-core": "^6.4.2", + "@fortawesome/free-solid-svg-icons": "^6.4.2", + "@fortawesome/vue-fontawesome": "^3.0.3", + "axios": "^1.6.0", + "date-fns": "^2.30.0", + "filesize": "^10.1.0", + "oidc-client-ts": "^2.4.0", + "pinia": "^2.1.7", + "pinia-plugin-persistedstate": "^3.2.0", + "primeflex": "^3.3.1", + "primeicons": "^6.0.1", + "primevue": "~3.34.1", + "qrcode.vue": "^3.4.1", + "vee-validate": "^4.11.8", + "vue": "^3.3.7", + "vue-router": "^4.2.5", + "yup": "^1.3.2" + }, + "devDependencies": { + "@pinia/testing": "^0.1.3", + "@testing-library/vue": "^8.0.0", + "@tsconfig/node18": "^18.2.2", + "@types/node": "^20.8.10", + "@vitejs/plugin-vue": "^4.4.0", + "@vitejs/plugin-vue-jsx": "^3.0.2", + "@vitest/coverage-c8": "^0.33.0", + "@vitest/coverage-istanbul": "^0.34.6", + "@vue/eslint-config-typescript": "^12.0.0", + "@vue/test-utils": "^2.4.1", + "@vue/tsconfig": "^0.4.0", + "eslint": "^8.52.0", + "eslint-config-prettier": "^9.0.0", + "eslint-plugin-vitest-globals": "^1.4.0", + "eslint-plugin-vue": "^9.18.1", + "happy-dom": "^12.10.3", + "prettier": "3.0.3", + "rimraf": "^5.0.5", + "sass": "^1.69.5", + "ts-node": "^10.9.1", + "typescript": "^5.2.2", + "vite": "^4.5.0", + "vitest": "^0.34.6", + "volar-service-eslint": "^0.0.16", + "vue-tsc": "^1.8.22" + } + }, + "node_modules/@aashutoshrathi/word-wrap": { + "version": "1.2.6", + "resolved": "https://registry.npmjs.org/@aashutoshrathi/word-wrap/-/word-wrap-1.2.6.tgz", + "integrity": "sha512-1Yjs2SvM8TflER/OD3cOjhWWOZb58A2t7wpE2S9XfBYTiIl+XFhQG2bjy4Pu1I+EAlCNUzRDYDdFwFYUKvXcIA==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/@ampproject/remapping": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/@ampproject/remapping/-/remapping-2.2.1.tgz", + "integrity": "sha512-lFMjJTrFL3j7L9yBxwYfCq2k6qqwHyzuUl/XBnif78PWTJYyL/dfowQHWE3sp6U6ZzqWiiIZnpTMO96zhkjwtg==", + "dev": true, + "dependencies": { + "@jridgewell/gen-mapping": "^0.3.0", + "@jridgewell/trace-mapping": "^0.3.9" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@babel/code-frame": { + "version": "7.22.13", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.22.13.tgz", + "integrity": "sha512-XktuhWlJ5g+3TJXc5upd9Ks1HutSArik6jf2eAjYFyIOf4ej3RN+184cZbzDvbPnuTJIUhPKKJE3cIsYTiAT3w==", + "dev": true, + "dependencies": { + "@babel/highlight": "^7.22.13", + "chalk": "^2.4.2" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/compat-data": { + "version": "7.22.9", + "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.22.9.tgz", + "integrity": "sha512-5UamI7xkUcJ3i9qVDS+KFDEK8/7oJ55/sJMB1Ge7IEapr7KfdfV/HErR+koZwOfd+SgtFKOKRhRakdg++DcJpQ==", + "dev": true, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/core": { + "version": "7.22.11", + "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.22.11.tgz", + "integrity": "sha512-lh7RJrtPdhibbxndr6/xx0w8+CVlY5FJZiaSz908Fpy+G0xkBFTvwLcKJFF4PJxVfGhVWNebikpWGnOoC71juQ==", + "dev": true, + "dependencies": { + "@ampproject/remapping": "^2.2.0", + "@babel/code-frame": "^7.22.10", + "@babel/generator": "^7.22.10", + "@babel/helper-compilation-targets": "^7.22.10", + "@babel/helper-module-transforms": "^7.22.9", + "@babel/helpers": "^7.22.11", + "@babel/parser": "^7.22.11", + "@babel/template": "^7.22.5", + "@babel/traverse": "^7.22.11", + "@babel/types": "^7.22.11", + "convert-source-map": "^1.7.0", + "debug": "^4.1.0", + "gensync": "^1.0.0-beta.2", + "json5": "^2.2.3", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/babel" + } + }, + "node_modules/@babel/generator": { + "version": "7.23.0", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.23.0.tgz", + "integrity": "sha512-lN85QRR+5IbYrMWM6Y4pE/noaQtg4pNiqeNGX60eqOfo6gtEj6uw/JagelB8vVztSd7R6M5n1+PQkDbHbBRU4g==", + "dev": true, + "dependencies": { + "@babel/types": "^7.23.0", + "@jridgewell/gen-mapping": "^0.3.2", + "@jridgewell/trace-mapping": "^0.3.17", + "jsesc": "^2.5.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-annotate-as-pure": { + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/helper-annotate-as-pure/-/helper-annotate-as-pure-7.22.5.tgz", + "integrity": "sha512-LvBTxu8bQSQkcyKOU+a1btnNFQ1dMAd0R6PyW3arXes06F6QLWLIrd681bxRPIXlrMGR3XYnW9JyML7dP3qgxg==", + "dev": true, + "dependencies": { + "@babel/types": "^7.22.5" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-compilation-targets": { + "version": "7.22.10", + "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.22.10.tgz", + "integrity": "sha512-JMSwHD4J7SLod0idLq5PKgI+6g/hLD/iuWBq08ZX49xE14VpVEojJ5rHWptpirV2j020MvypRLAXAO50igCJ5Q==", + "dev": true, + "dependencies": { + "@babel/compat-data": "^7.22.9", + "@babel/helper-validator-option": "^7.22.5", + "browserslist": "^4.21.9", + "lru-cache": "^5.1.1", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-compilation-targets/node_modules/lru-cache": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz", + "integrity": "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==", + "dev": true, + "dependencies": { + "yallist": "^3.0.2" + } + }, + "node_modules/@babel/helper-compilation-targets/node_modules/yallist": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz", + "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==", + "dev": true + }, + "node_modules/@babel/helper-create-class-features-plugin": { + "version": "7.22.11", + "resolved": "https://registry.npmjs.org/@babel/helper-create-class-features-plugin/-/helper-create-class-features-plugin-7.22.11.tgz", + "integrity": "sha512-y1grdYL4WzmUDBRGK0pDbIoFd7UZKoDurDzWEoNMYoj1EL+foGRQNyPWDcC+YyegN5y1DUsFFmzjGijB3nSVAQ==", + "dev": true, + "dependencies": { + "@babel/helper-annotate-as-pure": "^7.22.5", + "@babel/helper-environment-visitor": "^7.22.5", + "@babel/helper-function-name": "^7.22.5", + "@babel/helper-member-expression-to-functions": "^7.22.5", + "@babel/helper-optimise-call-expression": "^7.22.5", + "@babel/helper-replace-supers": "^7.22.9", + "@babel/helper-skip-transparent-expression-wrappers": "^7.22.5", + "@babel/helper-split-export-declaration": "^7.22.6", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/helper-environment-visitor": { + "version": "7.22.20", + "resolved": "https://registry.npmjs.org/@babel/helper-environment-visitor/-/helper-environment-visitor-7.22.20.tgz", + "integrity": "sha512-zfedSIzFhat/gFhWfHtgWvlec0nqB9YEIVrpuwjruLlXfUSnA8cJB0miHKwqDnQ7d32aKo2xt88/xZptwxbfhA==", + "dev": true, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-function-name": { + "version": "7.23.0", + "resolved": "https://registry.npmjs.org/@babel/helper-function-name/-/helper-function-name-7.23.0.tgz", + "integrity": "sha512-OErEqsrxjZTJciZ4Oo+eoZqeW9UIiOcuYKRJA4ZAgV9myA+pOXhhmpfNCKjEH/auVfEYVFJ6y1Tc4r0eIApqiw==", + "dev": true, + "dependencies": { + "@babel/template": "^7.22.15", + "@babel/types": "^7.23.0" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-hoist-variables": { + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/helper-hoist-variables/-/helper-hoist-variables-7.22.5.tgz", + "integrity": "sha512-wGjk9QZVzvknA6yKIUURb8zY3grXCcOZt+/7Wcy8O2uctxhplmUPkOdlgoNhmdVee2c92JXbf1xpMtVNbfoxRw==", + "dev": true, + "dependencies": { + "@babel/types": "^7.22.5" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-member-expression-to-functions": { + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/helper-member-expression-to-functions/-/helper-member-expression-to-functions-7.22.5.tgz", + "integrity": "sha512-aBiH1NKMG0H2cGZqspNvsaBe6wNGjbJjuLy29aU+eDZjSbbN53BaxlpB02xm9v34pLTZ1nIQPFYn2qMZoa5BQQ==", + "dev": true, + "dependencies": { + "@babel/types": "^7.22.5" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-module-imports": { + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.22.5.tgz", + "integrity": "sha512-8Dl6+HD/cKifutF5qGd/8ZJi84QeAKh+CEe1sBzz8UayBBGg1dAIJrdHOcOM5b2MpzWL2yuotJTtGjETq0qjXg==", + "dev": true, + "dependencies": { + "@babel/types": "^7.22.5" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-module-transforms": { + "version": "7.22.9", + "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.22.9.tgz", + "integrity": "sha512-t+WA2Xn5K+rTeGtC8jCsdAH52bjggG5TKRuRrAGNM/mjIbO4GxvlLMFOEz9wXY5I2XQ60PMFsAG2WIcG82dQMQ==", + "dev": true, + "dependencies": { + "@babel/helper-environment-visitor": "^7.22.5", + "@babel/helper-module-imports": "^7.22.5", + "@babel/helper-simple-access": "^7.22.5", + "@babel/helper-split-export-declaration": "^7.22.6", + "@babel/helper-validator-identifier": "^7.22.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/helper-optimise-call-expression": { + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/helper-optimise-call-expression/-/helper-optimise-call-expression-7.22.5.tgz", + "integrity": "sha512-HBwaojN0xFRx4yIvpwGqxiV2tUfl7401jlok564NgB9EHS1y6QT17FmKWm4ztqjeVdXLuC4fSvHc5ePpQjoTbw==", + "dev": true, + "dependencies": { + "@babel/types": "^7.22.5" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-plugin-utils": { + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.22.5.tgz", + "integrity": "sha512-uLls06UVKgFG9QD4OeFYLEGteMIAa5kpTPcFL28yuCIIzsf6ZyKZMllKVOCZFhiZ5ptnwX4mtKdWCBE/uT4amg==", + "dev": true, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-replace-supers": { + "version": "7.22.9", + "resolved": "https://registry.npmjs.org/@babel/helper-replace-supers/-/helper-replace-supers-7.22.9.tgz", + "integrity": "sha512-LJIKvvpgPOPUThdYqcX6IXRuIcTkcAub0IaDRGCZH0p5GPUp7PhRU9QVgFcDDd51BaPkk77ZjqFwh6DZTAEmGg==", + "dev": true, + "dependencies": { + "@babel/helper-environment-visitor": "^7.22.5", + "@babel/helper-member-expression-to-functions": "^7.22.5", + "@babel/helper-optimise-call-expression": "^7.22.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/helper-simple-access": { + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/helper-simple-access/-/helper-simple-access-7.22.5.tgz", + "integrity": "sha512-n0H99E/K+Bika3++WNL17POvo4rKWZ7lZEp1Q+fStVbUi8nxPQEBOlTmCOxW/0JsS56SKKQ+ojAe2pHKJHN35w==", + "dev": true, + "dependencies": { + "@babel/types": "^7.22.5" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-skip-transparent-expression-wrappers": { + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/helper-skip-transparent-expression-wrappers/-/helper-skip-transparent-expression-wrappers-7.22.5.tgz", + "integrity": "sha512-tK14r66JZKiC43p8Ki33yLBVJKlQDFoA8GYN67lWCDCqoL6EMMSuM9b+Iff2jHaM/RRFYl7K+iiru7hbRqNx8Q==", + "dev": true, + "dependencies": { + "@babel/types": "^7.22.5" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-split-export-declaration": { + "version": "7.22.6", + "resolved": "https://registry.npmjs.org/@babel/helper-split-export-declaration/-/helper-split-export-declaration-7.22.6.tgz", + "integrity": "sha512-AsUnxuLhRYsisFiaJwvp1QF+I3KjD5FOxut14q/GzovUe6orHLesW2C7d754kRm53h5gqrz6sFl6sxc4BVtE/g==", + "dev": true, + "dependencies": { + "@babel/types": "^7.22.5" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-string-parser": { + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.22.5.tgz", + "integrity": "sha512-mM4COjgZox8U+JcXQwPijIZLElkgEpO5rsERVDJTc2qfCDfERyob6k5WegS14SX18IIjv+XD+GrqNumY5JRCDw==", + "dev": true, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-identifier": { + "version": "7.22.20", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.22.20.tgz", + "integrity": "sha512-Y4OZ+ytlatR8AI+8KZfKuL5urKp7qey08ha31L8b3BwewJAoJamTzyvxPR/5D+KkdJCGPq/+8TukHBlY10FX9A==", + "dev": true, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-option": { + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.22.5.tgz", + "integrity": "sha512-R3oB6xlIVKUnxNUxbmgq7pKjxpru24zlimpE8WK47fACIlM0II/Hm1RS8IaOI7NgCr6LNS+jl5l75m20npAziw==", + "dev": true, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helpers": { + "version": "7.22.11", + "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.22.11.tgz", + "integrity": "sha512-vyOXC8PBWaGc5h7GMsNx68OH33cypkEDJCHvYVVgVbbxJDROYVtexSk0gK5iCF1xNjRIN2s8ai7hwkWDq5szWg==", + "dev": true, + "dependencies": { + "@babel/template": "^7.22.5", + "@babel/traverse": "^7.22.11", + "@babel/types": "^7.22.11" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/highlight": { + "version": "7.22.13", + "resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.22.13.tgz", + "integrity": "sha512-C/BaXcnnvBCmHTpz/VGZ8jgtE2aYlW4hxDhseJAWZb7gqGM/qtCK6iZUb0TyKFf7BOUsBH7Q7fkRsDRhg1XklQ==", + "dev": true, + "dependencies": { + "@babel/helper-validator-identifier": "^7.22.5", + "chalk": "^2.4.2", + "js-tokens": "^4.0.0" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/parser": { + "version": "7.23.0", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.23.0.tgz", + "integrity": "sha512-vvPKKdMemU85V9WE/l5wZEmImpCtLqbnTvqDS2U1fJ96KrxoW7KrXhNsNCblQlg8Ck4b85yxdTyelsMUgFUXiw==", + "bin": { + "parser": "bin/babel-parser.js" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@babel/plugin-syntax-jsx": { + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-jsx/-/plugin-syntax-jsx-7.22.5.tgz", + "integrity": "sha512-gvyP4hZrgrs/wWMaocvxZ44Hw0b3W8Pe+cMxc8V1ULQ07oh8VNbIRaoD1LRZVTvD+0nieDKjfgKg89sD7rrKrg==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.22.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-typescript": { + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-typescript/-/plugin-syntax-typescript-7.22.5.tgz", + "integrity": "sha512-1mS2o03i7t1c6VzH6fdQ3OA8tcEIxwG18zIPRp+UY1Ihv6W+XZzBCVxExF9upussPXJ0xE9XRHwMoNs1ep/nRQ==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.22.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-typescript": { + "version": "7.22.11", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-typescript/-/plugin-transform-typescript-7.22.11.tgz", + "integrity": "sha512-0E4/L+7gfvHub7wsbTv03oRtD69X31LByy44fGmFzbZScpupFByMcgCJ0VbBTkzyjSJKuRoGN8tcijOWKTmqOA==", + "dev": true, + "dependencies": { + "@babel/helper-annotate-as-pure": "^7.22.5", + "@babel/helper-create-class-features-plugin": "^7.22.11", + "@babel/helper-plugin-utils": "^7.22.5", + "@babel/plugin-syntax-typescript": "^7.22.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/runtime": { + "version": "7.23.2", + "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.23.2.tgz", + "integrity": "sha512-mM8eg4yl5D6i3lu2QKPuPH4FArvJ8KhTofbE7jwMUv9KX5mBvwPAqnV3MlyBNqdp9RyRKP6Yck8TrfYrPvX3bg==", + "dependencies": { + "regenerator-runtime": "^0.14.0" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/template": { + "version": "7.22.15", + "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.22.15.tgz", + "integrity": "sha512-QPErUVm4uyJa60rkI73qneDacvdvzxshT3kksGqlGWYdOTIUOwJ7RDUL8sGqslY1uXWSL6xMFKEXDS3ox2uF0w==", + "dev": true, + "dependencies": { + "@babel/code-frame": "^7.22.13", + "@babel/parser": "^7.22.15", + "@babel/types": "^7.22.15" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/traverse": { + "version": "7.23.2", + "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.23.2.tgz", + "integrity": "sha512-azpe59SQ48qG6nu2CzcMLbxUudtN+dOM9kDbUqGq3HXUJRlo7i8fvPoxQUzYgLZ4cMVmuZgm8vvBpNeRhd6XSw==", + "dev": true, + "dependencies": { + "@babel/code-frame": "^7.22.13", + "@babel/generator": "^7.23.0", + "@babel/helper-environment-visitor": "^7.22.20", + "@babel/helper-function-name": "^7.23.0", + "@babel/helper-hoist-variables": "^7.22.5", + "@babel/helper-split-export-declaration": "^7.22.6", + "@babel/parser": "^7.23.0", + "@babel/types": "^7.23.0", + "debug": "^4.1.0", + "globals": "^11.1.0" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/types": { + "version": "7.23.0", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.23.0.tgz", + "integrity": "sha512-0oIyUfKoI3mSqMvsxBdclDwxXKXAUA8v/apZbc+iSyARYou1o8ZGDxbUYyLFoW2arqS2jDGqJuZvv1d/io1axg==", + "dev": true, + "dependencies": { + "@babel/helper-string-parser": "^7.22.5", + "@babel/helper-validator-identifier": "^7.22.20", + "to-fast-properties": "^2.0.0" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@bcgov/bc-sans": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/@bcgov/bc-sans/-/bc-sans-2.1.0.tgz", + "integrity": "sha512-1MesF4NAVpM5dywoJ68wNcBylHbPqg1dDV/FNuQm0BbspETGlPmfX8LG8rtrCjCAPhWuL2qRT/lBYDUMvFTUnw==" + }, + "node_modules/@bcoe/v8-coverage": { + "version": "0.2.3", + "resolved": "https://registry.npmjs.org/@bcoe/v8-coverage/-/v8-coverage-0.2.3.tgz", + "integrity": "sha512-0hYQ8SB4Db5zvZB4axdMHGwEaQjkZzFjQiN9LVYvIFB2nSUHW9tYpxWriPrWDASIxiaXax83REcLxuSdnGPZtw==", + "dev": true + }, + "node_modules/@cspotcode/source-map-support": { + "version": "0.8.1", + "resolved": "https://registry.npmjs.org/@cspotcode/source-map-support/-/source-map-support-0.8.1.tgz", + "integrity": "sha512-IchNf6dN4tHoMFIn/7OE8LWZ19Y6q/67Bmf6vnGREv8RSbBVb9LPJxEcnwrcwX6ixSvaiGoomAUvu4YSxXrVgw==", + "dev": true, + "dependencies": { + "@jridgewell/trace-mapping": "0.3.9" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/@cspotcode/source-map-support/node_modules/@jridgewell/trace-mapping": { + "version": "0.3.9", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.9.tgz", + "integrity": "sha512-3Belt6tdc8bPgAtbcmdtNJlirVoTmEb5e2gC94PnkwEW9jI6CAHUeoG85tjWP5WquqfavoMtMwiG4P926ZKKuQ==", + "dev": true, + "dependencies": { + "@jridgewell/resolve-uri": "^3.0.3", + "@jridgewell/sourcemap-codec": "^1.4.10" + } + }, + "node_modules/@esbuild/android-arm": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.18.20.tgz", + "integrity": "sha512-fyi7TDI/ijKKNZTUJAQqiG5T7YjJXgnzkURqmGj13C6dCqckZBLdl4h7bkhHt/t0WP+zO9/zwroDvANaOqO5Sw==", + "cpu": [ + "arm" + ], + "dev": true, + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/android-arm64": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.18.20.tgz", + "integrity": "sha512-Nz4rJcchGDtENV0eMKUNa6L12zz2zBDXuhj/Vjh18zGqB44Bi7MBMSXjgunJgjRhCmKOjnPuZp4Mb6OKqtMHLQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/android-x64": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.18.20.tgz", + "integrity": "sha512-8GDdlePJA8D6zlZYJV/jnrRAi6rOiNaCC/JclcXpB+KIuvfBN4owLtgzY2bsxnx666XjJx2kDPUmnTtR8qKQUg==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/darwin-arm64": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.18.20.tgz", + "integrity": "sha512-bxRHW5kHU38zS2lPTPOyuyTm+S+eobPUnTNkdJEfAddYgEcll4xkT8DB9d2008DtTbl7uJag2HuE5NZAZgnNEA==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/darwin-x64": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.18.20.tgz", + "integrity": "sha512-pc5gxlMDxzm513qPGbCbDukOdsGtKhfxD1zJKXjCCcU7ju50O7MeAZ8c4krSJcOIJGFR+qx21yMMVYwiQvyTyQ==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/freebsd-arm64": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.18.20.tgz", + "integrity": "sha512-yqDQHy4QHevpMAaxhhIwYPMv1NECwOvIpGCZkECn8w2WFHXjEwrBn3CeNIYsibZ/iZEUemj++M26W3cNR5h+Tw==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/freebsd-x64": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.18.20.tgz", + "integrity": "sha512-tgWRPPuQsd3RmBZwarGVHZQvtzfEBOreNuxEMKFcd5DaDn2PbBxfwLcj4+aenoh7ctXcbXmOQIn8HI6mCSw5MQ==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-arm": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.18.20.tgz", + "integrity": "sha512-/5bHkMWnq1EgKr1V+Ybz3s1hWXok7mDFUMQ4cG10AfW3wL02PSZi5kFpYKrptDsgb2WAJIvRcDm+qIvXf/apvg==", + "cpu": [ + "arm" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-arm64": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.18.20.tgz", + "integrity": "sha512-2YbscF+UL7SQAVIpnWvYwM+3LskyDmPhe31pE7/aoTMFKKzIc9lLbyGUpmmb8a8AixOL61sQ/mFh3jEjHYFvdA==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-ia32": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.18.20.tgz", + "integrity": "sha512-P4etWwq6IsReT0E1KHU40bOnzMHoH73aXp96Fs8TIT6z9Hu8G6+0SHSw9i2isWrD2nbx2qo5yUqACgdfVGx7TA==", + "cpu": [ + "ia32" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-loong64": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.18.20.tgz", + "integrity": "sha512-nXW8nqBTrOpDLPgPY9uV+/1DjxoQ7DoB2N8eocyq8I9XuqJ7BiAMDMf9n1xZM9TgW0J8zrquIb/A7s3BJv7rjg==", + "cpu": [ + "loong64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-mips64el": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.18.20.tgz", + "integrity": "sha512-d5NeaXZcHp8PzYy5VnXV3VSd2D328Zb+9dEq5HE6bw6+N86JVPExrA6O68OPwobntbNJ0pzCpUFZTo3w0GyetQ==", + "cpu": [ + "mips64el" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-ppc64": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.18.20.tgz", + "integrity": "sha512-WHPyeScRNcmANnLQkq6AfyXRFr5D6N2sKgkFo2FqguP44Nw2eyDlbTdZwd9GYk98DZG9QItIiTlFLHJHjxP3FA==", + "cpu": [ + "ppc64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-riscv64": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.18.20.tgz", + "integrity": "sha512-WSxo6h5ecI5XH34KC7w5veNnKkju3zBRLEQNY7mv5mtBmrP/MjNBCAlsM2u5hDBlS3NGcTQpoBvRzqBcRtpq1A==", + "cpu": [ + "riscv64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-s390x": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.18.20.tgz", + "integrity": "sha512-+8231GMs3mAEth6Ja1iK0a1sQ3ohfcpzpRLH8uuc5/KVDFneH6jtAJLFGafpzpMRO6DzJ6AvXKze9LfFMrIHVQ==", + "cpu": [ + "s390x" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-x64": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.18.20.tgz", + "integrity": "sha512-UYqiqemphJcNsFEskc73jQ7B9jgwjWrSayxawS6UVFZGWrAAtkzjxSqnoclCXxWtfwLdzU+vTpcNYhpn43uP1w==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/netbsd-x64": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.18.20.tgz", + "integrity": "sha512-iO1c++VP6xUBUmltHZoMtCUdPlnPGdBom6IrO4gyKPFFVBKioIImVooR5I83nTew5UOYrk3gIJhbZh8X44y06A==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/openbsd-x64": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.18.20.tgz", + "integrity": "sha512-e5e4YSsuQfX4cxcygw/UCPIEP6wbIL+se3sxPdCiMbFLBWu0eiZOJ7WoD+ptCLrmjZBK1Wk7I6D/I3NglUGOxg==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/sunos-x64": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.18.20.tgz", + "integrity": "sha512-kDbFRFp0YpTQVVrqUd5FTYmWo45zGaXe0X8E1G/LKFC0v8x0vWrhOWSLITcCn63lmZIxfOMXtCfti/RxN/0wnQ==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/win32-arm64": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.18.20.tgz", + "integrity": "sha512-ddYFR6ItYgoaq4v4JmQQaAI5s7npztfV4Ag6NrhiaW0RrnOXqBkgwZLofVTlq1daVTQNhtI5oieTvkRPfZrePg==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/win32-ia32": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.18.20.tgz", + "integrity": "sha512-Wv7QBi3ID/rROT08SABTS7eV4hX26sVduqDOTe1MvGMjNd3EjOz4b7zeexIR62GTIEKrfJXKL9LFxTYgkyeu7g==", + "cpu": [ + "ia32" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/win32-x64": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.18.20.tgz", + "integrity": "sha512-kTdfRcSiDfQca/y9QIkng02avJ+NCaQvrMejlsB3RRv5sE9rRoeBPISaZpKxHELzRxZyLvNts1P27W3wV+8geQ==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@eslint-community/eslint-utils": { + "version": "4.4.0", + "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.4.0.tgz", + "integrity": "sha512-1/sA4dwrzBAyeUoQ6oxahHKmrZvsnLCg4RfxW3ZFGGmQkSNQPFNLV9CUEFQP1x9EYXHTo5p6xdhZM1Ne9p/AfA==", + "dev": true, + "dependencies": { + "eslint-visitor-keys": "^3.3.0" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "peerDependencies": { + "eslint": "^6.0.0 || ^7.0.0 || >=8.0.0" + } + }, + "node_modules/@eslint-community/regexpp": { + "version": "4.8.0", + "resolved": "https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.8.0.tgz", + "integrity": "sha512-JylOEEzDiOryeUnFbQz+oViCXS0KsvR1mvHkoMiu5+UiBvy+RYX7tzlIIIEstF/gVa2tj9AQXk3dgnxv6KxhFg==", + "dev": true, + "engines": { + "node": "^12.0.0 || ^14.0.0 || >=16.0.0" + } + }, + "node_modules/@eslint/eslintrc": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-2.1.2.tgz", + "integrity": "sha512-+wvgpDsrB1YqAMdEUCcnTlpfVBH7Vqn6A/NT3D8WVXFIaKMlErPIZT3oCIAVCOtarRpMtelZLqJeU3t7WY6X6g==", + "dev": true, + "dependencies": { + "ajv": "^6.12.4", + "debug": "^4.3.2", + "espree": "^9.6.0", + "globals": "^13.19.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" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/@eslint/eslintrc/node_modules/globals": { + "version": "13.21.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-13.21.0.tgz", + "integrity": "sha512-ybyme3s4yy/t/3s35bewwXKOf7cvzfreG2lH0lZl0JB7I4GxRP2ghxOK/Nb9EkRXdbBXZLfq/p/0W2JUONB/Gg==", + "dev": true, + "dependencies": { + "type-fest": "^0.20.2" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@eslint/js": { + "version": "8.52.0", + "resolved": "https://registry.npmjs.org/@eslint/js/-/js-8.52.0.tgz", + "integrity": "sha512-mjZVbpaeMZludF2fsWLD0Z9gCref1Tk4i9+wddjRvpUNqqcndPkBD09N/Mapey0b3jaXbLm2kICwFv2E64QinA==", + "dev": true, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + } + }, + "node_modules/@fortawesome/fontawesome-common-types": { + "version": "6.4.2", + "resolved": "https://registry.npmjs.org/@fortawesome/fontawesome-common-types/-/fontawesome-common-types-6.4.2.tgz", + "integrity": "sha512-1DgP7f+XQIJbLFCTX1V2QnxVmpLdKdzzo2k8EmvDOePfchaIGQ9eCHj2up3/jNEbZuBqel5OxiaOJf37TWauRA==", + "hasInstallScript": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/@fortawesome/fontawesome-svg-core": { + "version": "6.4.2", + "resolved": "https://registry.npmjs.org/@fortawesome/fontawesome-svg-core/-/fontawesome-svg-core-6.4.2.tgz", + "integrity": "sha512-gjYDSKv3TrM2sLTOKBc5rH9ckje8Wrwgx1CxAPbN5N3Fm4prfi7NsJVWd1jklp7i5uSCVwhZS5qlhMXqLrpAIg==", + "hasInstallScript": true, + "dependencies": { + "@fortawesome/fontawesome-common-types": "6.4.2" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/@fortawesome/free-solid-svg-icons": { + "version": "6.4.2", + "resolved": "https://registry.npmjs.org/@fortawesome/free-solid-svg-icons/-/free-solid-svg-icons-6.4.2.tgz", + "integrity": "sha512-sYwXurXUEQS32fZz9hVCUUv/xu49PEJEyUOsA51l6PU/qVgfbTb2glsTEaJngVVT8VqBATRIdh7XVgV1JF1LkA==", + "hasInstallScript": true, + "dependencies": { + "@fortawesome/fontawesome-common-types": "6.4.2" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/@fortawesome/vue-fontawesome": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/@fortawesome/vue-fontawesome/-/vue-fontawesome-3.0.3.tgz", + "integrity": "sha512-KCPHi9QemVXGMrfuwf3nNnNo129resAIQWut9QTAMXmXqL2ErABC6ohd2yY5Ipq0CLWNbKHk8TMdTXL/Zf3ZhA==", + "peerDependencies": { + "@fortawesome/fontawesome-svg-core": "~1 || ~6", + "vue": ">= 3.0.0 < 4" + } + }, + "node_modules/@humanwhocodes/config-array": { + "version": "0.11.13", + "resolved": "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.11.13.tgz", + "integrity": "sha512-JSBDMiDKSzQVngfRjOdFXgFfklaXI4K9nLF49Auh21lmBWRLIK3+xTErTWD4KU54pb6coM6ESE7Awz/FNU3zgQ==", + "dev": true, + "dependencies": { + "@humanwhocodes/object-schema": "^2.0.1", + "debug": "^4.1.1", + "minimatch": "^3.0.5" + }, + "engines": { + "node": ">=10.10.0" + } + }, + "node_modules/@humanwhocodes/module-importer": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@humanwhocodes/module-importer/-/module-importer-1.0.1.tgz", + "integrity": "sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==", + "dev": true, + "engines": { + "node": ">=12.22" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/nzakas" + } + }, + "node_modules/@humanwhocodes/object-schema": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/@humanwhocodes/object-schema/-/object-schema-2.0.1.tgz", + "integrity": "sha512-dvuCeX5fC9dXgJn9t+X5atfmgQAzUOWqS1254Gh0m6i8wKd10ebXkfNKiRK+1GWi/yTvvLDHpoxLr0xxxeslWw==", + "dev": true + }, + "node_modules/@isaacs/cliui": { + "version": "8.0.2", + "resolved": "https://registry.npmjs.org/@isaacs/cliui/-/cliui-8.0.2.tgz", + "integrity": "sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==", + "dev": true, + "dependencies": { + "string-width": "^5.1.2", + "string-width-cjs": "npm:string-width@^4.2.0", + "strip-ansi": "^7.0.1", + "strip-ansi-cjs": "npm:strip-ansi@^6.0.1", + "wrap-ansi": "^8.1.0", + "wrap-ansi-cjs": "npm:wrap-ansi@^7.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/@isaacs/cliui/node_modules/ansi-regex": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.0.1.tgz", + "integrity": "sha512-n5M855fKb2SsfMIiFFoVrABHJC8QtHwVx+mHWP3QcEqBHYienj5dHSgjbxtC0WEZXYt4wcD6zrQElDPhFuZgfA==", + "dev": true, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-regex?sponsor=1" + } + }, + "node_modules/@isaacs/cliui/node_modules/ansi-styles": { + "version": "6.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.1.tgz", + "integrity": "sha512-bN798gFfQX+viw3R7yrGWRqnrN2oRkEkUjjl4JNn4E8GxxbjtG3FbrEIIY3l8/hrwUwIeCZvi4QuOTP4MErVug==", + "dev": true, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/@isaacs/cliui/node_modules/strip-ansi": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.0.tgz", + "integrity": "sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ==", + "dev": true, + "dependencies": { + "ansi-regex": "^6.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/strip-ansi?sponsor=1" + } + }, + "node_modules/@isaacs/cliui/node_modules/wrap-ansi": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-8.1.0.tgz", + "integrity": "sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ==", + "dev": true, + "dependencies": { + "ansi-styles": "^6.1.0", + "string-width": "^5.0.1", + "strip-ansi": "^7.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/@istanbuljs/schema": { + "version": "0.1.3", + "resolved": "https://registry.npmjs.org/@istanbuljs/schema/-/schema-0.1.3.tgz", + "integrity": "sha512-ZXRY4jNvVgSVQ8DL3LTcakaAtXwTVUxE81hslsyD2AtoXW/wVob10HkOJ1X/pAlcI7D+2YoZKg5do8G/w6RYgA==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/@jest/schemas": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/@jest/schemas/-/schemas-29.6.3.tgz", + "integrity": "sha512-mo5j5X+jIZmJQveBKeS/clAueipV7KgiX1vMgCxam1RNYiqE1w62n0/tJJnHtjW8ZHcQco5gY85jA3mi0L+nSA==", + "dev": true, + "dependencies": { + "@sinclair/typebox": "^0.27.8" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jridgewell/gen-mapping": { + "version": "0.3.3", + "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.3.tgz", + "integrity": "sha512-HLhSWOLRi875zjjMG/r+Nv0oCW8umGb0BgEhyX3dDX3egwZtB8PqLnjz3yedt8R5StBrzcg4aBpnh8UA9D1BoQ==", + "dev": true, + "dependencies": { + "@jridgewell/set-array": "^1.0.1", + "@jridgewell/sourcemap-codec": "^1.4.10", + "@jridgewell/trace-mapping": "^0.3.9" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/resolve-uri": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.0.tgz", + "integrity": "sha512-F2msla3tad+Mfht5cJq7LSXcdudKTWCVYUgw6pLFOOHSTtZlj6SWNYAp+AhuqLmWdBO2X5hPrLcu8cVP8fy28w==", + "dev": true, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/set-array": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@jridgewell/set-array/-/set-array-1.1.2.tgz", + "integrity": "sha512-xnkseuNADM0gt2bs+BvhO0p78Mk762YnZdsuzFV018NoG1Sj1SCQvpSqa7XUaTam5vAGasABV9qXASMKnFMwMw==", + "dev": true, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/sourcemap-codec": { + "version": "1.4.14", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.4.14.tgz", + "integrity": "sha512-XPSJHWmi394fuUuzDnGz1wiKqWfo1yXecHQMRf2l6hztTO+nPru658AyDngaBe7isIxEkRsPR3FZh+s7iVa4Uw==", + "dev": true + }, + "node_modules/@jridgewell/trace-mapping": { + "version": "0.3.17", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.17.tgz", + "integrity": "sha512-MCNzAp77qzKca9+W/+I0+sEpaUnZoeasnghNeVc41VZCEKaCH73Vq3BZZ/SzWIgrqE4H4ceI+p+b6C0mHf9T4g==", + "dev": true, + "dependencies": { + "@jridgewell/resolve-uri": "3.1.0", + "@jridgewell/sourcemap-codec": "1.4.14" + } + }, + "node_modules/@nodelib/fs.scandir": { + "version": "2.1.5", + "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", + "integrity": "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==", + "dev": true, + "dependencies": { + "@nodelib/fs.stat": "2.0.5", + "run-parallel": "^1.1.9" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/@nodelib/fs.stat": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz", + "integrity": "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==", + "dev": true, + "engines": { + "node": ">= 8" + } + }, + "node_modules/@nodelib/fs.walk": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/@nodelib/fs.walk/-/fs.walk-1.2.8.tgz", + "integrity": "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==", + "dev": true, + "dependencies": { + "@nodelib/fs.scandir": "2.1.5", + "fastq": "^1.6.0" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/@one-ini/wasm": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/@one-ini/wasm/-/wasm-0.1.1.tgz", + "integrity": "sha512-XuySG1E38YScSJoMlqovLru4KTUNSjgVTIjyh7qMX6aNN5HY5Ct5LhRJdxO79JtTzKfzV/bnWpz+zquYrISsvw==", + "dev": true + }, + "node_modules/@pinia/testing": { + "version": "0.1.3", + "resolved": "https://registry.npmjs.org/@pinia/testing/-/testing-0.1.3.tgz", + "integrity": "sha512-D2Ds2s69kKFaRf2KCcP1NhNZEg5+we59aRyQalwRm7ygWfLM25nDH66267U3hNvRUOTx8ofL24GzodZkOmB5xw==", + "dev": true, + "dependencies": { + "vue-demi": ">=0.14.5" + }, + "funding": { + "url": "https://github.com/sponsors/posva" + }, + "peerDependencies": { + "pinia": ">=2.1.5" + } + }, + "node_modules/@pinia/testing/node_modules/vue-demi": { + "version": "0.14.5", + "resolved": "https://registry.npmjs.org/vue-demi/-/vue-demi-0.14.5.tgz", + "integrity": "sha512-o9NUVpl/YlsGJ7t+xuqJKx8EBGf1quRhCiT6D/J0pfwmk9zUwYkC7yrF4SZCe6fETvSM3UNL2edcbYrSyc4QHA==", + "dev": true, + "hasInstallScript": true, + "bin": { + "vue-demi-fix": "bin/vue-demi-fix.js", + "vue-demi-switch": "bin/vue-demi-switch.js" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/antfu" + }, + "peerDependencies": { + "@vue/composition-api": "^1.0.0-rc.1", + "vue": "^3.0.0-0 || ^2.6.0" + }, + "peerDependenciesMeta": { + "@vue/composition-api": { + "optional": true + } + } + }, + "node_modules/@pkgjs/parseargs": { + "version": "0.11.0", + "resolved": "https://registry.npmjs.org/@pkgjs/parseargs/-/parseargs-0.11.0.tgz", + "integrity": "sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==", + "dev": true, + "optional": true, + "engines": { + "node": ">=14" + } + }, + "node_modules/@sinclair/typebox": { + "version": "0.27.8", + "resolved": "https://registry.npmjs.org/@sinclair/typebox/-/typebox-0.27.8.tgz", + "integrity": "sha512-+Fj43pSMwJs4KRrH/938Uf+uAELIgVBmQzg/q1YG10djyfA3TnrU8N8XzqCh/okZdszqBQTZf96idMfE5lnwTA==", + "dev": true + }, + "node_modules/@testing-library/dom": { + "version": "9.3.3", + "resolved": "https://registry.npmjs.org/@testing-library/dom/-/dom-9.3.3.tgz", + "integrity": "sha512-fB0R+fa3AUqbLHWyxXa2kGVtf1Fe1ZZFr0Zp6AIbIAzXb2mKbEXl+PCQNUOaq5lbTab5tfctfXRNsWXxa2f7Aw==", + "dev": true, + "dependencies": { + "@babel/code-frame": "^7.10.4", + "@babel/runtime": "^7.12.5", + "@types/aria-query": "^5.0.1", + "aria-query": "5.1.3", + "chalk": "^4.1.0", + "dom-accessibility-api": "^0.5.9", + "lz-string": "^1.5.0", + "pretty-format": "^27.0.2" + }, + "engines": { + "node": ">=14" + } + }, + "node_modules/@testing-library/dom/node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/@testing-library/dom/node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/@testing-library/dom/node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/@testing-library/dom/node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true + }, + "node_modules/@testing-library/dom/node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/@testing-library/dom/node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/@testing-library/vue": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/@testing-library/vue/-/vue-8.0.0.tgz", + "integrity": "sha512-SP0qEY/SGpdT9+bPuHxYD3P/HCG0ZY8GlGJocVqdLn9EojbdQu69x06trJi1V7RW9tAZai/wwy+ZFcRsTp47kg==", + "dev": true, + "dependencies": { + "@babel/runtime": "^7.23.2", + "@testing-library/dom": "^9.3.3", + "@vue/test-utils": "^2.4.1" + }, + "engines": { + "node": ">=14" + }, + "peerDependencies": { + "@vue/compiler-sfc": ">= 3", + "vue": ">= 3" + } + }, + "node_modules/@tootallnate/once": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/@tootallnate/once/-/once-2.0.0.tgz", + "integrity": "sha512-XCuKFP5PS55gnMVu3dty8KPatLqUoy/ZYzDzAGCQ8JNFCkLXzmI7vNHCR+XpbZaMWQK/vQubr7PkYq8g470J/A==", + "dev": true, + "optional": true, + "peer": true, + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tsconfig/node10": { + "version": "1.0.9", + "resolved": "https://registry.npmjs.org/@tsconfig/node10/-/node10-1.0.9.tgz", + "integrity": "sha512-jNsYVVxU8v5g43Erja32laIDHXeoNvFEpX33OK4d6hljo3jDhCBDhx5dhCCTMWUojscpAagGiRkBKxpdl9fxqA==", + "dev": true + }, + "node_modules/@tsconfig/node12": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/@tsconfig/node12/-/node12-1.0.11.tgz", + "integrity": "sha512-cqefuRsh12pWyGsIoBKJA9luFu3mRxCA+ORZvA4ktLSzIuCUtWVxGIuXigEwO5/ywWFMZ2QEGKWvkZG1zDMTag==", + "dev": true + }, + "node_modules/@tsconfig/node14": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@tsconfig/node14/-/node14-1.0.3.tgz", + "integrity": "sha512-ysT8mhdixWK6Hw3i1V2AeRqZ5WfXg1G43mqoYlM2nc6388Fq5jcXyr5mRsqViLx/GJYdoL0bfXD8nmF+Zn/Iow==", + "dev": true + }, + "node_modules/@tsconfig/node16": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@tsconfig/node16/-/node16-1.0.3.tgz", + "integrity": "sha512-yOlFc+7UtL/89t2ZhjPvvB/DeAr3r+Dq58IgzsFkOAvVC6NMJXmCGjbptdXdR9qsX7pKcTL+s87FtYREi2dEEQ==", + "dev": true + }, + "node_modules/@tsconfig/node18": { + "version": "18.2.2", + "resolved": "https://registry.npmjs.org/@tsconfig/node18/-/node18-18.2.2.tgz", + "integrity": "sha512-d6McJeGsuoRlwWZmVIeE8CUA27lu6jLjvv1JzqmpsytOYYbVi1tHZEnwCNVOXnj4pyLvneZlFlpXUK+X9wBWyw==", + "dev": true + }, + "node_modules/@types/aria-query": { + "version": "5.0.3", + "resolved": "https://registry.npmjs.org/@types/aria-query/-/aria-query-5.0.3.tgz", + "integrity": "sha512-0Z6Tr7wjKJIk4OUEjVUQMtyunLDy339vcMaj38Kpj6jM2OE1p3S4kXExKZ7a3uXQAPCoy3sbrP1wibDKaf39oA==", + "dev": true + }, + "node_modules/@types/chai": { + "version": "4.3.5", + "resolved": "https://registry.npmjs.org/@types/chai/-/chai-4.3.5.tgz", + "integrity": "sha512-mEo1sAde+UCE6b2hxn332f1g1E8WfYRu6p5SvTKr2ZKC1f7gFJXk4h5PyGP9Dt6gCaG8y8XhwnXWC6Iy2cmBng==", + "dev": true + }, + "node_modules/@types/chai-subset": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/@types/chai-subset/-/chai-subset-1.3.3.tgz", + "integrity": "sha512-frBecisrNGz+F4T6bcc+NLeolfiojh5FxW2klu669+8BARtyQv2C/GkNW6FUodVe4BroGMP/wER/YDGc7rEllw==", + "dev": true, + "dependencies": { + "@types/chai": "*" + } + }, + "node_modules/@types/istanbul-lib-coverage": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/@types/istanbul-lib-coverage/-/istanbul-lib-coverage-2.0.4.tgz", + "integrity": "sha512-z/QT1XN4K4KYuslS23k62yDIDLwLFkzxOuMplDtObz0+y7VqJCaO2o+SPwHCvLFZh7xazvvoor2tA/hPz9ee7g==", + "dev": true + }, + "node_modules/@types/json-schema": { + "version": "7.0.14", + "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.14.tgz", + "integrity": "sha512-U3PUjAudAdJBeC2pgN8uTIKgxrb4nlDF3SF0++EldXQvQBGkpFZMSnwQiIoDU77tv45VgNkl/L4ouD+rEomujw==", + "dev": true + }, + "node_modules/@types/node": { + "version": "20.8.10", + "resolved": "https://registry.npmjs.org/@types/node/-/node-20.8.10.tgz", + "integrity": "sha512-TlgT8JntpcbmKUFzjhsyhGfP2fsiz1Mv56im6enJ905xG1DAYesxJaeSbGqQmAw8OWPdhyJGhGSQGKRNJ45u9w==", + "dev": true, + "dependencies": { + "undici-types": "~5.26.4" + } + }, + "node_modules/@types/semver": { + "version": "7.5.4", + "resolved": "https://registry.npmjs.org/@types/semver/-/semver-7.5.4.tgz", + "integrity": "sha512-MMzuxN3GdFwskAnb6fz0orFvhfqi752yjaXylr0Rp4oDg5H0Zn1IuyRhDVvYOwAXoJirx2xuS16I3WjxnAIHiQ==", + "dev": true + }, + "node_modules/@typescript-eslint/eslint-plugin": { + "version": "6.9.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-6.9.0.tgz", + "integrity": "sha512-lgX7F0azQwRPB7t7WAyeHWVfW1YJ9NIgd9mvGhfQpRY56X6AVf8mwM8Wol+0z4liE7XX3QOt8MN1rUKCfSjRIA==", + "dev": true, + "dependencies": { + "@eslint-community/regexpp": "^4.5.1", + "@typescript-eslint/scope-manager": "6.9.0", + "@typescript-eslint/type-utils": "6.9.0", + "@typescript-eslint/utils": "6.9.0", + "@typescript-eslint/visitor-keys": "6.9.0", + "debug": "^4.3.4", + "graphemer": "^1.4.0", + "ignore": "^5.2.4", + "natural-compare": "^1.4.0", + "semver": "^7.5.4", + "ts-api-utils": "^1.0.1" + }, + "engines": { + "node": "^16.0.0 || >=18.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "@typescript-eslint/parser": "^6.0.0 || ^6.0.0-alpha", + "eslint": "^7.0.0 || ^8.0.0" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/@typescript-eslint/eslint-plugin/node_modules/semver": { + "version": "7.5.4", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.5.4.tgz", + "integrity": "sha512-1bCSESV6Pv+i21Hvpxp3Dx+pSD8lIPt8uVjRrxAUt/nbswYc+tK6Y2btiULjd4+fnq15PX+nqQDC7Oft7WkwcA==", + "dev": true, + "dependencies": { + "lru-cache": "^6.0.0" + }, + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/@typescript-eslint/parser": { + "version": "6.9.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-6.9.0.tgz", + "integrity": "sha512-GZmjMh4AJ/5gaH4XF2eXA8tMnHWP+Pm1mjQR2QN4Iz+j/zO04b9TOvJYOX2sCNIQHtRStKTxRY1FX7LhpJT4Gw==", + "dev": true, + "dependencies": { + "@typescript-eslint/scope-manager": "6.9.0", + "@typescript-eslint/types": "6.9.0", + "@typescript-eslint/typescript-estree": "6.9.0", + "@typescript-eslint/visitor-keys": "6.9.0", + "debug": "^4.3.4" + }, + "engines": { + "node": "^16.0.0 || >=18.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^7.0.0 || ^8.0.0" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/@typescript-eslint/scope-manager": { + "version": "6.9.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-6.9.0.tgz", + "integrity": "sha512-1R8A9Mc39n4pCCz9o79qRO31HGNDvC7UhPhv26TovDsWPBDx+Sg3rOZdCELIA3ZmNoWAuxaMOT7aWtGRSYkQxw==", + "dev": true, + "dependencies": { + "@typescript-eslint/types": "6.9.0", + "@typescript-eslint/visitor-keys": "6.9.0" + }, + "engines": { + "node": "^16.0.0 || >=18.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@typescript-eslint/type-utils": { + "version": "6.9.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-6.9.0.tgz", + "integrity": "sha512-XXeahmfbpuhVbhSOROIzJ+b13krFmgtc4GlEuu1WBT+RpyGPIA4Y/eGnXzjbDj5gZLzpAXO/sj+IF/x2GtTMjQ==", + "dev": true, + "dependencies": { + "@typescript-eslint/typescript-estree": "6.9.0", + "@typescript-eslint/utils": "6.9.0", + "debug": "^4.3.4", + "ts-api-utils": "^1.0.1" + }, + "engines": { + "node": "^16.0.0 || >=18.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^7.0.0 || ^8.0.0" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/@typescript-eslint/types": { + "version": "6.9.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-6.9.0.tgz", + "integrity": "sha512-+KB0lbkpxBkBSiVCuQvduqMJy+I1FyDbdwSpM3IoBS7APl4Bu15lStPjgBIdykdRqQNYqYNMa8Kuidax6phaEw==", + "dev": true, + "engines": { + "node": "^16.0.0 || >=18.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@typescript-eslint/typescript-estree": { + "version": "6.9.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-6.9.0.tgz", + "integrity": "sha512-NJM2BnJFZBEAbCfBP00zONKXvMqihZCrmwCaik0UhLr0vAgb6oguXxLX1k00oQyD+vZZ+CJn3kocvv2yxm4awQ==", + "dev": true, + "dependencies": { + "@typescript-eslint/types": "6.9.0", + "@typescript-eslint/visitor-keys": "6.9.0", + "debug": "^4.3.4", + "globby": "^11.1.0", + "is-glob": "^4.0.3", + "semver": "^7.5.4", + "ts-api-utils": "^1.0.1" + }, + "engines": { + "node": "^16.0.0 || >=18.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/@typescript-eslint/typescript-estree/node_modules/semver": { + "version": "7.5.4", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.5.4.tgz", + "integrity": "sha512-1bCSESV6Pv+i21Hvpxp3Dx+pSD8lIPt8uVjRrxAUt/nbswYc+tK6Y2btiULjd4+fnq15PX+nqQDC7Oft7WkwcA==", + "dev": true, + "dependencies": { + "lru-cache": "^6.0.0" + }, + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/@typescript-eslint/utils": { + "version": "6.9.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-6.9.0.tgz", + "integrity": "sha512-5Wf+Jsqya7WcCO8me504FBigeQKVLAMPmUzYgDbWchINNh1KJbxCgVya3EQ2MjvJMVeXl3pofRmprqX6mfQkjQ==", + "dev": true, + "dependencies": { + "@eslint-community/eslint-utils": "^4.4.0", + "@types/json-schema": "^7.0.12", + "@types/semver": "^7.5.0", + "@typescript-eslint/scope-manager": "6.9.0", + "@typescript-eslint/types": "6.9.0", + "@typescript-eslint/typescript-estree": "6.9.0", + "semver": "^7.5.4" + }, + "engines": { + "node": "^16.0.0 || >=18.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^7.0.0 || ^8.0.0" + } + }, + "node_modules/@typescript-eslint/utils/node_modules/semver": { + "version": "7.5.4", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.5.4.tgz", + "integrity": "sha512-1bCSESV6Pv+i21Hvpxp3Dx+pSD8lIPt8uVjRrxAUt/nbswYc+tK6Y2btiULjd4+fnq15PX+nqQDC7Oft7WkwcA==", + "dev": true, + "dependencies": { + "lru-cache": "^6.0.0" + }, + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/@typescript-eslint/visitor-keys": { + "version": "6.9.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-6.9.0.tgz", + "integrity": "sha512-dGtAfqjV6RFOtIP8I0B4ZTBRrlTT8NHHlZZSchQx3qReaoDeXhYM++M4So2AgFK9ZB0emRPA6JI1HkafzA2Ibg==", + "dev": true, + "dependencies": { + "@typescript-eslint/types": "6.9.0", + "eslint-visitor-keys": "^3.4.1" + }, + "engines": { + "node": "^16.0.0 || >=18.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@ungap/structured-clone": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/@ungap/structured-clone/-/structured-clone-1.2.0.tgz", + "integrity": "sha512-zuVdFrMJiuCDQUMCzQaD6KL28MjnqqN8XnAqiEq9PNm/hCPTSGfrXCOfwj1ow4LFb/tNymJPwsNbVePc1xFqrQ==", + "dev": true + }, + "node_modules/@vitejs/plugin-vue": { + "version": "4.4.0", + "resolved": "https://registry.npmjs.org/@vitejs/plugin-vue/-/plugin-vue-4.4.0.tgz", + "integrity": "sha512-xdguqb+VUwiRpSg+nsc2HtbAUSGak25DXYvpQQi4RVU1Xq1uworyoH/md9Rfd8zMmPR/pSghr309QNcftUVseg==", + "dev": true, + "engines": { + "node": "^14.18.0 || >=16.0.0" + }, + "peerDependencies": { + "vite": "^4.0.0", + "vue": "^3.2.25" + } + }, + "node_modules/@vitejs/plugin-vue-jsx": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/@vitejs/plugin-vue-jsx/-/plugin-vue-jsx-3.0.2.tgz", + "integrity": "sha512-obF26P2Z4Ogy3cPp07B4VaW6rpiu0ue4OT2Y15UxT5BZZ76haUY9guOsZV3uWh/I6xc+VeiW+ZVabRE82FyzWw==", + "dev": true, + "dependencies": { + "@babel/core": "^7.22.10", + "@babel/plugin-transform-typescript": "^7.22.10", + "@vue/babel-plugin-jsx": "^1.1.5" + }, + "engines": { + "node": "^14.18.0 || >=16.0.0" + }, + "peerDependencies": { + "vite": "^4.0.0", + "vue": "^3.0.0" + } + }, + "node_modules/@vitest/coverage-c8": { + "version": "0.33.0", + "resolved": "https://registry.npmjs.org/@vitest/coverage-c8/-/coverage-c8-0.33.0.tgz", + "integrity": "sha512-DaF1zJz4dcOZS4k/neiQJokmOWqsGXwhthfmUdPGorXIQHjdPvV6JQSYhQDI41MyI8c+IieQUdIDs5XAMHtDDw==", + "deprecated": "v8 coverage is moved to @vitest/coverage-v8 package", + "dev": true, + "dependencies": { + "@ampproject/remapping": "^2.2.1", + "c8": "^7.14.0", + "magic-string": "^0.30.1", + "picocolors": "^1.0.0", + "std-env": "^3.3.3" + }, + "funding": { + "url": "https://opencollective.com/vitest" + }, + "peerDependencies": { + "vitest": ">=0.30.0 <1" + } + }, + "node_modules/@vitest/coverage-istanbul": { + "version": "0.34.6", + "resolved": "https://registry.npmjs.org/@vitest/coverage-istanbul/-/coverage-istanbul-0.34.6.tgz", + "integrity": "sha512-5KaBNZPDSk2ybavC3rZ1pWGniw7sJ5usuwVGRUYzJwiBfWvnLpuUer7bjw7qUCRGdKJXrBgb/Dsgif9rkwMX/A==", + "dev": true, + "dependencies": { + "istanbul-lib-coverage": "^3.2.0", + "istanbul-lib-instrument": "^6.0.0", + "istanbul-lib-report": "^3.0.1", + "istanbul-lib-source-maps": "^4.0.1", + "istanbul-reports": "^3.1.5", + "picocolors": "^1.0.0", + "test-exclude": "^6.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + }, + "peerDependencies": { + "vitest": ">=0.32.0 <1" + } + }, + "node_modules/@vitest/expect": { + "version": "0.34.6", + "resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-0.34.6.tgz", + "integrity": "sha512-QUzKpUQRc1qC7qdGo7rMK3AkETI7w18gTCUrsNnyjjJKYiuUB9+TQK3QnR1unhCnWRC0AbKv2omLGQDF/mIjOw==", + "dev": true, + "dependencies": { + "@vitest/spy": "0.34.6", + "@vitest/utils": "0.34.6", + "chai": "^4.3.10" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/runner": { + "version": "0.34.6", + "resolved": "https://registry.npmjs.org/@vitest/runner/-/runner-0.34.6.tgz", + "integrity": "sha512-1CUQgtJSLF47NnhN+F9X2ycxUP0kLHQ/JWvNHbeBfwW8CzEGgeskzNnHDyv1ieKTltuR6sdIHV+nmR6kPxQqzQ==", + "dev": true, + "dependencies": { + "@vitest/utils": "0.34.6", + "p-limit": "^4.0.0", + "pathe": "^1.1.1" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/runner/node_modules/p-limit": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-4.0.0.tgz", + "integrity": "sha512-5b0R4txpzjPWVw/cXXUResoD4hb6U/x9BH08L7nw+GN1sezDzPdxeRvpc9c433fZhBan/wusjbCsqwqm4EIBIQ==", + "dev": true, + "dependencies": { + "yocto-queue": "^1.0.0" + }, + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@vitest/runner/node_modules/yocto-queue": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-1.0.0.tgz", + "integrity": "sha512-9bnSc/HEW2uRy67wc+T8UwauLuPJVn28jb+GtJY16iiKWyvmYJRXVT4UamsAEGQfPohgr2q4Tq0sQbQlxTfi1g==", + "dev": true, + "engines": { + "node": ">=12.20" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@vitest/snapshot": { + "version": "0.34.6", + "resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-0.34.6.tgz", + "integrity": "sha512-B3OZqYn6k4VaN011D+ve+AA4whM4QkcwcrwaKwAbyyvS/NB1hCWjFIBQxAQQSQir9/RtyAAGuq+4RJmbn2dH4w==", + "dev": true, + "dependencies": { + "magic-string": "^0.30.1", + "pathe": "^1.1.1", + "pretty-format": "^29.5.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/snapshot/node_modules/ansi-styles": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz", + "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==", + "dev": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/@vitest/snapshot/node_modules/pretty-format": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-29.7.0.tgz", + "integrity": "sha512-Pdlw/oPxN+aXdmM9R00JVC9WVFoCLTKJvDVLgmJ+qAffBMxsV85l/Lu7sNx4zSzPyoL2euImuEwHhOXdEgNFZQ==", + "dev": true, + "dependencies": { + "@jest/schemas": "^29.6.3", + "ansi-styles": "^5.0.0", + "react-is": "^18.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@vitest/snapshot/node_modules/react-is": { + "version": "18.2.0", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-18.2.0.tgz", + "integrity": "sha512-xWGDIW6x921xtzPkhiULtthJHoJvBbF3q26fzloPCK0hsvxtPVelvftw3zjbHWSkR2km9Z+4uxbDDK/6Zw9B8w==", + "dev": true + }, + "node_modules/@vitest/spy": { + "version": "0.34.6", + "resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-0.34.6.tgz", + "integrity": "sha512-xaCvneSaeBw/cz8ySmF7ZwGvL0lBjfvqc1LpQ/vcdHEvpLn3Ff1vAvjw+CoGn0802l++5L/pxb7whwcWAw+DUQ==", + "dev": true, + "dependencies": { + "tinyspy": "^2.1.1" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/utils": { + "version": "0.34.6", + "resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-0.34.6.tgz", + "integrity": "sha512-IG5aDD8S6zlvloDsnzHw0Ut5xczlF+kv2BOTo+iXfPr54Yhi5qbVOgGB1hZaVq4iJ4C/MZ2J0y15IlsV/ZcI0A==", + "dev": true, + "dependencies": { + "diff-sequences": "^29.4.3", + "loupe": "^2.3.6", + "pretty-format": "^29.5.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/utils/node_modules/ansi-styles": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz", + "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==", + "dev": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/@vitest/utils/node_modules/pretty-format": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-29.7.0.tgz", + "integrity": "sha512-Pdlw/oPxN+aXdmM9R00JVC9WVFoCLTKJvDVLgmJ+qAffBMxsV85l/Lu7sNx4zSzPyoL2euImuEwHhOXdEgNFZQ==", + "dev": true, + "dependencies": { + "@jest/schemas": "^29.6.3", + "ansi-styles": "^5.0.0", + "react-is": "^18.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@vitest/utils/node_modules/react-is": { + "version": "18.2.0", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-18.2.0.tgz", + "integrity": "sha512-xWGDIW6x921xtzPkhiULtthJHoJvBbF3q26fzloPCK0hsvxtPVelvftw3zjbHWSkR2km9Z+4uxbDDK/6Zw9B8w==", + "dev": true + }, + "node_modules/@volar/language-core": { + "version": "1.10.5", + "resolved": "https://registry.npmjs.org/@volar/language-core/-/language-core-1.10.5.tgz", + "integrity": "sha512-xD71j4Ee0Ycq8WsiAE6H/aCThGdTobiZZeD+jFD+bvmbopa1Az296pqJysr3Ck8c7n5+GGF+xlKCS3WxRFYgSQ==", + "dev": true, + "dependencies": { + "@volar/source-map": "1.10.5" + } + }, + "node_modules/@volar/source-map": { + "version": "1.10.5", + "resolved": "https://registry.npmjs.org/@volar/source-map/-/source-map-1.10.5.tgz", + "integrity": "sha512-s4kgo66SA1kMzYvF9HFE6Vc1rxtXLUmcLrT2WKnchPDvLne+97Kw+xoR2NxJFmsvHoL18vmu/YGXYcN+Q5re1g==", + "dev": true, + "dependencies": { + "muggle-string": "^0.3.1" + } + }, + "node_modules/@volar/typescript": { + "version": "1.10.5", + "resolved": "https://registry.npmjs.org/@volar/typescript/-/typescript-1.10.5.tgz", + "integrity": "sha512-kfDehpeLJku9i1BgsFOYIczPmFFH4herl+GZrLGdvX5urTqeCKsKYlF36iNmFaADzjMb9WlENcUZzPjK8MxNrQ==", + "dev": true, + "dependencies": { + "@volar/language-core": "1.10.5" + } + }, + "node_modules/@vue/babel-helper-vue-transform-on": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/@vue/babel-helper-vue-transform-on/-/babel-helper-vue-transform-on-1.1.5.tgz", + "integrity": "sha512-SgUymFpMoAyWeYWLAY+MkCK3QEROsiUnfaw5zxOVD/M64KQs8D/4oK6Q5omVA2hnvEOE0SCkH2TZxs/jnnUj7w==", + "dev": true + }, + "node_modules/@vue/babel-plugin-jsx": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/@vue/babel-plugin-jsx/-/babel-plugin-jsx-1.1.5.tgz", + "integrity": "sha512-nKs1/Bg9U1n3qSWnsHhCVQtAzI6aQXqua8j/bZrau8ywT1ilXQbK4FwEJGmU8fV7tcpuFvWmmN7TMmV1OBma1g==", + "dev": true, + "dependencies": { + "@babel/helper-module-imports": "^7.22.5", + "@babel/plugin-syntax-jsx": "^7.22.5", + "@babel/template": "^7.22.5", + "@babel/traverse": "^7.22.5", + "@babel/types": "^7.22.5", + "@vue/babel-helper-vue-transform-on": "^1.1.5", + "camelcase": "^6.3.0", + "html-tags": "^3.3.1", + "svg-tags": "^1.0.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@vue/compiler-core": { + "version": "3.3.7", + "resolved": "https://registry.npmjs.org/@vue/compiler-core/-/compiler-core-3.3.7.tgz", + "integrity": "sha512-pACdY6YnTNVLXsB86YD8OF9ihwpolzhhtdLVHhBL6do/ykr6kKXNYABRtNMGrsQXpEXXyAdwvWWkuTbs4MFtPQ==", + "dependencies": { + "@babel/parser": "^7.23.0", + "@vue/shared": "3.3.7", + "estree-walker": "^2.0.2", + "source-map-js": "^1.0.2" + } + }, + "node_modules/@vue/compiler-dom": { + "version": "3.3.7", + "resolved": "https://registry.npmjs.org/@vue/compiler-dom/-/compiler-dom-3.3.7.tgz", + "integrity": "sha512-0LwkyJjnUPssXv/d1vNJ0PKfBlDoQs7n81CbO6Q0zdL7H1EzqYRrTVXDqdBVqro0aJjo/FOa1qBAPVI4PGSHBw==", + "dependencies": { + "@vue/compiler-core": "3.3.7", + "@vue/shared": "3.3.7" + } + }, + "node_modules/@vue/compiler-sfc": { + "version": "3.3.7", + "resolved": "https://registry.npmjs.org/@vue/compiler-sfc/-/compiler-sfc-3.3.7.tgz", + "integrity": "sha512-7pfldWy/J75U/ZyYIXRVqvLRw3vmfxDo2YLMwVtWVNew8Sm8d6wodM+OYFq4ll/UxfqVr0XKiVwti32PCrruAw==", + "dependencies": { + "@babel/parser": "^7.23.0", + "@vue/compiler-core": "3.3.7", + "@vue/compiler-dom": "3.3.7", + "@vue/compiler-ssr": "3.3.7", + "@vue/reactivity-transform": "3.3.7", + "@vue/shared": "3.3.7", + "estree-walker": "^2.0.2", + "magic-string": "^0.30.5", + "postcss": "^8.4.31", + "source-map-js": "^1.0.2" + } + }, + "node_modules/@vue/compiler-ssr": { + "version": "3.3.7", + "resolved": "https://registry.npmjs.org/@vue/compiler-ssr/-/compiler-ssr-3.3.7.tgz", + "integrity": "sha512-TxOfNVVeH3zgBc82kcUv+emNHo+vKnlRrkv8YvQU5+Y5LJGJwSNzcmLUoxD/dNzv0bhQ/F0s+InlgV0NrApJZg==", + "dependencies": { + "@vue/compiler-dom": "3.3.7", + "@vue/shared": "3.3.7" + } + }, + "node_modules/@vue/devtools-api": { + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/@vue/devtools-api/-/devtools-api-6.5.0.tgz", + "integrity": "sha512-o9KfBeaBmCKl10usN4crU53fYtC1r7jJwdGKjPT24t348rHxgfpZ0xL3Xm/gLUYnc0oTp8LAmrxOeLyu6tbk2Q==" + }, + "node_modules/@vue/eslint-config-typescript": { + "version": "12.0.0", + "resolved": "https://registry.npmjs.org/@vue/eslint-config-typescript/-/eslint-config-typescript-12.0.0.tgz", + "integrity": "sha512-StxLFet2Qe97T8+7L8pGlhYBBr8Eg05LPuTDVopQV6il+SK6qqom59BA/rcFipUef2jD8P2X44Vd8tMFytfvlg==", + "dev": true, + "dependencies": { + "@typescript-eslint/eslint-plugin": "^6.7.0", + "@typescript-eslint/parser": "^6.7.0", + "vue-eslint-parser": "^9.3.1" + }, + "engines": { + "node": "^14.17.0 || >=16.0.0" + }, + "peerDependencies": { + "eslint": "^6.2.0 || ^7.0.0 || ^8.0.0", + "eslint-plugin-vue": "^9.0.0", + "typescript": "*" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/@vue/language-core": { + "version": "1.8.22", + "resolved": "https://registry.npmjs.org/@vue/language-core/-/language-core-1.8.22.tgz", + "integrity": "sha512-bsMoJzCrXZqGsxawtUea1cLjUT9dZnDsy5TuZ+l1fxRMzUGQUG9+Ypq4w//CqpWmrx7nIAJpw2JVF/t258miRw==", + "dev": true, + "dependencies": { + "@volar/language-core": "~1.10.5", + "@volar/source-map": "~1.10.5", + "@vue/compiler-dom": "^3.3.0", + "@vue/shared": "^3.3.0", + "computeds": "^0.0.1", + "minimatch": "^9.0.3", + "muggle-string": "^0.3.1", + "vue-template-compiler": "^2.7.14" + }, + "peerDependencies": { + "typescript": "*" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/@vue/language-core/node_modules/brace-expansion": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", + "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", + "dev": true, + "dependencies": { + "balanced-match": "^1.0.0" + } + }, + "node_modules/@vue/language-core/node_modules/minimatch": { + "version": "9.0.3", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.3.tgz", + "integrity": "sha512-RHiac9mvaRw0x3AYRgDC1CxAP7HTcNrrECeA8YYJeWnpo+2Q5CegtZjaotWTWxDG3UeGA1coE05iH1mPjT/2mg==", + "dev": true, + "dependencies": { + "brace-expansion": "^2.0.1" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/@vue/reactivity": { + "version": "3.3.7", + "resolved": "https://registry.npmjs.org/@vue/reactivity/-/reactivity-3.3.7.tgz", + "integrity": "sha512-cZNVjWiw00708WqT0zRpyAgduG79dScKEPYJXq2xj/aMtk3SKvL3FBt2QKUlh6EHBJ1m8RhBY+ikBUzwc7/khg==", + "dependencies": { + "@vue/shared": "3.3.7" + } + }, + "node_modules/@vue/reactivity-transform": { + "version": "3.3.7", + "resolved": "https://registry.npmjs.org/@vue/reactivity-transform/-/reactivity-transform-3.3.7.tgz", + "integrity": "sha512-APhRmLVbgE1VPGtoLQoWBJEaQk4V8JUsqrQihImVqKT+8U6Qi3t5ATcg4Y9wGAPb3kIhetpufyZ1RhwbZCIdDA==", + "dependencies": { + "@babel/parser": "^7.23.0", + "@vue/compiler-core": "3.3.7", + "@vue/shared": "3.3.7", + "estree-walker": "^2.0.2", + "magic-string": "^0.30.5" + } + }, + "node_modules/@vue/runtime-core": { + "version": "3.3.7", + "resolved": "https://registry.npmjs.org/@vue/runtime-core/-/runtime-core-3.3.7.tgz", + "integrity": "sha512-LHq9du3ubLZFdK/BP0Ysy3zhHqRfBn80Uc+T5Hz3maFJBGhci1MafccnL3rpd5/3wVfRHAe6c+PnlO2PAavPTQ==", + "dependencies": { + "@vue/reactivity": "3.3.7", + "@vue/shared": "3.3.7" + } + }, + "node_modules/@vue/runtime-dom": { + "version": "3.3.7", + "resolved": "https://registry.npmjs.org/@vue/runtime-dom/-/runtime-dom-3.3.7.tgz", + "integrity": "sha512-PFQU1oeJxikdDmrfoNQay5nD4tcPNYixUBruZzVX/l0eyZvFKElZUjW4KctCcs52nnpMGO6UDK+jF5oV4GT5Lw==", + "dependencies": { + "@vue/runtime-core": "3.3.7", + "@vue/shared": "3.3.7", + "csstype": "^3.1.2" + } + }, + "node_modules/@vue/server-renderer": { + "version": "3.3.7", + "resolved": "https://registry.npmjs.org/@vue/server-renderer/-/server-renderer-3.3.7.tgz", + "integrity": "sha512-UlpKDInd1hIZiNuVVVvLgxpfnSouxKQOSE2bOfQpBuGwxRV/JqqTCyyjXUWiwtVMyeRaZhOYYqntxElk8FhBhw==", + "dependencies": { + "@vue/compiler-ssr": "3.3.7", + "@vue/shared": "3.3.7" + }, + "peerDependencies": { + "vue": "3.3.7" + } + }, + "node_modules/@vue/shared": { + "version": "3.3.7", + "resolved": "https://registry.npmjs.org/@vue/shared/-/shared-3.3.7.tgz", + "integrity": "sha512-N/tbkINRUDExgcPTBvxNkvHGu504k8lzlNQRITVnm6YjOjwa4r0nnbd4Jb01sNpur5hAllyRJzSK5PvB9PPwRg==" + }, + "node_modules/@vue/test-utils": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/@vue/test-utils/-/test-utils-2.4.1.tgz", + "integrity": "sha512-VO8nragneNzUZUah6kOjiFmD/gwRjUauG9DROh6oaOeFwX1cZRUNHhdeogE8635cISigXFTtGLUQWx5KCb0xeg==", + "dev": true, + "dependencies": { + "js-beautify": "1.14.9", + "vue-component-type-helpers": "1.8.4" + }, + "peerDependencies": { + "@vue/server-renderer": "^3.0.1", + "vue": "^3.0.1" + }, + "peerDependenciesMeta": { + "@vue/server-renderer": { + "optional": true + } + } + }, + "node_modules/@vue/tsconfig": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/@vue/tsconfig/-/tsconfig-0.4.0.tgz", + "integrity": "sha512-CPuIReonid9+zOG/CGTT05FXrPYATEqoDGNrEaqS4hwcw5BUNM2FguC0mOwJD4Jr16UpRVl9N0pY3P+srIbqmg==", + "dev": true + }, + "node_modules/abab": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/abab/-/abab-2.0.6.tgz", + "integrity": "sha512-j2afSsaIENvHZN2B8GOpF566vZ5WVk5opAiMTvWgaQT8DkbOqsTfvNAvHoRGU2zzP8cPoqys+xHTRDWW8L+/BA==", + "dev": true, + "optional": true, + "peer": true + }, + "node_modules/abbrev": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/abbrev/-/abbrev-1.1.1.tgz", + "integrity": "sha512-nne9/IiQ/hzIhY6pdDnbBtz7DjPTKrY00P/zvPSm5pOFkl6xuGrGnXn/VtTNNfNtAfZ9/1RtehkszU9qcTii0Q==", + "dev": true + }, + "node_modules/acorn": { + "version": "8.10.0", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.10.0.tgz", + "integrity": "sha512-F0SAmZ8iUtS//m8DmCTA0jlh6TDKkHQyK6xc6V4KDTyZKA9dnvX9/3sRTVQrWm79glUAZbnmmNcdYwUIHWVybw==", + "dev": true, + "bin": { + "acorn": "bin/acorn" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/acorn-jsx": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.3.2.tgz", + "integrity": "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==", + "dev": true, + "peerDependencies": { + "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" + } + }, + "node_modules/acorn-walk": { + "version": "8.2.0", + "resolved": "https://registry.npmjs.org/acorn-walk/-/acorn-walk-8.2.0.tgz", + "integrity": "sha512-k+iyHEuPgSw6SbuDpGQM+06HQUa04DZ3o+F6CSzXMvvI5KMvnaEqXe+YVe555R9nn6GPt404fos4wcgpw12SDA==", + "dev": true, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/agent-base": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-6.0.2.tgz", + "integrity": "sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ==", + "dev": true, + "optional": true, + "peer": true, + "dependencies": { + "debug": "4" + }, + "engines": { + "node": ">= 6.0.0" + } + }, + "node_modules/ajv": { + "version": "6.12.6", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", + "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", + "dev": true, + "dependencies": { + "fast-deep-equal": "^3.1.1", + "fast-json-stable-stringify": "^2.0.0", + "json-schema-traverse": "^0.4.1", + "uri-js": "^4.2.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/ansi-styles": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz", + "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==", + "dev": true, + "dependencies": { + "color-convert": "^1.9.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/anymatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.2.tgz", + "integrity": "sha512-P43ePfOAIupkguHUycrc4qJ9kz8ZiuOUijaETwX7THt0Y/GNK7v0aa8rY816xWjZ7rJdA5XdMcpVFTKMq+RvWg==", + "dev": true, + "dependencies": { + "normalize-path": "^3.0.0", + "picomatch": "^2.0.4" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/arg": { + "version": "4.1.3", + "resolved": "https://registry.npmjs.org/arg/-/arg-4.1.3.tgz", + "integrity": "sha512-58S9QDqG0Xx27YwPSt9fJxivjYl432YCwfDMfZ+71RAqUrZef7LrKQZ3LHLOwCS4FLNBplP533Zx895SeOCHvA==", + "dev": true + }, + "node_modules/argparse": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", + "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", + "dev": true + }, + "node_modules/aria-query": { + "version": "5.1.3", + "resolved": "https://registry.npmjs.org/aria-query/-/aria-query-5.1.3.tgz", + "integrity": "sha512-R5iJ5lkuHybztUfuOAznmboyjWq8O6sqNqtK7CLOqdydi54VNbORp49mb14KbWgG1QD3JFO9hJdZ+y4KutfdOQ==", + "dev": true, + "dependencies": { + "deep-equal": "^2.0.5" + } + }, + "node_modules/array-buffer-byte-length": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/array-buffer-byte-length/-/array-buffer-byte-length-1.0.0.tgz", + "integrity": "sha512-LPuwb2P+NrQw3XhxGc36+XSvuBPopovXYTR9Ew++Du9Yb/bx5AzBfrIsBoj0EZUifjQU+sHL21sseZ3jerWO/A==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.2", + "is-array-buffer": "^3.0.1" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/array-union": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/array-union/-/array-union-2.1.0.tgz", + "integrity": "sha512-HGyxoOTYUyCM6stUe6EJgnd4EoewAI7zMdfqO+kGjnlZmBDz/cR5pf8r/cR4Wq60sL/p0IkcjUEEPwS3GFrIyw==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/assertion-error": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/assertion-error/-/assertion-error-1.1.0.tgz", + "integrity": "sha512-jgsaNduz+ndvGyFt3uSuWqvy4lCnIJiovtouQN5JZHOKCS2QuhEdbcQHFhVksz2N2U9hXJo8odG7ETyWlEeuDw==", + "dev": true, + "engines": { + "node": "*" + } + }, + "node_modules/asynckit": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", + "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==" + }, + "node_modules/available-typed-arrays": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/available-typed-arrays/-/available-typed-arrays-1.0.5.tgz", + "integrity": "sha512-DMD0KiN46eipeziST1LPP/STfDU0sufISXmjSgvVsoU2tqxctQeASejWcfNtxYKqETM1UxQ8sp2OrSBWpHY6sw==", + "dev": true, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/axios": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/axios/-/axios-1.6.0.tgz", + "integrity": "sha512-EZ1DYihju9pwVB+jg67ogm+Tmqc6JmhamRN6I4Zt8DfZu5lbcQGw3ozH9lFejSJgs/ibaef3A9PMXPLeefFGJg==", + "dependencies": { + "follow-redirects": "^1.15.0", + "form-data": "^4.0.0", + "proxy-from-env": "^1.1.0" + } + }, + "node_modules/balanced-match": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", + "dev": true + }, + "node_modules/binary-extensions": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.2.0.tgz", + "integrity": "sha512-jDctJ/IVQbZoJykoeHbhXpOlNBqGNcwXJKJog42E5HDPUwQTSdjCHdihjj0DlnheQ7blbT6dHOafNAiS8ooQKA==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/boolbase": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/boolbase/-/boolbase-1.0.0.tgz", + "integrity": "sha512-JZOSA7Mo9sNGB8+UjSgzdLtokWAky1zbztM3WRLCbZ70/3cTANmQmOdR7y2g+J0e2WXywy1yS468tY+IruqEww==", + "dev": true + }, + "node_modules/brace-expansion": { + "version": "1.1.11", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", + "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "dev": true, + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/braces": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.2.tgz", + "integrity": "sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A==", + "dev": true, + "dependencies": { + "fill-range": "^7.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/browserslist": { + "version": "4.21.10", + "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.21.10.tgz", + "integrity": "sha512-bipEBdZfVH5/pwrvqc+Ub0kUPVfGUhlKxbvfD+z1BDnPEO/X98ruXGA1WP5ASpAFKan7Qr6j736IacbZQuAlKQ==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "dependencies": { + "caniuse-lite": "^1.0.30001517", + "electron-to-chromium": "^1.4.477", + "node-releases": "^2.0.13", + "update-browserslist-db": "^1.0.11" + }, + "bin": { + "browserslist": "cli.js" + }, + "engines": { + "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7" + } + }, + "node_modules/c8": { + "version": "7.14.0", + "resolved": "https://registry.npmjs.org/c8/-/c8-7.14.0.tgz", + "integrity": "sha512-i04rtkkcNcCf7zsQcSv/T9EbUn4RXQ6mropeMcjFOsQXQ0iGLAr/xT6TImQg4+U9hmNpN9XdvPkjUL1IzbgxJw==", + "dev": true, + "dependencies": { + "@bcoe/v8-coverage": "^0.2.3", + "@istanbuljs/schema": "^0.1.3", + "find-up": "^5.0.0", + "foreground-child": "^2.0.0", + "istanbul-lib-coverage": "^3.2.0", + "istanbul-lib-report": "^3.0.0", + "istanbul-reports": "^3.1.4", + "rimraf": "^3.0.2", + "test-exclude": "^6.0.0", + "v8-to-istanbul": "^9.0.0", + "yargs": "^16.2.0", + "yargs-parser": "^20.2.9" + }, + "bin": { + "c8": "bin/c8.js" + }, + "engines": { + "node": ">=10.12.0" + } + }, + "node_modules/c8/node_modules/rimraf": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz", + "integrity": "sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==", + "dev": true, + "dependencies": { + "glob": "^7.1.3" + }, + "bin": { + "rimraf": "bin.js" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/cac": { + "version": "6.7.14", + "resolved": "https://registry.npmjs.org/cac/-/cac-6.7.14.tgz", + "integrity": "sha512-b6Ilus+c3RrdDk+JhLKUAQfzzgLEPy6wcXqS7f/xe1EETvsDP6GORG7SFuOs6cID5YkqchW/LXZbX5bc8j7ZcQ==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/call-bind": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.5.tgz", + "integrity": "sha512-C3nQxfFZxFRVoJoGKKI8y3MOEo129NQ+FgQ08iye+Mk4zNZZGdjfs06bVTr+DBSlA66Q2VEcMki/cUCP4SercQ==", + "dev": true, + "dependencies": { + "function-bind": "^1.1.2", + "get-intrinsic": "^1.2.1", + "set-function-length": "^1.1.1" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/callsites": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz", + "integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==", + "dev": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/camelcase": { + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-6.3.0.tgz", + "integrity": "sha512-Gmy6FhYlCY7uOElZUSbxo2UCDH8owEk996gkbrpsgGtrJLM3J7jGxl9Ic7Qwwj4ivOE5AWZWRMecDdF7hqGjFA==", + "dev": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/caniuse-lite": { + "version": "1.0.30001524", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001524.tgz", + "integrity": "sha512-Jj917pJtYg9HSJBF95HVX3Cdr89JUyLT4IZ8SvM5aDRni95swKgYi3TgYLH5hnGfPE/U1dg6IfZ50UsIlLkwSA==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/caniuse-lite" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ] + }, + "node_modules/chai": { + "version": "4.3.10", + "resolved": "https://registry.npmjs.org/chai/-/chai-4.3.10.tgz", + "integrity": "sha512-0UXG04VuVbruMUYbJ6JctvH0YnC/4q3/AkT18q4NaITo91CUm0liMS9VqzT9vZhVQ/1eqPanMWjBM+Juhfb/9g==", + "dev": true, + "dependencies": { + "assertion-error": "^1.1.0", + "check-error": "^1.0.3", + "deep-eql": "^4.1.3", + "get-func-name": "^2.0.2", + "loupe": "^2.3.6", + "pathval": "^1.1.1", + "type-detect": "^4.0.8" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/chalk": { + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", + "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==", + "dev": true, + "dependencies": { + "ansi-styles": "^3.2.1", + "escape-string-regexp": "^1.0.5", + "supports-color": "^5.3.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/check-error": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/check-error/-/check-error-1.0.3.tgz", + "integrity": "sha512-iKEoDYaRmd1mxM90a2OEfWhjsjPpYPuQ+lMYsoxB126+t8fw7ySEO48nmDg5COTjxDI65/Y2OWpeEHk3ZOe8zg==", + "dev": true, + "dependencies": { + "get-func-name": "^2.0.2" + }, + "engines": { + "node": "*" + } + }, + "node_modules/chokidar": { + "version": "3.5.3", + "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.5.3.tgz", + "integrity": "sha512-Dr3sfKRP6oTcjf2JmUmFJfeVMvXBdegxB0iVQ5eb2V10uFJUCAS8OByZdVAyVb8xXNz3GjjTgj9kLWsZTqE6kw==", + "dev": true, + "funding": [ + { + "type": "individual", + "url": "https://paulmillr.com/funding/" + } + ], + "dependencies": { + "anymatch": "~3.1.2", + "braces": "~3.0.2", + "glob-parent": "~5.1.2", + "is-binary-path": "~2.1.0", + "is-glob": "~4.0.1", + "normalize-path": "~3.0.0", + "readdirp": "~3.6.0" + }, + "engines": { + "node": ">= 8.10.0" + }, + "optionalDependencies": { + "fsevents": "~2.3.2" + } + }, + "node_modules/chokidar/node_modules/glob-parent": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", + "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", + "dev": true, + "dependencies": { + "is-glob": "^4.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/cliui": { + "version": "7.0.4", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-7.0.4.tgz", + "integrity": "sha512-OcRE68cOsVMXp1Yvonl/fzkQOyjLSu/8bhPDfQt0e0/Eb283TKP20Fs2MqoPsr9SwA595rRCA+QMzYc9nBP+JQ==", + "dev": true, + "dependencies": { + "string-width": "^4.2.0", + "strip-ansi": "^6.0.0", + "wrap-ansi": "^7.0.0" + } + }, + "node_modules/cliui/node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "dev": true + }, + "node_modules/cliui/node_modules/string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "dev": true, + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/color-convert": { + "version": "1.9.3", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz", + "integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==", + "dev": true, + "dependencies": { + "color-name": "1.1.3" + } + }, + "node_modules/color-name": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz", + "integrity": "sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw==", + "dev": true + }, + "node_modules/combined-stream": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", + "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", + "dependencies": { + "delayed-stream": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/commander": { + "version": "10.0.1", + "resolved": "https://registry.npmjs.org/commander/-/commander-10.0.1.tgz", + "integrity": "sha512-y4Mg2tXshplEbSGzx7amzPwKKOCGuoSRP/CjEdwwk0FOGlUbq6lKuoyDZTNZkmxHdJtp54hdfY/JUrdL7Xfdug==", + "dev": true, + "engines": { + "node": ">=14" + } + }, + "node_modules/computeds": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/computeds/-/computeds-0.0.1.tgz", + "integrity": "sha512-7CEBgcMjVmitjYo5q8JTJVra6X5mQ20uTThdK+0kR7UEaDrAWEQcRiBtWJzga4eRpP6afNwwLsX2SET2JhVB1Q==", + "dev": true + }, + "node_modules/concat-map": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", + "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==", + "dev": true + }, + "node_modules/config-chain": { + "version": "1.1.13", + "resolved": "https://registry.npmjs.org/config-chain/-/config-chain-1.1.13.tgz", + "integrity": "sha512-qj+f8APARXHrM0hraqXYb2/bOVSV4PvJQlNZ/DVj0QrmNM2q2euizkeuVckQ57J+W0mRH6Hvi+k50M4Jul2VRQ==", + "dev": true, + "dependencies": { + "ini": "^1.3.4", + "proto-list": "~1.2.1" + } + }, + "node_modules/convert-source-map": { + "version": "1.9.0", + "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-1.9.0.tgz", + "integrity": "sha512-ASFBup0Mz1uyiIjANan1jzLQami9z1PoYSZCiiYW2FczPbenXc45FZdBZLzOT+r6+iciuEModtmCti+hjaAk0A==", + "dev": true + }, + "node_modules/create-require": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/create-require/-/create-require-1.1.1.tgz", + "integrity": "sha512-dcKFX3jn0MpIaXjisoRvexIJVEKzaq7z2rZKxf+MSr9TkdmHmsU4m2lcLojrj/FHl8mk5VxMmYA+ftRkP/3oKQ==", + "dev": true + }, + "node_modules/cross-spawn": { + "version": "7.0.3", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.3.tgz", + "integrity": "sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w==", + "dev": true, + "dependencies": { + "path-key": "^3.1.0", + "shebang-command": "^2.0.0", + "which": "^2.0.1" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/crypto-js": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/crypto-js/-/crypto-js-4.2.0.tgz", + "integrity": "sha512-KALDyEYgpY+Rlob/iriUtjV6d5Eq+Y191A5g4UqLAi8CyGP9N1+FdVbkc1SxKc2r4YAYqG8JzO2KGL+AizD70Q==" + }, + "node_modules/css.escape": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/css.escape/-/css.escape-1.5.1.tgz", + "integrity": "sha512-YUifsXXuknHlUsmlgyY0PKzgPOr7/FjCePfHNt0jxm83wHZi44VDMQ7/fGNkjY3/jV1MC+1CmZbaHzugyeRtpg==", + "dev": true + }, + "node_modules/cssesc": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/cssesc/-/cssesc-3.0.0.tgz", + "integrity": "sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg==", + "dev": true, + "bin": { + "cssesc": "bin/cssesc" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/cssstyle": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/cssstyle/-/cssstyle-3.0.0.tgz", + "integrity": "sha512-N4u2ABATi3Qplzf0hWbVCdjenim8F3ojEXpBDF5hBpjzW182MjNGLqfmQ0SkSPeQ+V86ZXgeH8aXj6kayd4jgg==", + "dev": true, + "optional": true, + "peer": true, + "dependencies": { + "rrweb-cssom": "^0.6.0" + }, + "engines": { + "node": ">=14" + } + }, + "node_modules/csstype": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.2.tgz", + "integrity": "sha512-I7K1Uu0MBPzaFKg4nI5Q7Vs2t+3gWWW648spaF+Rg7pI9ds18Ugn+lvg4SHczUdKlHI5LWBXyqfS8+DufyBsgQ==" + }, + "node_modules/data-urls": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/data-urls/-/data-urls-4.0.0.tgz", + "integrity": "sha512-/mMTei/JXPqvFqQtfyTowxmJVwr2PVAeCcDxyFf6LhoOu/09TX2OX3kb2wzi4DMXcfj4OItwDOnhl5oziPnT6g==", + "dev": true, + "optional": true, + "peer": true, + "dependencies": { + "abab": "^2.0.6", + "whatwg-mimetype": "^3.0.0", + "whatwg-url": "^12.0.0" + }, + "engines": { + "node": ">=14" + } + }, + "node_modules/data-urls/node_modules/tr46": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/tr46/-/tr46-4.1.1.tgz", + "integrity": "sha512-2lv/66T7e5yNyhAAC4NaKe5nVavzuGJQVVtRYLyQ2OI8tsJ61PMLlelehb0wi2Hx6+hT/OJUWZcw8MjlSRnxvw==", + "dev": true, + "optional": true, + "peer": true, + "dependencies": { + "punycode": "^2.3.0" + }, + "engines": { + "node": ">=14" + } + }, + "node_modules/data-urls/node_modules/whatwg-url": { + "version": "12.0.1", + "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-12.0.1.tgz", + "integrity": "sha512-Ed/LrqB8EPlGxjS+TrsXcpUond1mhccS3pchLhzSgPCnTimUCKj3IZE75pAs5m6heB2U2TMerKFUXheyHY+VDQ==", + "dev": true, + "optional": true, + "peer": true, + "dependencies": { + "tr46": "^4.1.1", + "webidl-conversions": "^7.0.0" + }, + "engines": { + "node": ">=14" + } + }, + "node_modules/date-fns": { + "version": "2.30.0", + "resolved": "https://registry.npmjs.org/date-fns/-/date-fns-2.30.0.tgz", + "integrity": "sha512-fnULvOpxnC5/Vg3NCiWelDsLiUc9bRwAPs/+LfTLNvetFCtCTN+yQz15C/fs4AwX1R9K5GLtLfn8QW+dWisaAw==", + "dependencies": { + "@babel/runtime": "^7.21.0" + }, + "engines": { + "node": ">=0.11" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/date-fns" + } + }, + "node_modules/de-indent": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/de-indent/-/de-indent-1.0.2.tgz", + "integrity": "sha512-e/1zu3xH5MQryN2zdVaF0OrdNLUbvWxzMbi+iNA6Bky7l1RoP8a2fIbRocyHclXt/arDrrR6lL3TqFD9pMQTsg==", + "dev": true + }, + "node_modules/debug": { + "version": "4.3.4", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", + "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==", + "dev": true, + "dependencies": { + "ms": "2.1.2" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/decimal.js": { + "version": "10.4.3", + "resolved": "https://registry.npmjs.org/decimal.js/-/decimal.js-10.4.3.tgz", + "integrity": "sha512-VBBaLc1MgL5XpzgIP7ny5Z6Nx3UrRkIViUkPUdtl9aya5amy3De1gsUUSB1g3+3sExYNjCAsAznmukyxCb1GRA==", + "dev": true, + "optional": true, + "peer": true + }, + "node_modules/deep-eql": { + "version": "4.1.3", + "resolved": "https://registry.npmjs.org/deep-eql/-/deep-eql-4.1.3.tgz", + "integrity": "sha512-WaEtAOpRA1MQ0eohqZjpGD8zdI0Ovsm8mmFhaDN8dvDZzyoUMcYDnf5Y6iu7HTXxf8JDS23qWa4a+hKCDyOPzw==", + "dev": true, + "dependencies": { + "type-detect": "^4.0.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/deep-equal": { + "version": "2.2.2", + "resolved": "https://registry.npmjs.org/deep-equal/-/deep-equal-2.2.2.tgz", + "integrity": "sha512-xjVyBf0w5vH0I42jdAZzOKVldmPgSulmiyPRywoyq7HXC9qdgo17kxJE+rdnif5Tz6+pIrpJI8dCpMNLIGkUiA==", + "dev": true, + "dependencies": { + "array-buffer-byte-length": "^1.0.0", + "call-bind": "^1.0.2", + "es-get-iterator": "^1.1.3", + "get-intrinsic": "^1.2.1", + "is-arguments": "^1.1.1", + "is-array-buffer": "^3.0.2", + "is-date-object": "^1.0.5", + "is-regex": "^1.1.4", + "is-shared-array-buffer": "^1.0.2", + "isarray": "^2.0.5", + "object-is": "^1.1.5", + "object-keys": "^1.1.1", + "object.assign": "^4.1.4", + "regexp.prototype.flags": "^1.5.0", + "side-channel": "^1.0.4", + "which-boxed-primitive": "^1.0.2", + "which-collection": "^1.0.1", + "which-typed-array": "^1.1.9" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/deep-is": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz", + "integrity": "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==", + "dev": true + }, + "node_modules/define-data-property": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/define-data-property/-/define-data-property-1.1.1.tgz", + "integrity": "sha512-E7uGkTzkk1d0ByLeSc6ZsFS79Axg+m1P/VsgYsxHgiuc3tFSj+MjMIwe90FC4lOAZzNBdY7kkO2P2wKdsQ1vgQ==", + "dev": true, + "dependencies": { + "get-intrinsic": "^1.2.1", + "gopd": "^1.0.1", + "has-property-descriptors": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/define-properties": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/define-properties/-/define-properties-1.2.1.tgz", + "integrity": "sha512-8QmQKqEASLd5nx0U1B1okLElbUuuttJ/AnYmRXbbbGDWh6uS208EjD4Xqq/I9wK7u0v6O08XhTWnt5XtEbR6Dg==", + "dev": true, + "dependencies": { + "define-data-property": "^1.0.1", + "has-property-descriptors": "^1.0.0", + "object-keys": "^1.1.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/delayed-stream": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", + "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==", + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/diff": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/diff/-/diff-4.0.2.tgz", + "integrity": "sha512-58lmxKSA4BNyLz+HHMUzlOEpg09FV+ev6ZMe3vJihgdxzgcwZ8VoEEPmALCZG9LmqfVoNMMKpttIYTVG6uDY7A==", + "dev": true, + "engines": { + "node": ">=0.3.1" + } + }, + "node_modules/diff-sequences": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/diff-sequences/-/diff-sequences-29.6.3.tgz", + "integrity": "sha512-EjePK1srD3P08o2j4f0ExnylqRs5B9tJjcp9t1krH2qRi8CCdsYfwe9JgSLurFBWwq4uOlipzfk5fHNvwFKr8Q==", + "dev": true, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/dir-glob": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/dir-glob/-/dir-glob-3.0.1.tgz", + "integrity": "sha512-WkrWp9GR4KXfKGYzOLmTuGVi1UWFfws377n9cc55/tb6DuqyF6pcQ5AbiHEshaDpY9v6oaSr2XCDidGmMwdzIA==", + "dev": true, + "dependencies": { + "path-type": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/doctrine": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/doctrine/-/doctrine-3.0.0.tgz", + "integrity": "sha512-yS+Q5i3hBf7GBkd4KG8a7eBNNWNGLTaEwwYWUijIYM7zrlYDM0BFXHjjPWlWZ1Rg7UaddZeIDmi9jF3HmqiQ2w==", + "dev": true, + "dependencies": { + "esutils": "^2.0.2" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/dom-accessibility-api": { + "version": "0.5.16", + "resolved": "https://registry.npmjs.org/dom-accessibility-api/-/dom-accessibility-api-0.5.16.tgz", + "integrity": "sha512-X7BJ2yElsnOJ30pZF4uIIDfBEVgF4XEBxL9Bxhy6dnrm5hkzqmsWHGTiHqRiITNhMyFLyAiWndIJP7Z1NTteDg==", + "dev": true + }, + "node_modules/domexception": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/domexception/-/domexception-4.0.0.tgz", + "integrity": "sha512-A2is4PLG+eeSfoTMA95/s4pvAoSo2mKtiM5jlHkAVewmiO8ISFTFKZjH7UAM1Atli/OT/7JHOrJRJiMKUZKYBw==", + "dev": true, + "optional": true, + "peer": true, + "dependencies": { + "webidl-conversions": "^7.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/eastasianwidth": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/eastasianwidth/-/eastasianwidth-0.2.0.tgz", + "integrity": "sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==", + "dev": true + }, + "node_modules/editorconfig": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/editorconfig/-/editorconfig-1.0.4.tgz", + "integrity": "sha512-L9Qe08KWTlqYMVvMcTIvMAdl1cDUubzRNYL+WfA4bLDMHe4nemKkpmYzkznE1FwLKu0EEmy6obgQKzMJrg4x9Q==", + "dev": true, + "dependencies": { + "@one-ini/wasm": "0.1.1", + "commander": "^10.0.0", + "minimatch": "9.0.1", + "semver": "^7.5.3" + }, + "bin": { + "editorconfig": "bin/editorconfig" + }, + "engines": { + "node": ">=14" + } + }, + "node_modules/editorconfig/node_modules/brace-expansion": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", + "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", + "dev": true, + "dependencies": { + "balanced-match": "^1.0.0" + } + }, + "node_modules/editorconfig/node_modules/minimatch": { + "version": "9.0.1", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.1.tgz", + "integrity": "sha512-0jWhJpD/MdhPXwPuiRkCbfYfSKp2qnn2eOc279qI7f+osl/l+prKSrvhg157zSYvx/1nmgn2NqdT6k2Z7zSH9w==", + "dev": true, + "dependencies": { + "brace-expansion": "^2.0.1" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/editorconfig/node_modules/semver": { + "version": "7.5.4", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.5.4.tgz", + "integrity": "sha512-1bCSESV6Pv+i21Hvpxp3Dx+pSD8lIPt8uVjRrxAUt/nbswYc+tK6Y2btiULjd4+fnq15PX+nqQDC7Oft7WkwcA==", + "dev": true, + "dependencies": { + "lru-cache": "^6.0.0" + }, + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/electron-to-chromium": { + "version": "1.4.505", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.4.505.tgz", + "integrity": "sha512-0A50eL5BCCKdxig2SsCXhpuztnB9PfUgRMojj5tMvt8O54lbwz3t6wNgnpiTRosw5QjlJB7ixhVyeg8daLQwSQ==", + "dev": true + }, + "node_modules/emoji-regex": { + "version": "9.2.2", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-9.2.2.tgz", + "integrity": "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==", + "dev": true + }, + "node_modules/entities": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/entities/-/entities-4.5.0.tgz", + "integrity": "sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==", + "dev": true, + "engines": { + "node": ">=0.12" + }, + "funding": { + "url": "https://github.com/fb55/entities?sponsor=1" + } + }, + "node_modules/es-get-iterator": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/es-get-iterator/-/es-get-iterator-1.1.3.tgz", + "integrity": "sha512-sPZmqHBe6JIiTfN5q2pEi//TwxmAFHwj/XEuYjTuse78i8KxaqMTTzxPoFKuzRpDpTJ+0NAbpfenkmH2rePtuw==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.2", + "get-intrinsic": "^1.1.3", + "has-symbols": "^1.0.3", + "is-arguments": "^1.1.1", + "is-map": "^2.0.2", + "is-set": "^2.0.2", + "is-string": "^1.0.7", + "isarray": "^2.0.5", + "stop-iteration-iterator": "^1.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/esbuild": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.18.20.tgz", + "integrity": "sha512-ceqxoedUrcayh7Y7ZX6NdbbDzGROiyVBgC4PriJThBKSVPWnnFHZAkfI1lJT8QFkOwH4qOS2SJkS4wvpGl8BpA==", + "dev": true, + "hasInstallScript": true, + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=12" + }, + "optionalDependencies": { + "@esbuild/android-arm": "0.18.20", + "@esbuild/android-arm64": "0.18.20", + "@esbuild/android-x64": "0.18.20", + "@esbuild/darwin-arm64": "0.18.20", + "@esbuild/darwin-x64": "0.18.20", + "@esbuild/freebsd-arm64": "0.18.20", + "@esbuild/freebsd-x64": "0.18.20", + "@esbuild/linux-arm": "0.18.20", + "@esbuild/linux-arm64": "0.18.20", + "@esbuild/linux-ia32": "0.18.20", + "@esbuild/linux-loong64": "0.18.20", + "@esbuild/linux-mips64el": "0.18.20", + "@esbuild/linux-ppc64": "0.18.20", + "@esbuild/linux-riscv64": "0.18.20", + "@esbuild/linux-s390x": "0.18.20", + "@esbuild/linux-x64": "0.18.20", + "@esbuild/netbsd-x64": "0.18.20", + "@esbuild/openbsd-x64": "0.18.20", + "@esbuild/sunos-x64": "0.18.20", + "@esbuild/win32-arm64": "0.18.20", + "@esbuild/win32-ia32": "0.18.20", + "@esbuild/win32-x64": "0.18.20" + } + }, + "node_modules/escalade": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.1.1.tgz", + "integrity": "sha512-k0er2gUkLf8O0zKJiAhmkTnJlTvINGv7ygDNPbeIsX/TJjGJZHuh9B2UxbsaEkmlEo9MfhrSzmhIlhRlI2GXnw==", + "dev": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/escape-string-regexp": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", + "integrity": "sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg==", + "dev": true, + "engines": { + "node": ">=0.8.0" + } + }, + "node_modules/eslint": { + "version": "8.52.0", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-8.52.0.tgz", + "integrity": "sha512-zh/JHnaixqHZsolRB/w9/02akBk9EPrOs9JwcTP2ek7yL5bVvXuRariiaAjjoJ5DvuwQ1WAE/HsMz+w17YgBCg==", + "dev": true, + "dependencies": { + "@eslint-community/eslint-utils": "^4.2.0", + "@eslint-community/regexpp": "^4.6.1", + "@eslint/eslintrc": "^2.1.2", + "@eslint/js": "8.52.0", + "@humanwhocodes/config-array": "^0.11.13", + "@humanwhocodes/module-importer": "^1.0.1", + "@nodelib/fs.walk": "^1.2.8", + "@ungap/structured-clone": "^1.2.0", + "ajv": "^6.12.4", + "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.2.2", + "eslint-visitor-keys": "^3.4.3", + "espree": "^9.6.1", + "esquery": "^1.4.2", + "esutils": "^2.0.2", + "fast-deep-equal": "^3.1.3", + "file-entry-cache": "^6.0.1", + "find-up": "^5.0.0", + "glob-parent": "^6.0.2", + "globals": "^13.19.0", + "graphemer": "^1.4.0", + "ignore": "^5.2.0", + "imurmurhash": "^0.1.4", + "is-glob": "^4.0.0", + "is-path-inside": "^3.0.3", + "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.3", + "strip-ansi": "^6.0.1", + "text-table": "^0.2.0" + }, + "bin": { + "eslint": "bin/eslint.js" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/eslint-config-prettier": { + "version": "9.0.0", + "resolved": "https://registry.npmjs.org/eslint-config-prettier/-/eslint-config-prettier-9.0.0.tgz", + "integrity": "sha512-IcJsTkJae2S35pRsRAwoCE+925rJJStOdkKnLVgtE+tEpqU0EVVM7OqrwxqgptKdX29NUwC82I5pXsGFIgSevw==", + "dev": true, + "bin": { + "eslint-config-prettier": "bin/cli.js" + }, + "peerDependencies": { + "eslint": ">=7.0.0" + } + }, + "node_modules/eslint-plugin-vitest-globals": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/eslint-plugin-vitest-globals/-/eslint-plugin-vitest-globals-1.4.0.tgz", + "integrity": "sha512-WE+YlK9X9s4vf5EaYRU0Scw7WItDZStm+PapFSYlg2ABNtaQ4zIG7wEqpoUB3SlfM+SgkhgmzR0TeJOO5k3/Nw==", + "dev": true + }, + "node_modules/eslint-plugin-vue": { + "version": "9.18.1", + "resolved": "https://registry.npmjs.org/eslint-plugin-vue/-/eslint-plugin-vue-9.18.1.tgz", + "integrity": "sha512-7hZFlrEgg9NIzuVik2I9xSnJA5RsmOfueYgsUGUokEDLJ1LHtxO0Pl4duje1BriZ/jDWb+44tcIlC3yi0tdlZg==", + "dev": true, + "dependencies": { + "@eslint-community/eslint-utils": "^4.4.0", + "natural-compare": "^1.4.0", + "nth-check": "^2.1.1", + "postcss-selector-parser": "^6.0.13", + "semver": "^7.5.4", + "vue-eslint-parser": "^9.3.1", + "xml-name-validator": "^4.0.0" + }, + "engines": { + "node": "^14.17.0 || >=16.0.0" + }, + "peerDependencies": { + "eslint": "^6.2.0 || ^7.0.0 || ^8.0.0" + } + }, + "node_modules/eslint-plugin-vue/node_modules/semver": { + "version": "7.5.4", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.5.4.tgz", + "integrity": "sha512-1bCSESV6Pv+i21Hvpxp3Dx+pSD8lIPt8uVjRrxAUt/nbswYc+tK6Y2btiULjd4+fnq15PX+nqQDC7Oft7WkwcA==", + "dev": true, + "dependencies": { + "lru-cache": "^6.0.0" + }, + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/eslint-visitor-keys": { + "version": "3.4.3", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.4.3.tgz", + "integrity": "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==", + "dev": true, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/eslint/node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/eslint/node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/eslint/node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/eslint/node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true + }, + "node_modules/eslint/node_modules/escape-string-regexp": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", + "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==", + "dev": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/eslint/node_modules/eslint-scope": { + "version": "7.2.2", + "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-7.2.2.tgz", + "integrity": "sha512-dOt21O7lTMhDM+X9mB4GX+DZrZtCUJPL/wlcTqxyrx5IvO0IYtILdtrQGQp+8n5S0gwSVmOf9NQrjMOgfQZlIg==", + "dev": true, + "dependencies": { + "esrecurse": "^4.3.0", + "estraverse": "^5.2.0" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/eslint/node_modules/estraverse": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz", + "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==", + "dev": true, + "engines": { + "node": ">=4.0" + } + }, + "node_modules/eslint/node_modules/globals": { + "version": "13.20.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-13.20.0.tgz", + "integrity": "sha512-Qg5QtVkCy/kv3FUSlu4ukeZDVf9ee0iXLAUYX13gbR17bnejFTzr4iS9bY7kwCf1NztRNm1t91fjOiyx4CSwPQ==", + "dev": true, + "dependencies": { + "type-fest": "^0.20.2" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/eslint/node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/eslint/node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/espree": { + "version": "9.6.1", + "resolved": "https://registry.npmjs.org/espree/-/espree-9.6.1.tgz", + "integrity": "sha512-oruZaFkjorTpF32kDSI5/75ViwGeZginGGy2NoOSg3Q9bnwlnmDm4HLnkl0RE3n+njDXR037aY1+x58Z/zFdwQ==", + "dev": true, + "dependencies": { + "acorn": "^8.9.0", + "acorn-jsx": "^5.3.2", + "eslint-visitor-keys": "^3.4.1" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/esquery": { + "version": "1.4.2", + "resolved": "https://registry.npmjs.org/esquery/-/esquery-1.4.2.tgz", + "integrity": "sha512-JVSoLdTlTDkmjFmab7H/9SL9qGSyjElT3myyKp7krqjVFQCDLmj1QFaCLRFBszBKI0XVZaiiXvuPIX3ZwHe1Ng==", + "dev": true, + "dependencies": { + "estraverse": "^5.1.0" + }, + "engines": { + "node": ">=0.10" + } + }, + "node_modules/esquery/node_modules/estraverse": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz", + "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==", + "dev": true, + "engines": { + "node": ">=4.0" + } + }, + "node_modules/esrecurse": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/esrecurse/-/esrecurse-4.3.0.tgz", + "integrity": "sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==", + "dev": true, + "dependencies": { + "estraverse": "^5.2.0" + }, + "engines": { + "node": ">=4.0" + } + }, + "node_modules/esrecurse/node_modules/estraverse": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz", + "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==", + "dev": true, + "engines": { + "node": ">=4.0" + } + }, + "node_modules/estree-walker": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-2.0.2.tgz", + "integrity": "sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w==" + }, + "node_modules/esutils": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz", + "integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/fast-deep-equal": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", + "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", + "dev": true + }, + "node_modules/fast-glob": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.1.tgz", + "integrity": "sha512-kNFPyjhh5cKjrUltxs+wFx+ZkbRaxxmZ+X0ZU31SOsxCEtP9VPgtq2teZw1DebupL5GmDaNQ6yKMMVcM41iqDg==", + "dev": true, + "dependencies": { + "@nodelib/fs.stat": "^2.0.2", + "@nodelib/fs.walk": "^1.2.3", + "glob-parent": "^5.1.2", + "merge2": "^1.3.0", + "micromatch": "^4.0.4" + }, + "engines": { + "node": ">=8.6.0" + } + }, + "node_modules/fast-glob/node_modules/glob-parent": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", + "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", + "dev": true, + "dependencies": { + "is-glob": "^4.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/fast-json-stable-stringify": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", + "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==", + "dev": true + }, + "node_modules/fast-levenshtein": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz", + "integrity": "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==", + "dev": true + }, + "node_modules/fastq": { + "version": "1.13.0", + "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.13.0.tgz", + "integrity": "sha512-YpkpUnK8od0o1hmeSc7UUs/eB/vIPWJYjKck2QKIzAf71Vm1AAQ3EbuZB3g2JIy+pg+ERD0vqI79KyZiB2e2Nw==", + "dev": true, + "dependencies": { + "reusify": "^1.0.4" + } + }, + "node_modules/file-entry-cache": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-6.0.1.tgz", + "integrity": "sha512-7Gps/XWymbLk2QLYK4NzpMOrYjMhdIxXuIvy2QBsLE6ljuodKvdkWs/cpyJJ3CVIVpH0Oi1Hvg1ovbMzLdFBBg==", + "dev": true, + "dependencies": { + "flat-cache": "^3.0.4" + }, + "engines": { + "node": "^10.12.0 || >=12.0.0" + } + }, + "node_modules/filesize": { + "version": "10.1.0", + "resolved": "https://registry.npmjs.org/filesize/-/filesize-10.1.0.tgz", + "integrity": "sha512-GTLKYyBSDz3nPhlLVPjPWZCnhkd9TrrRArNcy8Z+J2cqScB7h2McAzR6NBX6nYOoWafql0roY8hrocxnZBv9CQ==", + "engines": { + "node": ">= 10.4.0" + } + }, + "node_modules/fill-range": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.0.1.tgz", + "integrity": "sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ==", + "dev": true, + "dependencies": { + "to-regex-range": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/find-up": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz", + "integrity": "sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==", + "dev": true, + "dependencies": { + "locate-path": "^6.0.0", + "path-exists": "^4.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/flat-cache": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-3.0.4.tgz", + "integrity": "sha512-dm9s5Pw7Jc0GvMYbshN6zchCA9RgQlzzEZX3vylR9IqFfS8XciblUXOKfW6SiuJ0e13eDYZoZV5wdrev7P3Nwg==", + "dev": true, + "dependencies": { + "flatted": "^3.1.0", + "rimraf": "^3.0.2" + }, + "engines": { + "node": "^10.12.0 || >=12.0.0" + } + }, + "node_modules/flat-cache/node_modules/rimraf": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz", + "integrity": "sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==", + "dev": true, + "dependencies": { + "glob": "^7.1.3" + }, + "bin": { + "rimraf": "bin.js" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/flatted": { + "version": "3.2.7", + "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.2.7.tgz", + "integrity": "sha512-5nqDSxl8nn5BSNxyR3n4I6eDmbolI6WT+QqR547RwxQapgjQBmtktdP+HTBb/a/zLsbzERTONyUB5pefh5TtjQ==", + "dev": true + }, + "node_modules/follow-redirects": { + "version": "1.15.2", + "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.2.tgz", + "integrity": "sha512-VQLG33o04KaQ8uYi2tVNbdrWp1QWxNNea+nmIB4EVM28v0hmP17z7aG1+wAkNzVq4KeXTq3221ye5qTJP91JwA==", + "funding": [ + { + "type": "individual", + "url": "https://github.com/sponsors/RubenVerborgh" + } + ], + "engines": { + "node": ">=4.0" + }, + "peerDependenciesMeta": { + "debug": { + "optional": true + } + } + }, + "node_modules/for-each": { + "version": "0.3.3", + "resolved": "https://registry.npmjs.org/for-each/-/for-each-0.3.3.tgz", + "integrity": "sha512-jqYfLp7mo9vIyQf8ykW2v7A+2N4QjeCeI5+Dz9XraiO1ign81wjiH7Fb9vSOWvQfNtmSa4H2RoQTrrXivdUZmw==", + "dev": true, + "dependencies": { + "is-callable": "^1.1.3" + } + }, + "node_modules/foreground-child": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/foreground-child/-/foreground-child-2.0.0.tgz", + "integrity": "sha512-dCIq9FpEcyQyXKCkyzmlPTFNgrCzPudOe+mhvJU5zAtlBnGVy2yKxtfsxK2tQBThwq225jcvBjpw1Gr40uzZCA==", + "dev": true, + "dependencies": { + "cross-spawn": "^7.0.0", + "signal-exit": "^3.0.2" + }, + "engines": { + "node": ">=8.0.0" + } + }, + "node_modules/form-data": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.0.tgz", + "integrity": "sha512-ETEklSGi5t0QMZuiXoA/Q6vcnxcLQP5vdugSpuAyi6SVGi2clPPp+xgEhuMaHC+zGgn31Kd235W35f7Hykkaww==", + "dependencies": { + "asynckit": "^0.4.0", + "combined-stream": "^1.0.8", + "mime-types": "^2.1.12" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/fs.realpath": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", + "integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==", + "dev": true + }, + "node_modules/fsevents": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz", + "integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==", + "dev": true, + "hasInstallScript": true, + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/function-bind": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", + "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", + "dev": true, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/functions-have-names": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/functions-have-names/-/functions-have-names-1.2.3.tgz", + "integrity": "sha512-xckBUXyTIqT97tq2x2AMb+g163b5JFysYk0x4qxNFwbfQkmNZoiRHb6sPzI9/QV33WeuvVYBUIiD4NzNIyqaRQ==", + "dev": true, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/gensync": { + "version": "1.0.0-beta.2", + "resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz", + "integrity": "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==", + "dev": true, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/get-caller-file": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz", + "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==", + "dev": true, + "engines": { + "node": "6.* || 8.* || >= 10.*" + } + }, + "node_modules/get-func-name": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/get-func-name/-/get-func-name-2.0.2.tgz", + "integrity": "sha512-8vXOvuE167CtIc3OyItco7N/dpRtBbYOsPsXCz7X/PMnlGjYjSGuZJgM1Y7mmew7BKf9BqvLX2tnOVy1BBUsxQ==", + "dev": true, + "engines": { + "node": "*" + } + }, + "node_modules/get-intrinsic": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.2.2.tgz", + "integrity": "sha512-0gSo4ml/0j98Y3lngkFEot/zhiCeWsbYIlZ+uZOVgzLyLaUw7wxUL+nCTP0XJvJg1AXulJRI3UJi8GsbDuxdGA==", + "dev": true, + "dependencies": { + "function-bind": "^1.1.2", + "has-proto": "^1.0.1", + "has-symbols": "^1.0.3", + "hasown": "^2.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/glob": { + "version": "7.2.3", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", + "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", + "dev": true, + "dependencies": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.1.1", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + }, + "engines": { + "node": "*" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/glob-parent": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz", + "integrity": "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==", + "dev": true, + "dependencies": { + "is-glob": "^4.0.3" + }, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/globals": { + "version": "11.12.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-11.12.0.tgz", + "integrity": "sha512-WOBp/EEGUiIsJSp7wcv/y6MO+lV9UoncWqxuFfm8eBwzWNgyfBd6Gz+IeKQ9jCmyhoH99g15M3T+QaVHFjizVA==", + "dev": true, + "engines": { + "node": ">=4" + } + }, + "node_modules/globby": { + "version": "11.1.0", + "resolved": "https://registry.npmjs.org/globby/-/globby-11.1.0.tgz", + "integrity": "sha512-jhIXaOzy1sb8IyocaruWSn1TjmnBVs8Ayhcy83rmxNJ8q2uWKCAj3CnJY+KpGSXCueAPc0i05kVvVKtP1t9S3g==", + "dev": true, + "dependencies": { + "array-union": "^2.1.0", + "dir-glob": "^3.0.1", + "fast-glob": "^3.2.9", + "ignore": "^5.2.0", + "merge2": "^1.4.1", + "slash": "^3.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/gopd": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.0.1.tgz", + "integrity": "sha512-d65bNlIadxvpb/A2abVdlqKqV563juRnZ1Wtk6s1sIR8uNsXR70xqIzVqxVf1eTqDunwT2MkczEeaezCKTZhwA==", + "dev": true, + "dependencies": { + "get-intrinsic": "^1.1.3" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/graphemer": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/graphemer/-/graphemer-1.4.0.tgz", + "integrity": "sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag==", + "dev": true + }, + "node_modules/happy-dom": { + "version": "12.10.3", + "resolved": "https://registry.npmjs.org/happy-dom/-/happy-dom-12.10.3.tgz", + "integrity": "sha512-JzUXOh0wdNGY54oKng5hliuBkq/+aT1V3YpTM+lrN/GoLQTANZsMaIvmHiHe612rauHvPJnDZkZ+5GZR++1Abg==", + "dev": true, + "dependencies": { + "css.escape": "^1.5.1", + "entities": "^4.5.0", + "iconv-lite": "^0.6.3", + "webidl-conversions": "^7.0.0", + "whatwg-encoding": "^2.0.0", + "whatwg-mimetype": "^3.0.0" + } + }, + "node_modules/has-bigints": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/has-bigints/-/has-bigints-1.0.2.tgz", + "integrity": "sha512-tSvCKtBr9lkF0Ex0aQiP9N+OpV4zi2r/Nee5VkRDbaqv35RLYMzbwQfFSZZH0kR+Rd6302UJZ2p/bJCEoR3VoQ==", + "dev": true, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-flag": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", + "integrity": "sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==", + "dev": true, + "engines": { + "node": ">=4" + } + }, + "node_modules/has-property-descriptors": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/has-property-descriptors/-/has-property-descriptors-1.0.1.tgz", + "integrity": "sha512-VsX8eaIewvas0xnvinAe9bw4WfIeODpGYikiWYLH+dma0Jw6KHYqWiWfhQlgOVK8D6PvjubK5Uc4P0iIhIcNVg==", + "dev": true, + "dependencies": { + "get-intrinsic": "^1.2.2" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/has-proto/-/has-proto-1.0.1.tgz", + "integrity": "sha512-7qE+iP+O+bgF9clE5+UoBFzE65mlBiVj3tKCrlNQ0Ogwm0BjpT/gK4SlLYDMybDh5I3TCTKnPPa0oMG7JDYrhg==", + "dev": true, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-symbols": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.0.3.tgz", + "integrity": "sha512-l3LCuF6MgDNwTDKkdYGEihYjt5pRPbEg46rtlmnSPlUbgmB8LOIrKJbYYFBSbnPaJexMKtiPO8hmeRjRz2Td+A==", + "dev": true, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-tostringtag": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.0.tgz", + "integrity": "sha512-kFjcSNhnlGV1kyoGk7OXKSawH5JOb/LzUc5w9B02hOTO0dfFRjbHQKvg1d6cf3HbeUmtU9VbbV3qzZ2Teh97WQ==", + "dev": true, + "dependencies": { + "has-symbols": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/hasown": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.0.tgz", + "integrity": "sha512-vUptKVTpIJhcczKBbgnS+RtcuYMB8+oNzPK2/Hp3hanz8JmpATdmmgLgSaadVREkDm+e2giHwY3ZRkyjSIDDFA==", + "dev": true, + "dependencies": { + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/he": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/he/-/he-1.2.0.tgz", + "integrity": "sha512-F/1DnUGPopORZi0ni+CvrCgHQ5FyEAHRLSApuYWMmrbSwoN2Mn/7k+Gl38gJnR7yyDZk6WLXwiGod1JOWNDKGw==", + "dev": true, + "bin": { + "he": "bin/he" + } + }, + "node_modules/html-encoding-sniffer": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/html-encoding-sniffer/-/html-encoding-sniffer-3.0.0.tgz", + "integrity": "sha512-oWv4T4yJ52iKrufjnyZPkrN0CH3QnrUqdB6In1g5Fe1mia8GmF36gnfNySxoZtxD5+NmYw1EElVXiBk93UeskA==", + "dev": true, + "optional": true, + "peer": true, + "dependencies": { + "whatwg-encoding": "^2.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/html-escaper": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/html-escaper/-/html-escaper-2.0.2.tgz", + "integrity": "sha512-H2iMtd0I4Mt5eYiapRdIDjp+XzelXQ0tFE4JS7YFwFevXXMmOp9myNrUvCg0D6ws8iqkRPBfKHgbwig1SmlLfg==", + "dev": true + }, + "node_modules/html-tags": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/html-tags/-/html-tags-3.3.1.tgz", + "integrity": "sha512-ztqyC3kLto0e9WbNp0aeP+M3kTt+nbaIveGmUxAtZa+8iFgKLUOD4YKM5j+f3QD89bra7UeumolZHKuOXnTmeQ==", + "dev": true, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/http-proxy-agent": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/http-proxy-agent/-/http-proxy-agent-5.0.0.tgz", + "integrity": "sha512-n2hY8YdoRE1i7r6M0w9DIw5GgZN0G25P8zLCRQ8rjXtTU3vsNFBI/vWK/UIeE6g5MUUz6avwAPXmL6Fy9D/90w==", + "dev": true, + "optional": true, + "peer": true, + "dependencies": { + "@tootallnate/once": "2", + "agent-base": "6", + "debug": "4" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/https-proxy-agent": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-5.0.1.tgz", + "integrity": "sha512-dFcAjpTQFgoLMzC2VwU+C/CbS7uRL0lWmxDITmqm7C+7F0Odmj6s9l6alZc6AELXhrnggM2CeWSXHGOdX2YtwA==", + "dev": true, + "optional": true, + "peer": true, + "dependencies": { + "agent-base": "6", + "debug": "4" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/iconv-lite": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz", + "integrity": "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==", + "dev": true, + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/ignore": { + "version": "5.2.4", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.2.4.tgz", + "integrity": "sha512-MAb38BcSbH0eHNBxn7ql2NH/kX33OkB3lZ1BNdh7ENeRChHTYsTvWrMubiIAMNS2llXEEgZ1MUOBtXChP3kaFQ==", + "dev": true, + "engines": { + "node": ">= 4" + } + }, + "node_modules/immutable": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/immutable/-/immutable-4.1.0.tgz", + "integrity": "sha512-oNkuqVTA8jqG1Q6c+UglTOD1xhC1BtjKI7XkCXRkZHrN5m18/XsnUp8Q89GkQO/z+0WjonSvl0FLhDYftp46nQ==", + "dev": true + }, + "node_modules/import-fresh": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.0.tgz", + "integrity": "sha512-veYYhQa+D1QBKznvhUHxb8faxlrwUnxseDAbAp457E0wLNio2bOSKnjYDhMj+YiAq61xrMGhQk9iXVk5FzgQMw==", + "dev": true, + "dependencies": { + "parent-module": "^1.0.0", + "resolve-from": "^4.0.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/imurmurhash": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz", + "integrity": "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==", + "dev": true, + "engines": { + "node": ">=0.8.19" + } + }, + "node_modules/inflight": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", + "integrity": "sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==", + "dev": true, + "dependencies": { + "once": "^1.3.0", + "wrappy": "1" + } + }, + "node_modules/inherits": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", + "dev": true + }, + "node_modules/ini": { + "version": "1.3.8", + "resolved": "https://registry.npmjs.org/ini/-/ini-1.3.8.tgz", + "integrity": "sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew==", + "dev": true + }, + "node_modules/internal-slot": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/internal-slot/-/internal-slot-1.0.6.tgz", + "integrity": "sha512-Xj6dv+PsbtwyPpEflsejS+oIZxmMlV44zAhG479uYu89MsjcYOhCFnNyKrkJrihbsiasQyY0afoCl/9BLR65bg==", + "dev": true, + "dependencies": { + "get-intrinsic": "^1.2.2", + "hasown": "^2.0.0", + "side-channel": "^1.0.4" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/is-arguments": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/is-arguments/-/is-arguments-1.1.1.tgz", + "integrity": "sha512-8Q7EARjzEnKpt/PCD7e1cgUS0a6X8u5tdSiMqXhojOdoV9TsMsiO+9VLC5vAmO8N7/GmXn7yjR8qnA6bVAEzfA==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.2", + "has-tostringtag": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-array-buffer": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/is-array-buffer/-/is-array-buffer-3.0.2.tgz", + "integrity": "sha512-y+FyyR/w8vfIRq4eQcM1EYgSTnmHXPqaF+IgzgraytCFq5Xh8lllDVmAZolPJiZttZLeFSINPYMaEJ7/vWUa1w==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.2", + "get-intrinsic": "^1.2.0", + "is-typed-array": "^1.1.10" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-bigint": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/is-bigint/-/is-bigint-1.0.4.tgz", + "integrity": "sha512-zB9CruMamjym81i2JZ3UMn54PKGsQzsJeo6xvN3HJJ4CAsQNB6iRutp2To77OfCNuoxspsIhzaPoO1zyCEhFOg==", + "dev": true, + "dependencies": { + "has-bigints": "^1.0.1" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-binary-path": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz", + "integrity": "sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==", + "dev": true, + "dependencies": { + "binary-extensions": "^2.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/is-boolean-object": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/is-boolean-object/-/is-boolean-object-1.1.2.tgz", + "integrity": "sha512-gDYaKHJmnj4aWxyj6YHyXVpdQawtVLHU5cb+eztPGczf6cjuTdwve5ZIEfgXqH4e57An1D1AKf8CZ3kYrQRqYA==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.2", + "has-tostringtag": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-callable": { + "version": "1.2.7", + "resolved": "https://registry.npmjs.org/is-callable/-/is-callable-1.2.7.tgz", + "integrity": "sha512-1BC0BVFhS/p0qtw6enp8e+8OD0UrK0oFLztSjNzhcKA3WDuJxxAPXzPuPtKkjEY9UUoEWlX/8fgKeu2S8i9JTA==", + "dev": true, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-date-object": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/is-date-object/-/is-date-object-1.0.5.tgz", + "integrity": "sha512-9YQaSxsAiSwcvS33MBk3wTCVnWK+HhF8VZR2jRxehM16QcVOdHqPn4VPHmRK4lSr38n9JriurInLcP90xsYNfQ==", + "dev": true, + "dependencies": { + "has-tostringtag": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-extglob": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", + "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-fullwidth-code-point": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/is-glob": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", + "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", + "dev": true, + "dependencies": { + "is-extglob": "^2.1.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-map": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/is-map/-/is-map-2.0.2.tgz", + "integrity": "sha512-cOZFQQozTha1f4MxLFzlgKYPTyj26picdZTx82hbc/Xf4K/tZOOXSCkMvU4pKioRXGDLJRn0GM7Upe7kR721yg==", + "dev": true, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-number": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", + "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", + "dev": true, + "engines": { + "node": ">=0.12.0" + } + }, + "node_modules/is-number-object": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/is-number-object/-/is-number-object-1.0.7.tgz", + "integrity": "sha512-k1U0IRzLMo7ZlYIfzRu23Oh6MiIFasgpb9X76eqfFZAqwH44UI4KTBvBYIZ1dSL9ZzChTB9ShHfLkR4pdW5krQ==", + "dev": true, + "dependencies": { + "has-tostringtag": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-path-inside": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/is-path-inside/-/is-path-inside-3.0.3.tgz", + "integrity": "sha512-Fd4gABb+ycGAmKou8eMftCupSir5lRxqf4aD/vd0cD2qc4HL07OjCeuHMr8Ro4CoMaeCKDB0/ECBOVWjTwUvPQ==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/is-potential-custom-element-name": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/is-potential-custom-element-name/-/is-potential-custom-element-name-1.0.1.tgz", + "integrity": "sha512-bCYeRA2rVibKZd+s2625gGnGF/t7DSqDs4dP7CrLA1m7jKWz6pps0LpYLJN8Q64HtmPKJ1hrN3nzPNKFEKOUiQ==", + "dev": true, + "optional": true, + "peer": true + }, + "node_modules/is-regex": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/is-regex/-/is-regex-1.1.4.tgz", + "integrity": "sha512-kvRdxDsxZjhzUX07ZnLydzS1TU/TJlTUHHY4YLL87e37oUA49DfkLqgy+VjFocowy29cKvcSiu+kIv728jTTVg==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.2", + "has-tostringtag": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-set": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/is-set/-/is-set-2.0.2.tgz", + "integrity": "sha512-+2cnTEZeY5z/iXGbLhPrOAaK/Mau5k5eXq9j14CpRTftq0pAJu2MwVRSZhyZWBzx3o6X795Lz6Bpb6R0GKf37g==", + "dev": true, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-shared-array-buffer": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/is-shared-array-buffer/-/is-shared-array-buffer-1.0.2.tgz", + "integrity": "sha512-sqN2UDu1/0y6uvXyStCOzyhAjCSlHceFoMKJW8W9EU9cvic/QdsZ0kEU93HEy3IUEFZIiH/3w+AH/UQbPHNdhA==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.2" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-string": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/is-string/-/is-string-1.0.7.tgz", + "integrity": "sha512-tE2UXzivje6ofPW7l23cjDOMa09gb7xlAqG6jG5ej6uPV32TlWP3NKPigtaGeHNu9fohccRYvIiZMfOOnOYUtg==", + "dev": true, + "dependencies": { + "has-tostringtag": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-symbol": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/is-symbol/-/is-symbol-1.0.4.tgz", + "integrity": "sha512-C/CPBqKWnvdcxqIARxyOh4v1UUEOCHpgDa0WYgpKDFMszcrPcffg5uhwSgPCLD2WWxmq6isisz87tzT01tuGhg==", + "dev": true, + "dependencies": { + "has-symbols": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-typed-array": { + "version": "1.1.12", + "resolved": "https://registry.npmjs.org/is-typed-array/-/is-typed-array-1.1.12.tgz", + "integrity": "sha512-Z14TF2JNG8Lss5/HMqt0//T9JeHXttXy5pH/DBU4vi98ozO2btxzq9MwYDZYnKwU8nRsz/+GVFVRDq3DkVuSPg==", + "dev": true, + "dependencies": { + "which-typed-array": "^1.1.11" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-weakmap": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/is-weakmap/-/is-weakmap-2.0.1.tgz", + "integrity": "sha512-NSBR4kH5oVj1Uwvv970ruUkCV7O1mzgVFO4/rev2cLRda9Tm9HrL70ZPut4rOHgY0FNrUu9BCbXA2sdQ+x0chA==", + "dev": true, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-weakset": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/is-weakset/-/is-weakset-2.0.2.tgz", + "integrity": "sha512-t2yVvttHkQktwnNNmBQ98AhENLdPUTDTE21uPqAQ0ARwQfGeQKRVS0NNurH7bTf7RrvcVn1OOge45CnBeHCSmg==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.2", + "get-intrinsic": "^1.1.1" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/isarray": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-2.0.5.tgz", + "integrity": "sha512-xHjhDr3cNBK0BzdUJSPXZntQUx/mwMS5Rw4A7lPJ90XGAO6ISP/ePDNuo0vhqOZU+UD5JoodwCAAoZQd3FeAKw==", + "dev": true + }, + "node_modules/isexe": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", + "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", + "dev": true + }, + "node_modules/istanbul-lib-coverage": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/istanbul-lib-coverage/-/istanbul-lib-coverage-3.2.0.tgz", + "integrity": "sha512-eOeJ5BHCmHYvQK7xt9GkdHuzuCGS1Y6g9Gvnx3Ym33fz/HpLRYxiS0wHNr+m/MBC8B647Xt608vCDEvhl9c6Mw==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/istanbul-lib-instrument": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/istanbul-lib-instrument/-/istanbul-lib-instrument-6.0.0.tgz", + "integrity": "sha512-x58orMzEVfzPUKqlbLd1hXCnySCxKdDKa6Rjg97CwuLLRI4g3FHTdnExu1OqffVFay6zeMW+T6/DowFLndWnIw==", + "dev": true, + "dependencies": { + "@babel/core": "^7.12.3", + "@babel/parser": "^7.14.7", + "@istanbuljs/schema": "^0.1.2", + "istanbul-lib-coverage": "^3.2.0", + "semver": "^7.5.4" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/istanbul-lib-instrument/node_modules/semver": { + "version": "7.5.4", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.5.4.tgz", + "integrity": "sha512-1bCSESV6Pv+i21Hvpxp3Dx+pSD8lIPt8uVjRrxAUt/nbswYc+tK6Y2btiULjd4+fnq15PX+nqQDC7Oft7WkwcA==", + "dev": true, + "dependencies": { + "lru-cache": "^6.0.0" + }, + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/istanbul-lib-report": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/istanbul-lib-report/-/istanbul-lib-report-3.0.1.tgz", + "integrity": "sha512-GCfE1mtsHGOELCU8e/Z7YWzpmybrx/+dSTfLrvY8qRmaY6zXTKWn6WQIjaAFw069icm6GVMNkgu0NzI4iPZUNw==", + "dev": true, + "dependencies": { + "istanbul-lib-coverage": "^3.0.0", + "make-dir": "^4.0.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/istanbul-lib-report/node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/istanbul-lib-report/node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/istanbul-lib-source-maps": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/istanbul-lib-source-maps/-/istanbul-lib-source-maps-4.0.1.tgz", + "integrity": "sha512-n3s8EwkdFIJCG3BPKBYvskgXGoy88ARzvegkitk60NxRdwltLOTaH7CUiMRXvwYorl0Q712iEjcWB+fK/MrWVw==", + "dev": true, + "dependencies": { + "debug": "^4.1.1", + "istanbul-lib-coverage": "^3.0.0", + "source-map": "^0.6.1" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/istanbul-reports": { + "version": "3.1.5", + "resolved": "https://registry.npmjs.org/istanbul-reports/-/istanbul-reports-3.1.5.tgz", + "integrity": "sha512-nUsEMa9pBt/NOHqbcbeJEgqIlY/K7rVWUX6Lql2orY5e9roQOthbR3vtY4zzf2orPELg80fnxxk9zUyPlgwD1w==", + "dev": true, + "dependencies": { + "html-escaper": "^2.0.0", + "istanbul-lib-report": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/jackspeak": { + "version": "2.3.6", + "resolved": "https://registry.npmjs.org/jackspeak/-/jackspeak-2.3.6.tgz", + "integrity": "sha512-N3yCS/NegsOBokc8GAdM8UcmfsKiSS8cipheD/nivzr700H+nsMOxJjQnvwOcRYVuFkdH0wGUvW2WbXGmrZGbQ==", + "dev": true, + "dependencies": { + "@isaacs/cliui": "^8.0.2" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + }, + "optionalDependencies": { + "@pkgjs/parseargs": "^0.11.0" + } + }, + "node_modules/js-beautify": { + "version": "1.14.9", + "resolved": "https://registry.npmjs.org/js-beautify/-/js-beautify-1.14.9.tgz", + "integrity": "sha512-coM7xq1syLcMyuVGyToxcj2AlzhkDjmfklL8r0JgJ7A76wyGMpJ1oA35mr4APdYNO/o/4YY8H54NQIJzhMbhBg==", + "dev": true, + "dependencies": { + "config-chain": "^1.1.13", + "editorconfig": "^1.0.3", + "glob": "^8.1.0", + "nopt": "^6.0.0" + }, + "bin": { + "css-beautify": "js/bin/css-beautify.js", + "html-beautify": "js/bin/html-beautify.js", + "js-beautify": "js/bin/js-beautify.js" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/js-beautify/node_modules/brace-expansion": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", + "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", + "dev": true, + "dependencies": { + "balanced-match": "^1.0.0" + } + }, + "node_modules/js-beautify/node_modules/glob": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/glob/-/glob-8.1.0.tgz", + "integrity": "sha512-r8hpEjiQEYlF2QU0df3dS+nxxSIreXQS1qRhMJM0Q5NDdR386C7jb7Hwwod8Fgiuex+k0GFjgft18yvxm5XoCQ==", + "dev": true, + "dependencies": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^5.0.1", + "once": "^1.3.0" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/js-beautify/node_modules/minimatch": { + "version": "5.1.6", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-5.1.6.tgz", + "integrity": "sha512-lKwV/1brpG6mBUFHtb7NUmtABCb2WZZmm2wNiOA5hAb8VdCS4B3dtMWyvcoViccwAW/COERjXLt0zP1zXUN26g==", + "dev": true, + "dependencies": { + "brace-expansion": "^2.0.1" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/js-tokens": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", + "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", + "dev": true + }, + "node_modules/js-yaml": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz", + "integrity": "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==", + "dev": true, + "dependencies": { + "argparse": "^2.0.1" + }, + "bin": { + "js-yaml": "bin/js-yaml.js" + } + }, + "node_modules/jsdom": { + "version": "22.1.0", + "resolved": "https://registry.npmjs.org/jsdom/-/jsdom-22.1.0.tgz", + "integrity": "sha512-/9AVW7xNbsBv6GfWho4TTNjEo9fe6Zhf9O7s0Fhhr3u+awPwAJMKwAMXnkk5vBxflqLW9hTHX/0cs+P3gW+cQw==", + "dev": true, + "optional": true, + "peer": true, + "dependencies": { + "abab": "^2.0.6", + "cssstyle": "^3.0.0", + "data-urls": "^4.0.0", + "decimal.js": "^10.4.3", + "domexception": "^4.0.0", + "form-data": "^4.0.0", + "html-encoding-sniffer": "^3.0.0", + "http-proxy-agent": "^5.0.0", + "https-proxy-agent": "^5.0.1", + "is-potential-custom-element-name": "^1.0.1", + "nwsapi": "^2.2.4", + "parse5": "^7.1.2", + "rrweb-cssom": "^0.6.0", + "saxes": "^6.0.0", + "symbol-tree": "^3.2.4", + "tough-cookie": "^4.1.2", + "w3c-xmlserializer": "^4.0.0", + "webidl-conversions": "^7.0.0", + "whatwg-encoding": "^2.0.0", + "whatwg-mimetype": "^3.0.0", + "whatwg-url": "^12.0.1", + "ws": "^8.13.0", + "xml-name-validator": "^4.0.0" + }, + "engines": { + "node": ">=16" + }, + "peerDependencies": { + "canvas": "^2.5.0" + }, + "peerDependenciesMeta": { + "canvas": { + "optional": true + } + } + }, + "node_modules/jsdom/node_modules/tr46": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/tr46/-/tr46-4.1.1.tgz", + "integrity": "sha512-2lv/66T7e5yNyhAAC4NaKe5nVavzuGJQVVtRYLyQ2OI8tsJ61PMLlelehb0wi2Hx6+hT/OJUWZcw8MjlSRnxvw==", + "dev": true, + "optional": true, + "peer": true, + "dependencies": { + "punycode": "^2.3.0" + }, + "engines": { + "node": ">=14" + } + }, + "node_modules/jsdom/node_modules/whatwg-url": { + "version": "12.0.1", + "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-12.0.1.tgz", + "integrity": "sha512-Ed/LrqB8EPlGxjS+TrsXcpUond1mhccS3pchLhzSgPCnTimUCKj3IZE75pAs5m6heB2U2TMerKFUXheyHY+VDQ==", + "dev": true, + "optional": true, + "peer": true, + "dependencies": { + "tr46": "^4.1.1", + "webidl-conversions": "^7.0.0" + }, + "engines": { + "node": ">=14" + } + }, + "node_modules/jsesc": { + "version": "2.5.2", + "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-2.5.2.tgz", + "integrity": "sha512-OYu7XEzjkCQ3C5Ps3QIZsQfNpqoJyZZA99wd9aWd05NCtC5pWOkShK2mkL6HXQR6/Cy2lbNdPlZBpuQHXE63gA==", + "dev": true, + "bin": { + "jsesc": "bin/jsesc" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/json-schema-traverse": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", + "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", + "dev": true + }, + "node_modules/json-stable-stringify-without-jsonify": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz", + "integrity": "sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==", + "dev": true + }, + "node_modules/json5": { + "version": "2.2.3", + "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz", + "integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==", + "dev": true, + "bin": { + "json5": "lib/cli.js" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/jsonc-parser": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/jsonc-parser/-/jsonc-parser-3.2.0.tgz", + "integrity": "sha512-gfFQZrcTc8CnKXp6Y4/CBT3fTc0OVuDofpre4aEeEpSBPV5X5v4+Vmx+8snU7RLPrNHPKSgLxGo9YuQzz20o+w==", + "dev": true + }, + "node_modules/jwt-decode": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/jwt-decode/-/jwt-decode-3.1.2.tgz", + "integrity": "sha512-UfpWE/VZn0iP50d8cz9NrZLM9lSWhcJ+0Gt/nm4by88UL+J1SiKN8/5dkjMmbEzwL2CAe+67GsegCbIKtbp75A==" + }, + "node_modules/levn": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/levn/-/levn-0.4.1.tgz", + "integrity": "sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==", + "dev": true, + "dependencies": { + "prelude-ls": "^1.2.1", + "type-check": "~0.4.0" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/local-pkg": { + "version": "0.4.3", + "resolved": "https://registry.npmjs.org/local-pkg/-/local-pkg-0.4.3.tgz", + "integrity": "sha512-SFppqq5p42fe2qcZQqqEOiVRXl+WCP1MdT6k7BDEW1j++sp5fIY+/fdRQitvKgB5BrBcmrs5m/L0v2FrU5MY1g==", + "dev": true, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/antfu" + } + }, + "node_modules/locate-path": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz", + "integrity": "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==", + "dev": true, + "dependencies": { + "p-locate": "^5.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/lodash": { + "version": "4.17.21", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz", + "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==", + "dev": true + }, + "node_modules/lodash.merge": { + "version": "4.6.2", + "resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz", + "integrity": "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==", + "dev": true + }, + "node_modules/loupe": { + "version": "2.3.7", + "resolved": "https://registry.npmjs.org/loupe/-/loupe-2.3.7.tgz", + "integrity": "sha512-zSMINGVYkdpYSOBmLi0D1Uo7JU9nVdQKrHxC8eYlV+9YKK9WePqAlL7lSlorG/U2Fw1w0hTBmaa/jrQ3UbPHtA==", + "dev": true, + "dependencies": { + "get-func-name": "^2.0.1" + } + }, + "node_modules/lru-cache": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", + "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==", + "dev": true, + "dependencies": { + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/lz-string": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/lz-string/-/lz-string-1.5.0.tgz", + "integrity": "sha512-h5bgJWpxJNswbU7qCrV0tIKQCaS3blPDrqKWx+QxzuzL1zGUzij9XCWLrSLsJPu5t+eWA/ycetzYAO5IOMcWAQ==", + "dev": true, + "bin": { + "lz-string": "bin/bin.js" + } + }, + "node_modules/magic-string": { + "version": "0.30.5", + "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.5.tgz", + "integrity": "sha512-7xlpfBaQaP/T6Vh8MO/EqXSW5En6INHEvEXQiuff7Gku0PWjU3uf6w/j9o7O+SpB5fOAkrI5HeoNgwjEO0pFsA==", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.4.15" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/magic-string/node_modules/@jridgewell/sourcemap-codec": { + "version": "1.4.15", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.4.15.tgz", + "integrity": "sha512-eF2rxCRulEKXHTRiDrDy6erMYWqNw4LPdQ8UQA4huuxaQsVeRPFl2oM8oDGxMFhJUWZf9McpLtJasDDZb/Bpeg==" + }, + "node_modules/make-dir": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-4.0.0.tgz", + "integrity": "sha512-hXdUTZYIVOt1Ex//jAQi+wTZZpUpwBj/0QsOzqegb3rGMMeJiSEu5xLHnYfBrRV4RH2+OCSOO95Is/7x1WJ4bw==", + "dev": true, + "dependencies": { + "semver": "^7.5.3" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/make-dir/node_modules/semver": { + "version": "7.5.4", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.5.4.tgz", + "integrity": "sha512-1bCSESV6Pv+i21Hvpxp3Dx+pSD8lIPt8uVjRrxAUt/nbswYc+tK6Y2btiULjd4+fnq15PX+nqQDC7Oft7WkwcA==", + "dev": true, + "dependencies": { + "lru-cache": "^6.0.0" + }, + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/make-error": { + "version": "1.3.6", + "resolved": "https://registry.npmjs.org/make-error/-/make-error-1.3.6.tgz", + "integrity": "sha512-s8UhlNe7vPKomQhC1qFelMokr/Sc3AgNbso3n74mVPA5LTZwkB9NlXf4XPamLxJE8h0gh73rM94xvwRT2CVInw==", + "dev": true + }, + "node_modules/merge2": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz", + "integrity": "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==", + "dev": true, + "engines": { + "node": ">= 8" + } + }, + "node_modules/micromatch": { + "version": "4.0.5", + "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.5.tgz", + "integrity": "sha512-DMy+ERcEW2q8Z2Po+WNXuw3c5YaUSFjAO5GsJqfEl7UjvtIuFKO6ZrKvcItdy98dwFI2N1tg3zNIdKaQT+aNdA==", + "dev": true, + "dependencies": { + "braces": "^3.0.2", + "picomatch": "^2.3.1" + }, + "engines": { + "node": ">=8.6" + } + }, + "node_modules/mime-db": { + "version": "1.52.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", + "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime-types": { + "version": "2.1.35", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", + "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", + "dependencies": { + "mime-db": "1.52.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "dev": true, + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/minipass": { + "version": "7.0.4", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.0.4.tgz", + "integrity": "sha512-jYofLM5Dam9279rdkWzqHozUo4ybjdZmCsDHePy5V/PbBcVMiSZR97gmAy45aqi8CK1lG2ECd356FU86avfwUQ==", + "dev": true, + "engines": { + "node": ">=16 || 14 >=14.17" + } + }, + "node_modules/mlly": { + "version": "1.4.2", + "resolved": "https://registry.npmjs.org/mlly/-/mlly-1.4.2.tgz", + "integrity": "sha512-i/Ykufi2t1EZ6NaPLdfnZk2AX8cs0d+mTzVKuPfqPKPatxLApaBoxJQ9x1/uckXtrS/U5oisPMDkNs0yQTaBRg==", + "dev": true, + "dependencies": { + "acorn": "^8.10.0", + "pathe": "^1.1.1", + "pkg-types": "^1.0.3", + "ufo": "^1.3.0" + } + }, + "node_modules/ms": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", + "dev": true + }, + "node_modules/muggle-string": { + "version": "0.3.1", + "resolved": "https://registry.npmjs.org/muggle-string/-/muggle-string-0.3.1.tgz", + "integrity": "sha512-ckmWDJjphvd/FvZawgygcUeQCxzvohjFO5RxTjj4eq8kw359gFF3E1brjfI+viLMxss5JrHTDRHZvu2/tuy0Qg==", + "dev": true + }, + "node_modules/nanoid": { + "version": "3.3.6", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.6.tgz", + "integrity": "sha512-BGcqMMJuToF7i1rt+2PWSNVnWIkGCU78jBG3RxO/bZlnZPK2Cmi2QaffxGO/2RvWi9sL+FAiRiXMgsyxQ1DIDA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "bin": { + "nanoid": "bin/nanoid.cjs" + }, + "engines": { + "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" + } + }, + "node_modules/natural-compare": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz", + "integrity": "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==", + "dev": true + }, + "node_modules/node-releases": { + "version": "2.0.13", + "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.13.tgz", + "integrity": "sha512-uYr7J37ae/ORWdZeQ1xxMJe3NtdmqMC/JZK+geofDrkLUApKRHPd18/TxtBOJ4A0/+uUIliorNrfYV6s1b02eQ==", + "dev": true + }, + "node_modules/nopt": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/nopt/-/nopt-6.0.0.tgz", + "integrity": "sha512-ZwLpbTgdhuZUnZzjd7nb1ZV+4DoiC6/sfiVKok72ym/4Tlf+DFdlHYmT2JPmcNNWV6Pi3SDf1kT+A4r9RTuT9g==", + "dev": true, + "dependencies": { + "abbrev": "^1.0.0" + }, + "bin": { + "nopt": "bin/nopt.js" + }, + "engines": { + "node": "^12.13.0 || ^14.15.0 || >=16.0.0" + } + }, + "node_modules/normalize-path": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", + "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/nth-check": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/nth-check/-/nth-check-2.1.1.tgz", + "integrity": "sha512-lqjrjmaOoAnWfMmBPL+XNnynZh2+swxiX3WUE0s4yEHI6m+AwrK2UZOimIRl3X/4QctVqS8AiZjFqyOGrMXb/w==", + "dev": true, + "dependencies": { + "boolbase": "^1.0.0" + }, + "funding": { + "url": "https://github.com/fb55/nth-check?sponsor=1" + } + }, + "node_modules/nwsapi": { + "version": "2.2.4", + "resolved": "https://registry.npmjs.org/nwsapi/-/nwsapi-2.2.4.tgz", + "integrity": "sha512-NHj4rzRo0tQdijE9ZqAx6kYDcoRwYwSYzCA8MY3JzfxlrvEU0jhnhJT9BhqhJs7I/dKcrDm6TyulaRqZPIhN5g==", + "dev": true, + "optional": true, + "peer": true + }, + "node_modules/object-inspect": { + "version": "1.13.1", + "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.1.tgz", + "integrity": "sha512-5qoj1RUiKOMsCCNLV1CBiPYE10sziTsnmNxkAI/rZhiD63CF7IqdFGC/XzjWjpSgLf0LxXX3bDFIh0E18f6UhQ==", + "dev": true, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/object-is": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/object-is/-/object-is-1.1.5.tgz", + "integrity": "sha512-3cyDsyHgtmi7I7DfSSI2LDp6SK2lwvtbg0p0R1e0RvTqF5ceGx+K2dfSjm1bKDMVCFEDAQvy+o8c6a7VujOddw==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.2", + "define-properties": "^1.1.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/object-keys": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/object-keys/-/object-keys-1.1.1.tgz", + "integrity": "sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA==", + "dev": true, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/object.assign": { + "version": "4.1.4", + "resolved": "https://registry.npmjs.org/object.assign/-/object.assign-4.1.4.tgz", + "integrity": "sha512-1mxKf0e58bvyjSCtKYY4sRe9itRk3PJpquJOjeIkz885CczcI4IvJJDLPS72oowuSh+pBxUFROpX+TU++hxhZQ==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.2", + "define-properties": "^1.1.4", + "has-symbols": "^1.0.3", + "object-keys": "^1.1.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/oidc-client-ts": { + "version": "2.4.0", + "resolved": "https://registry.npmjs.org/oidc-client-ts/-/oidc-client-ts-2.4.0.tgz", + "integrity": "sha512-WijhkTrlXK2VvgGoakWJiBdfIsVGz6CFzgjNNqZU1hPKV2kyeEaJgLs7RwuiSp2WhLfWBQuLvr2SxVlZnk3N1w==", + "dependencies": { + "crypto-js": "^4.2.0", + "jwt-decode": "^3.1.2" + }, + "engines": { + "node": ">=12.13.0" + } + }, + "node_modules/once": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", + "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", + "dev": true, + "dependencies": { + "wrappy": "1" + } + }, + "node_modules/optionator": { + "version": "0.9.3", + "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.3.tgz", + "integrity": "sha512-JjCoypp+jKn1ttEFExxhetCKeJt9zhAgAve5FXHixTvFDW/5aEktX9bufBKLRRMdU7bNtpLfcGu94B3cdEJgjg==", + "dev": true, + "dependencies": { + "@aashutoshrathi/word-wrap": "^1.2.3", + "deep-is": "^0.1.3", + "fast-levenshtein": "^2.0.6", + "levn": "^0.4.1", + "prelude-ls": "^1.2.1", + "type-check": "^0.4.0" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/p-limit": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", + "integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==", + "dev": true, + "dependencies": { + "yocto-queue": "^0.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/p-locate": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-5.0.0.tgz", + "integrity": "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==", + "dev": true, + "dependencies": { + "p-limit": "^3.0.2" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/parent-module": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz", + "integrity": "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==", + "dev": true, + "dependencies": { + "callsites": "^3.0.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/parse5": { + "version": "7.1.2", + "resolved": "https://registry.npmjs.org/parse5/-/parse5-7.1.2.tgz", + "integrity": "sha512-Czj1WaSVpaoj0wbhMzLmWD69anp2WH7FXMB9n1Sy8/ZFF9jolSQVMu1Ij5WIyGmcBmhk7EOndpO4mIpihVqAXw==", + "dev": true, + "optional": true, + "peer": true, + "dependencies": { + "entities": "^4.4.0" + }, + "funding": { + "url": "https://github.com/inikulin/parse5?sponsor=1" + } + }, + "node_modules/path-exists": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", + "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/path-is-absolute": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", + "integrity": "sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/path-key": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", + "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/path-scurry": { + "version": "1.10.1", + "resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-1.10.1.tgz", + "integrity": "sha512-MkhCqzzBEpPvxxQ71Md0b1Kk51W01lrYvlMzSUaIzNsODdd7mqhiimSZlr+VegAz5Z6Vzt9Xg2ttE//XBhH3EQ==", + "dev": true, + "dependencies": { + "lru-cache": "^9.1.1 || ^10.0.0", + "minipass": "^5.0.0 || ^6.0.2 || ^7.0.0" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/path-scurry/node_modules/lru-cache": { + "version": "10.0.1", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.0.1.tgz", + "integrity": "sha512-IJ4uwUTi2qCccrioU6g9g/5rvvVl13bsdczUUcqbciD9iLr095yj8DQKdObriEvuNSx325N1rV1O0sJFszx75g==", + "dev": true, + "engines": { + "node": "14 || >=16.14" + } + }, + "node_modules/path-type": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-type/-/path-type-4.0.0.tgz", + "integrity": "sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/pathe": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/pathe/-/pathe-1.1.1.tgz", + "integrity": "sha512-d+RQGp0MAYTIaDBIMmOfMwz3E+LOZnxx1HZd5R18mmCZY0QBlK0LDZfPc8FW8Ed2DlvsuE6PRjroDY+wg4+j/Q==", + "dev": true + }, + "node_modules/pathval": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/pathval/-/pathval-1.1.1.tgz", + "integrity": "sha512-Dp6zGqpTdETdR63lehJYPeIOqpiNBNtc7BpWSLrOje7UaIsE5aY92r/AunQA7rsXvet3lrJ3JnZX29UPTKXyKQ==", + "dev": true, + "engines": { + "node": "*" + } + }, + "node_modules/picocolors": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.0.0.tgz", + "integrity": "sha512-1fygroTLlHu66zi26VoTDv8yRgm0Fccecssto+MhsZ0D/DGW2sm8E8AjW7NU5VVTRt5GxbeZ5qBuJr+HyLYkjQ==" + }, + "node_modules/picomatch": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", + "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", + "dev": true, + "engines": { + "node": ">=8.6" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/pinia": { + "version": "2.1.7", + "resolved": "https://registry.npmjs.org/pinia/-/pinia-2.1.7.tgz", + "integrity": "sha512-+C2AHFtcFqjPih0zpYuvof37SFxMQ7OEG2zV9jRI12i9BOy3YQVAHwdKtyyc8pDcDyIc33WCIsZaCFWU7WWxGQ==", + "dependencies": { + "@vue/devtools-api": "^6.5.0", + "vue-demi": ">=0.14.5" + }, + "funding": { + "url": "https://github.com/sponsors/posva" + }, + "peerDependencies": { + "@vue/composition-api": "^1.4.0", + "typescript": ">=4.4.4", + "vue": "^2.6.14 || ^3.3.0" + }, + "peerDependenciesMeta": { + "@vue/composition-api": { + "optional": true + }, + "typescript": { + "optional": true + } + } + }, + "node_modules/pinia-plugin-persistedstate": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/pinia-plugin-persistedstate/-/pinia-plugin-persistedstate-3.2.0.tgz", + "integrity": "sha512-tZbNGf2vjAQcIm7alK40sE51Qu/m9oWr+rEgNm/2AWr1huFxj72CjvpQcIQzMknDBJEkQznCLAGtJTIcLKrKdw==", + "peerDependencies": { + "pinia": "^2.0.0" + } + }, + "node_modules/pinia/node_modules/vue-demi": { + "version": "0.14.5", + "resolved": "https://registry.npmjs.org/vue-demi/-/vue-demi-0.14.5.tgz", + "integrity": "sha512-o9NUVpl/YlsGJ7t+xuqJKx8EBGf1quRhCiT6D/J0pfwmk9zUwYkC7yrF4SZCe6fETvSM3UNL2edcbYrSyc4QHA==", + "hasInstallScript": true, + "bin": { + "vue-demi-fix": "bin/vue-demi-fix.js", + "vue-demi-switch": "bin/vue-demi-switch.js" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/antfu" + }, + "peerDependencies": { + "@vue/composition-api": "^1.0.0-rc.1", + "vue": "^3.0.0-0 || ^2.6.0" + }, + "peerDependenciesMeta": { + "@vue/composition-api": { + "optional": true + } + } + }, + "node_modules/pkg-types": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/pkg-types/-/pkg-types-1.0.3.tgz", + "integrity": "sha512-nN7pYi0AQqJnoLPC9eHFQ8AcyaixBUOwvqc5TDnIKCMEE6I0y8P7OKA7fPexsXGCGxQDl/cmrLAp26LhcwxZ4A==", + "dev": true, + "dependencies": { + "jsonc-parser": "^3.2.0", + "mlly": "^1.2.0", + "pathe": "^1.1.0" + } + }, + "node_modules/postcss": { + "version": "8.4.31", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.31.tgz", + "integrity": "sha512-PS08Iboia9mts/2ygV3eLpY5ghnUcfLV/EXTOW1E2qYxJKGGBUtNjN76FYHnMs36RmARn41bC0AZmn+rR0OVpQ==", + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/postcss" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "dependencies": { + "nanoid": "^3.3.6", + "picocolors": "^1.0.0", + "source-map-js": "^1.0.2" + }, + "engines": { + "node": "^10 || ^12 || >=14" + } + }, + "node_modules/postcss-selector-parser": { + "version": "6.0.13", + "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-6.0.13.tgz", + "integrity": "sha512-EaV1Gl4mUEV4ddhDnv/xtj7sxwrwxdetHdWUGnT4VJQf+4d05v6lHYZr8N573k5Z0BViss7BDhfWtKS3+sfAqQ==", + "dev": true, + "dependencies": { + "cssesc": "^3.0.0", + "util-deprecate": "^1.0.2" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/prelude-ls": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz", + "integrity": "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==", + "dev": true, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/prettier": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.0.3.tgz", + "integrity": "sha512-L/4pUDMxcNa8R/EthV08Zt42WBO4h1rarVtK0K+QJG0X187OLo7l699jWw0GKuwzkPQ//jMFA/8Xm6Fh3J/DAg==", + "dev": true, + "bin": { + "prettier": "bin/prettier.cjs" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/prettier/prettier?sponsor=1" + } + }, + "node_modules/pretty-format": { + "version": "27.5.1", + "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-27.5.1.tgz", + "integrity": "sha512-Qb1gy5OrP5+zDf2Bvnzdl3jsTf1qXVMazbvCoKhtKqVs4/YK4ozX4gKQJJVyNe+cajNPn0KoC0MC3FUmaHWEmQ==", + "dev": true, + "dependencies": { + "ansi-regex": "^5.0.1", + "ansi-styles": "^5.0.0", + "react-is": "^17.0.1" + }, + "engines": { + "node": "^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0" + } + }, + "node_modules/pretty-format/node_modules/ansi-styles": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz", + "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==", + "dev": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/primeflex": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/primeflex/-/primeflex-3.3.1.tgz", + "integrity": "sha512-zaOq3YvcOYytbAmKv3zYc+0VNS9Wg5d37dfxZnveKBFPr7vEIwfV5ydrpiouTft8MVW6qNjfkaQphHSnvgQbpQ==" + }, + "node_modules/primeicons": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/primeicons/-/primeicons-6.0.1.tgz", + "integrity": "sha512-KDeO94CbWI4pKsPnYpA1FPjo79EsY9I+M8ywoPBSf9XMXoe/0crjbUK7jcQEDHuc0ZMRIZsxH3TYLv4TUtHmAA==" + }, + "node_modules/primevue": { + "version": "3.34.1", + "resolved": "https://registry.npmjs.org/primevue/-/primevue-3.34.1.tgz", + "integrity": "sha512-5QPy8I+TMYSQgC0Bs/9vINsOVjgCOQFAr6uz49Wzcj8u04qJ2mG/z6OhAana+f4yKTTHVwHLVnuGkrIp/nI9DA==", + "peerDependencies": { + "vue": "^3.0.0" + } + }, + "node_modules/property-expr": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/property-expr/-/property-expr-2.0.5.tgz", + "integrity": "sha512-IJUkICM5dP5znhCckHSv30Q4b5/JA5enCtkRHYaOVOAocnH/1BQEYTC5NMfT3AVl/iXKdr3aqQbQn9DxyWknwA==" + }, + "node_modules/proto-list": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/proto-list/-/proto-list-1.2.4.tgz", + "integrity": "sha512-vtK/94akxsTMhe0/cbfpR+syPuszcuwhqVjJq26CuNDgFGj682oRBXOP5MJpv2r7JtE8MsiepGIqvvOTBwn2vA==", + "dev": true + }, + "node_modules/proxy-from-env": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz", + "integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==" + }, + "node_modules/psl": { + "version": "1.9.0", + "resolved": "https://registry.npmjs.org/psl/-/psl-1.9.0.tgz", + "integrity": "sha512-E/ZsdU4HLs/68gYzgGTkMicWTLPdAftJLfJFlLUAAKZGkStNU72sZjT66SnMDVOfOWY/YAoiD7Jxa9iHvngcag==", + "dev": true, + "optional": true, + "peer": true + }, + "node_modules/punycode": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.0.tgz", + "integrity": "sha512-rRV+zQD8tVFys26lAGR9WUuS4iUAngJScM+ZRSKtvl5tKeZ2t5bvdNFdNHBW9FWR4guGHlgmsZ1G7BSm2wTbuA==", + "dev": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/qrcode.vue": { + "version": "3.4.1", + "resolved": "https://registry.npmjs.org/qrcode.vue/-/qrcode.vue-3.4.1.tgz", + "integrity": "sha512-wq/zHsifH4FJ1GXQi8/wNxD1KfQkckIpjK1KPTc/qwYU5/Bkd4me0w4xZSg6EXk6xLBkVDE0zxVagewv5EMAVA==", + "peerDependencies": { + "vue": "^3.0.0" + } + }, + "node_modules/querystringify": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/querystringify/-/querystringify-2.2.0.tgz", + "integrity": "sha512-FIqgj2EUvTa7R50u0rGsyTftzjYmv/a3hO345bZNrqabNqjtgiDMgmo4mkUjd+nzU5oF3dClKqFIPUKybUyqoQ==", + "dev": true, + "optional": true, + "peer": true + }, + "node_modules/queue-microtask": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz", + "integrity": "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ] + }, + "node_modules/react-is": { + "version": "17.0.2", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-17.0.2.tgz", + "integrity": "sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w==", + "dev": true + }, + "node_modules/readdirp": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz", + "integrity": "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==", + "dev": true, + "dependencies": { + "picomatch": "^2.2.1" + }, + "engines": { + "node": ">=8.10.0" + } + }, + "node_modules/regenerator-runtime": { + "version": "0.14.0", + "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.14.0.tgz", + "integrity": "sha512-srw17NI0TUWHuGa5CFGGmhfNIeja30WMBfbslPNhf6JrqQlLN5gcrvig1oqPxiVaXb0oW0XRKtH6Nngs5lKCIA==" + }, + "node_modules/regexp.prototype.flags": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/regexp.prototype.flags/-/regexp.prototype.flags-1.5.1.tgz", + "integrity": "sha512-sy6TXMN+hnP/wMy+ISxg3krXx7BAtWVO4UouuCN/ziM9UEne0euamVNafDfvC83bRNr95y0V5iijeDQFUNpvrg==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.2", + "define-properties": "^1.2.0", + "set-function-name": "^2.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/require-directory": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", + "integrity": "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/requires-port": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/requires-port/-/requires-port-1.0.0.tgz", + "integrity": "sha512-KigOCHcocU3XODJxsu8i/j8T9tzT4adHiecwORRQ0ZZFcp7ahwXuRU1m+yuO90C5ZUyGeGfocHDI14M3L3yDAQ==", + "dev": true, + "optional": true, + "peer": true + }, + "node_modules/resolve-from": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz", + "integrity": "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==", + "dev": true, + "engines": { + "node": ">=4" + } + }, + "node_modules/reusify": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.0.4.tgz", + "integrity": "sha512-U9nH88a3fc/ekCF1l0/UP1IosiuIjyTh7hBvXVMHYgVcfGvt897Xguj2UOLDeI5BG2m7/uwyaLVT6fbtCwTyzw==", + "dev": true, + "engines": { + "iojs": ">=1.0.0", + "node": ">=0.10.0" + } + }, + "node_modules/rimraf": { + "version": "5.0.5", + "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-5.0.5.tgz", + "integrity": "sha512-CqDakW+hMe/Bz202FPEymy68P+G50RfMQK+Qo5YUqc9SPipvbGjCGKd0RSKEelbsfQuw3g5NZDSrlZZAJurH1A==", + "dev": true, + "dependencies": { + "glob": "^10.3.7" + }, + "bin": { + "rimraf": "dist/esm/bin.mjs" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/rimraf/node_modules/brace-expansion": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", + "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", + "dev": true, + "dependencies": { + "balanced-match": "^1.0.0" + } + }, + "node_modules/rimraf/node_modules/foreground-child": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/foreground-child/-/foreground-child-3.1.1.tgz", + "integrity": "sha512-TMKDUnIte6bfb5nWv7V/caI169OHgvwjb7V4WkeUvbQQdjr5rWKqHFiKWb/fcOwB+CzBT+qbWjvj+DVwRskpIg==", + "dev": true, + "dependencies": { + "cross-spawn": "^7.0.0", + "signal-exit": "^4.0.1" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/rimraf/node_modules/glob": { + "version": "10.3.10", + "resolved": "https://registry.npmjs.org/glob/-/glob-10.3.10.tgz", + "integrity": "sha512-fa46+tv1Ak0UPK1TOy/pZrIybNNt4HCv7SDzwyfiOZkvZLEbjsZkJBPtDHVshZjbecAoAGSC20MjLDG/qr679g==", + "dev": true, + "dependencies": { + "foreground-child": "^3.1.0", + "jackspeak": "^2.3.5", + "minimatch": "^9.0.1", + "minipass": "^5.0.0 || ^6.0.2 || ^7.0.0", + "path-scurry": "^1.10.1" + }, + "bin": { + "glob": "dist/esm/bin.mjs" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/rimraf/node_modules/minimatch": { + "version": "9.0.3", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.3.tgz", + "integrity": "sha512-RHiac9mvaRw0x3AYRgDC1CxAP7HTcNrrECeA8YYJeWnpo+2Q5CegtZjaotWTWxDG3UeGA1coE05iH1mPjT/2mg==", + "dev": true, + "dependencies": { + "brace-expansion": "^2.0.1" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/rimraf/node_modules/signal-exit": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz", + "integrity": "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==", + "dev": true, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/rollup": { + "version": "3.28.1", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-3.28.1.tgz", + "integrity": "sha512-R9OMQmIHJm9znrU3m3cpE8uhN0fGdXiawME7aZIpQqvpS/85+Vt1Hq1/yVIcYfOmaQiHjvXkQAoJukvLpau6Yw==", + "dev": true, + "bin": { + "rollup": "dist/bin/rollup" + }, + "engines": { + "node": ">=14.18.0", + "npm": ">=8.0.0" + }, + "optionalDependencies": { + "fsevents": "~2.3.2" + } + }, + "node_modules/rrweb-cssom": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/rrweb-cssom/-/rrweb-cssom-0.6.0.tgz", + "integrity": "sha512-APM0Gt1KoXBz0iIkkdB/kfvGOwC4UuJFeG/c+yV7wSc7q96cG/kJ0HiYCnzivD9SB53cLV1MlHFNfOuPaadYSw==", + "dev": true, + "optional": true, + "peer": true + }, + "node_modules/run-parallel": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz", + "integrity": "sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "dependencies": { + "queue-microtask": "^1.2.2" + } + }, + "node_modules/safer-buffer": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", + "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", + "dev": true + }, + "node_modules/sass": { + "version": "1.69.5", + "resolved": "https://registry.npmjs.org/sass/-/sass-1.69.5.tgz", + "integrity": "sha512-qg2+UCJibLr2LCVOt3OlPhr/dqVHWOa9XtZf2OjbLs/T4VPSJ00udtgJxH3neXZm+QqX8B+3cU7RaLqp1iVfcQ==", + "dev": true, + "dependencies": { + "chokidar": ">=3.0.0 <4.0.0", + "immutable": "^4.0.0", + "source-map-js": ">=0.6.2 <2.0.0" + }, + "bin": { + "sass": "sass.js" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/saxes": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/saxes/-/saxes-6.0.0.tgz", + "integrity": "sha512-xAg7SOnEhrm5zI3puOOKyy1OMcMlIJZYNJY7xLBwSze0UjhPLnWfj2GF2EpT0jmzaJKIWKHLsaSSajf35bcYnA==", + "dev": true, + "optional": true, + "peer": true, + "dependencies": { + "xmlchars": "^2.2.0" + }, + "engines": { + "node": ">=v12.22.7" + } + }, + "node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "dev": true, + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/set-function-length": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/set-function-length/-/set-function-length-1.1.1.tgz", + "integrity": "sha512-VoaqjbBJKiWtg4yRcKBQ7g7wnGnLV3M8oLvVWwOk2PdYY6PEFegR1vezXR0tw6fZGF9csVakIRjrJiy2veSBFQ==", + "dev": true, + "dependencies": { + "define-data-property": "^1.1.1", + "get-intrinsic": "^1.2.1", + "gopd": "^1.0.1", + "has-property-descriptors": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/set-function-name": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/set-function-name/-/set-function-name-2.0.1.tgz", + "integrity": "sha512-tMNCiqYVkXIZgc2Hnoy2IvC/f8ezc5koaRFkCjrpWzGpCd3qbZXPzVy9MAZzK1ch/X0jvSkojys3oqJN0qCmdA==", + "dev": true, + "dependencies": { + "define-data-property": "^1.0.1", + "functions-have-names": "^1.2.3", + "has-property-descriptors": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/shebang-command": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", + "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", + "dev": true, + "dependencies": { + "shebang-regex": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/shebang-regex": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", + "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/side-channel": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.0.4.tgz", + "integrity": "sha512-q5XPytqFEIKHkGdiMIrY10mvLRvnQh42/+GoBlFW3b2LXLE2xxJpZFdm94we0BaoV3RwJyGqg5wS7epxTv0Zvw==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.0", + "get-intrinsic": "^1.0.2", + "object-inspect": "^1.9.0" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/siginfo": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/siginfo/-/siginfo-2.0.0.tgz", + "integrity": "sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g==", + "dev": true + }, + "node_modules/signal-exit": { + "version": "3.0.7", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.7.tgz", + "integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==", + "dev": true + }, + "node_modules/slash": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/slash/-/slash-3.0.0.tgz", + "integrity": "sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/source-map-js": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.0.2.tgz", + "integrity": "sha512-R0XvVJ9WusLiqTCEiGCmICCMplcCkIwwR11mOSD9CR5u+IXYdiseeEuXCVAjS54zqwkLcPNnmU4OeJ6tUrWhDw==", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/stackback": { + "version": "0.0.2", + "resolved": "https://registry.npmjs.org/stackback/-/stackback-0.0.2.tgz", + "integrity": "sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw==", + "dev": true + }, + "node_modules/std-env": { + "version": "3.4.3", + "resolved": "https://registry.npmjs.org/std-env/-/std-env-3.4.3.tgz", + "integrity": "sha512-f9aPhy8fYBuMN+sNfakZV18U39PbalgjXG3lLB9WkaYTxijru61wb57V9wxxNthXM5Sd88ETBWi29qLAsHO52Q==", + "dev": true + }, + "node_modules/stop-iteration-iterator": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/stop-iteration-iterator/-/stop-iteration-iterator-1.0.0.tgz", + "integrity": "sha512-iCGQj+0l0HOdZ2AEeBADlsRC+vsnDsZsbdSiH1yNSjcfKM7fdpCMfqAL/dwF5BLiw/XhRft/Wax6zQbhq2BcjQ==", + "dev": true, + "dependencies": { + "internal-slot": "^1.0.4" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/string-width": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-5.1.2.tgz", + "integrity": "sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==", + "dev": true, + "dependencies": { + "eastasianwidth": "^0.2.0", + "emoji-regex": "^9.2.2", + "strip-ansi": "^7.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/string-width-cjs": { + "name": "string-width", + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "dev": true, + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/string-width-cjs/node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "dev": true + }, + "node_modules/string-width/node_modules/ansi-regex": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.0.1.tgz", + "integrity": "sha512-n5M855fKb2SsfMIiFFoVrABHJC8QtHwVx+mHWP3QcEqBHYienj5dHSgjbxtC0WEZXYt4wcD6zrQElDPhFuZgfA==", + "dev": true, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-regex?sponsor=1" + } + }, + "node_modules/string-width/node_modules/strip-ansi": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.0.tgz", + "integrity": "sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ==", + "dev": true, + "dependencies": { + "ansi-regex": "^6.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/strip-ansi?sponsor=1" + } + }, + "node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-ansi-cjs": { + "name": "strip-ansi", + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-json-comments": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz", + "integrity": "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==", + "dev": true, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/strip-literal": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/strip-literal/-/strip-literal-1.0.1.tgz", + "integrity": "sha512-QZTsipNpa2Ppr6v1AmJHESqJ3Uz247MUS0OjrnnZjFAvEoWqxuyFuXn2xLgMtRnijJShAa1HL0gtJyUs7u7n3Q==", + "dev": true, + "dependencies": { + "acorn": "^8.8.2" + }, + "funding": { + "url": "https://github.com/sponsors/antfu" + } + }, + "node_modules/supports-color": { + "version": "5.5.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", + "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", + "dev": true, + "dependencies": { + "has-flag": "^3.0.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/svg-tags": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/svg-tags/-/svg-tags-1.0.0.tgz", + "integrity": "sha512-ovssysQTa+luh7A5Weu3Rta6FJlFBBbInjOh722LIt6klpU2/HtdUbszju/G4devcvk8PGt7FCLv5wftu3THUA==", + "dev": true + }, + "node_modules/symbol-tree": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/symbol-tree/-/symbol-tree-3.2.4.tgz", + "integrity": "sha512-9QNk5KwDF+Bvz+PyObkmSYjI5ksVUYtjW7AU22r2NKcfLJcXp96hkDWU3+XndOsUb+AQ9QhfzfCT2O+CNWT5Tw==", + "dev": true, + "optional": true, + "peer": true + }, + "node_modules/test-exclude": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/test-exclude/-/test-exclude-6.0.0.tgz", + "integrity": "sha512-cAGWPIyOHU6zlmg88jwm7VRyXnMN7iV68OGAbYDk/Mh/xC/pzVPlQtY6ngoIH/5/tciuhGfvESU8GrHrcxD56w==", + "dev": true, + "dependencies": { + "@istanbuljs/schema": "^0.1.2", + "glob": "^7.1.4", + "minimatch": "^3.0.4" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/text-table": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/text-table/-/text-table-0.2.0.tgz", + "integrity": "sha512-N+8UisAXDGk8PFXP4HAzVR9nbfmVJ3zYLAWiTIoqC5v5isinhr+r5uaO8+7r3BMfuNIufIsA7RdpVgacC2cSpw==", + "dev": true + }, + "node_modules/tiny-case": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/tiny-case/-/tiny-case-1.0.3.tgz", + "integrity": "sha512-Eet/eeMhkO6TX8mnUteS9zgPbUMQa4I6Kkp5ORiBD5476/m+PIRiumP5tmh5ioJpH7k51Kehawy2UDfsnxxY8Q==" + }, + "node_modules/tinybench": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/tinybench/-/tinybench-2.5.0.tgz", + "integrity": "sha512-kRwSG8Zx4tjF9ZiyH4bhaebu+EDz1BOx9hOigYHlUW4xxI/wKIUQUqo018UlU4ar6ATPBsaMrdbKZ+tmPdohFA==", + "dev": true + }, + "node_modules/tinypool": { + "version": "0.7.0", + "resolved": "https://registry.npmjs.org/tinypool/-/tinypool-0.7.0.tgz", + "integrity": "sha512-zSYNUlYSMhJ6Zdou4cJwo/p7w5nmAH17GRfU/ui3ctvjXFErXXkruT4MWW6poDeXgCaIBlGLrfU6TbTXxyGMww==", + "dev": true, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/tinyspy": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/tinyspy/-/tinyspy-2.2.0.tgz", + "integrity": "sha512-d2eda04AN/cPOR89F7Xv5bK/jrQEhmcLFe6HFldoeO9AJtps+fqEnh486vnT/8y4bw38pSyxDcTCAq+Ks2aJTg==", + "dev": true, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/to-fast-properties": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/to-fast-properties/-/to-fast-properties-2.0.0.tgz", + "integrity": "sha512-/OaKK0xYrs3DmxRYqL/yDc+FxFUVYhDlXMhRmv3z915w2HF1tnN1omB354j8VUGO/hbRzyD6Y3sA7v7GS/ceog==", + "dev": true, + "engines": { + "node": ">=4" + } + }, + "node_modules/to-regex-range": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", + "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", + "dev": true, + "dependencies": { + "is-number": "^7.0.0" + }, + "engines": { + "node": ">=8.0" + } + }, + "node_modules/toposort": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/toposort/-/toposort-2.0.2.tgz", + "integrity": "sha512-0a5EOkAUp8D4moMi2W8ZF8jcga7BgZd91O/yabJCFY8az+XSzeGyTKs0Aoo897iV1Nj6guFq8orWDS96z91oGg==" + }, + "node_modules/tough-cookie": { + "version": "4.1.3", + "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-4.1.3.tgz", + "integrity": "sha512-aX/y5pVRkfRnfmuX+OdbSdXvPe6ieKX/G2s7e98f4poJHnqH3281gDPm/metm6E/WRamfx7WC4HUqkWHfQHprw==", + "dev": true, + "optional": true, + "peer": true, + "dependencies": { + "psl": "^1.1.33", + "punycode": "^2.1.1", + "universalify": "^0.2.0", + "url-parse": "^1.5.3" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/ts-api-utils": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-1.0.3.tgz", + "integrity": "sha512-wNMeqtMz5NtwpT/UZGY5alT+VoKdSsOOP/kqHFcUW1P/VRhH2wJ48+DN2WwUliNbQ976ETwDL0Ifd2VVvgonvg==", + "dev": true, + "engines": { + "node": ">=16.13.0" + }, + "peerDependencies": { + "typescript": ">=4.2.0" + } + }, + "node_modules/ts-node": { + "version": "10.9.1", + "resolved": "https://registry.npmjs.org/ts-node/-/ts-node-10.9.1.tgz", + "integrity": "sha512-NtVysVPkxxrwFGUUxGYhfux8k78pQB3JqYBXlLRZgdGUqTO5wU/UyHop5p70iEbGhB7q5KmiZiU0Y3KlJrScEw==", + "dev": true, + "dependencies": { + "@cspotcode/source-map-support": "^0.8.0", + "@tsconfig/node10": "^1.0.7", + "@tsconfig/node12": "^1.0.7", + "@tsconfig/node14": "^1.0.0", + "@tsconfig/node16": "^1.0.2", + "acorn": "^8.4.1", + "acorn-walk": "^8.1.1", + "arg": "^4.1.0", + "create-require": "^1.1.0", + "diff": "^4.0.1", + "make-error": "^1.1.1", + "v8-compile-cache-lib": "^3.0.1", + "yn": "3.1.1" + }, + "bin": { + "ts-node": "dist/bin.js", + "ts-node-cwd": "dist/bin-cwd.js", + "ts-node-esm": "dist/bin-esm.js", + "ts-node-script": "dist/bin-script.js", + "ts-node-transpile-only": "dist/bin-transpile.js", + "ts-script": "dist/bin-script-deprecated.js" + }, + "peerDependencies": { + "@swc/core": ">=1.2.50", + "@swc/wasm": ">=1.2.50", + "@types/node": "*", + "typescript": ">=2.7" + }, + "peerDependenciesMeta": { + "@swc/core": { + "optional": true + }, + "@swc/wasm": { + "optional": true + } + } + }, + "node_modules/type-check": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz", + "integrity": "sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==", + "dev": true, + "dependencies": { + "prelude-ls": "^1.2.1" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/type-detect": { + "version": "4.0.8", + "resolved": "https://registry.npmjs.org/type-detect/-/type-detect-4.0.8.tgz", + "integrity": "sha512-0fr/mIH1dlO+x7TlcMy+bIDqKPsw/70tVyeHW787goQjhmqaZe10uwLujubK9q9Lg6Fiho1KUKDYz0Z7k7g5/g==", + "dev": true, + "engines": { + "node": ">=4" + } + }, + "node_modules/type-fest": { + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.20.2.tgz", + "integrity": "sha512-Ne+eE4r0/iWnpAxD852z3A+N0Bt5RN//NjJwRd2VFHEmrywxf5vsZlh4R6lixl6B+wz/8d+maTSAkN1FIkI3LQ==", + "dev": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/typescript": { + "version": "5.2.2", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.2.2.tgz", + "integrity": "sha512-mI4WrpHsbCIcwT9cF4FZvr80QUeKvsUsUvKDoR+X/7XHQH98xYD8YHZg7ANtz2GtZt/CBq2QJ0thkGJMHfqc1w==", + "devOptional": true, + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/ufo": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/ufo/-/ufo-1.3.1.tgz", + "integrity": "sha512-uY/99gMLIOlJPwATcMVYfqDSxUR9//AUcgZMzwfSTJPDKzA1S8mX4VLqa+fiAtveraQUBCz4FFcwVZBGbwBXIw==", + "dev": true + }, + "node_modules/undici-types": { + "version": "5.26.5", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-5.26.5.tgz", + "integrity": "sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA==", + "dev": true + }, + "node_modules/universalify": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/universalify/-/universalify-0.2.0.tgz", + "integrity": "sha512-CJ1QgKmNg3CwvAv/kOFmtnEN05f0D/cn9QntgNOQlQF9dgvVTHj3t+8JPdjqawCHk7V/KA+fbUqzZ9XWhcqPUg==", + "dev": true, + "optional": true, + "peer": true, + "engines": { + "node": ">= 4.0.0" + } + }, + "node_modules/update-browserslist-db": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.0.11.tgz", + "integrity": "sha512-dCwEFf0/oT85M1fHBg4F0jtLwJrutGoHSQXCh7u4o2t1drG+c0a9Flnqww6XUKSfQMPpJBRjU8d4RXB09qtvaA==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "dependencies": { + "escalade": "^3.1.1", + "picocolors": "^1.0.0" + }, + "bin": { + "update-browserslist-db": "cli.js" + }, + "peerDependencies": { + "browserslist": ">= 4.21.0" + } + }, + "node_modules/uri-js": { + "version": "4.4.1", + "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz", + "integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==", + "dev": true, + "dependencies": { + "punycode": "^2.1.0" + } + }, + "node_modules/url-parse": { + "version": "1.5.10", + "resolved": "https://registry.npmjs.org/url-parse/-/url-parse-1.5.10.tgz", + "integrity": "sha512-WypcfiRhfeUP9vvF0j6rw0J3hrWrw6iZv3+22h6iRMJ/8z1Tj6XfLP4DsUix5MhMPnXpiHDoKyoZ/bdCkwBCiQ==", + "dev": true, + "optional": true, + "peer": true, + "dependencies": { + "querystringify": "^2.1.1", + "requires-port": "^1.0.0" + } + }, + "node_modules/util-deprecate": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", + "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==", + "dev": true + }, + "node_modules/v8-compile-cache-lib": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/v8-compile-cache-lib/-/v8-compile-cache-lib-3.0.1.tgz", + "integrity": "sha512-wa7YjyUGfNZngI/vtK0UHAN+lgDCxBPCylVXGp0zu59Fz5aiGtNXaq3DhIov063MorB+VfufLh3JlF2KdTK3xg==", + "dev": true + }, + "node_modules/v8-to-istanbul": { + "version": "9.1.0", + "resolved": "https://registry.npmjs.org/v8-to-istanbul/-/v8-to-istanbul-9.1.0.tgz", + "integrity": "sha512-6z3GW9x8G1gd+JIIgQQQxXuiJtCXeAjp6RaPEPLv62mH3iPHPxV6W3robxtCzNErRo6ZwTmzWhsbNvjyEBKzKA==", + "dev": true, + "dependencies": { + "@jridgewell/trace-mapping": "^0.3.12", + "@types/istanbul-lib-coverage": "^2.0.1", + "convert-source-map": "^1.6.0" + }, + "engines": { + "node": ">=10.12.0" + } + }, + "node_modules/vee-validate": { + "version": "4.11.8", + "resolved": "https://registry.npmjs.org/vee-validate/-/vee-validate-4.11.8.tgz", + "integrity": "sha512-ZuVpw0axWYBM3aVTD/bm94hcWHumqeUgNjptOqfBT0gyqyHaGYCrm0tSD/0bygEbWUDwEPJOQaEKaUGM82j8TQ==", + "dependencies": { + "@vue/devtools-api": "^6.5.0", + "type-fest": "^4.3.1" + }, + "peerDependencies": { + "vue": "^3.3.4" + } + }, + "node_modules/vee-validate/node_modules/type-fest": { + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-4.3.1.tgz", + "integrity": "sha512-pphNW/msgOUSkJbH58x8sqpq8uQj6b0ZKGxEsLKMUnGorRcDjrUaLS+39+/ub41JNTwrrMyJcUB8+YZs3mbwqw==", + "engines": { + "node": ">=16" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/vite": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/vite/-/vite-4.5.0.tgz", + "integrity": "sha512-ulr8rNLA6rkyFAlVWw2q5YJ91v098AFQ2R0PRFwPzREXOUJQPtFUG0t+/ZikhaOCDqFoDhN6/v8Sq0o4araFAw==", + "dev": true, + "dependencies": { + "esbuild": "^0.18.10", + "postcss": "^8.4.27", + "rollup": "^3.27.1" + }, + "bin": { + "vite": "bin/vite.js" + }, + "engines": { + "node": "^14.18.0 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/vitejs/vite?sponsor=1" + }, + "optionalDependencies": { + "fsevents": "~2.3.2" + }, + "peerDependencies": { + "@types/node": ">= 14", + "less": "*", + "lightningcss": "^1.21.0", + "sass": "*", + "stylus": "*", + "sugarss": "*", + "terser": "^5.4.0" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + }, + "less": { + "optional": true + }, + "lightningcss": { + "optional": true + }, + "sass": { + "optional": true + }, + "stylus": { + "optional": true + }, + "sugarss": { + "optional": true + }, + "terser": { + "optional": true + } + } + }, + "node_modules/vite-node": { + "version": "0.34.6", + "resolved": "https://registry.npmjs.org/vite-node/-/vite-node-0.34.6.tgz", + "integrity": "sha512-nlBMJ9x6n7/Amaz6F3zJ97EBwR2FkzhBRxF5e+jE6LA3yi6Wtc2lyTij1OnDMIr34v5g/tVQtsVAzhT0jc5ygA==", + "dev": true, + "dependencies": { + "cac": "^6.7.14", + "debug": "^4.3.4", + "mlly": "^1.4.0", + "pathe": "^1.1.1", + "picocolors": "^1.0.0", + "vite": "^3.0.0 || ^4.0.0 || ^5.0.0-0" + }, + "bin": { + "vite-node": "vite-node.mjs" + }, + "engines": { + "node": ">=v14.18.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/vitest": { + "version": "0.34.6", + "resolved": "https://registry.npmjs.org/vitest/-/vitest-0.34.6.tgz", + "integrity": "sha512-+5CALsOvbNKnS+ZHMXtuUC7nL8/7F1F2DnHGjSsszX8zCjWSSviphCb/NuS9Nzf4Q03KyyDRBAXhF/8lffME4Q==", + "dev": true, + "dependencies": { + "@types/chai": "^4.3.5", + "@types/chai-subset": "^1.3.3", + "@types/node": "*", + "@vitest/expect": "0.34.6", + "@vitest/runner": "0.34.6", + "@vitest/snapshot": "0.34.6", + "@vitest/spy": "0.34.6", + "@vitest/utils": "0.34.6", + "acorn": "^8.9.0", + "acorn-walk": "^8.2.0", + "cac": "^6.7.14", + "chai": "^4.3.10", + "debug": "^4.3.4", + "local-pkg": "^0.4.3", + "magic-string": "^0.30.1", + "pathe": "^1.1.1", + "picocolors": "^1.0.0", + "std-env": "^3.3.3", + "strip-literal": "^1.0.1", + "tinybench": "^2.5.0", + "tinypool": "^0.7.0", + "vite": "^3.1.0 || ^4.0.0 || ^5.0.0-0", + "vite-node": "0.34.6", + "why-is-node-running": "^2.2.2" + }, + "bin": { + "vitest": "vitest.mjs" + }, + "engines": { + "node": ">=v14.18.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + }, + "peerDependencies": { + "@edge-runtime/vm": "*", + "@vitest/browser": "*", + "@vitest/ui": "*", + "happy-dom": "*", + "jsdom": "*", + "playwright": "*", + "safaridriver": "*", + "webdriverio": "*" + }, + "peerDependenciesMeta": { + "@edge-runtime/vm": { + "optional": true + }, + "@vitest/browser": { + "optional": true + }, + "@vitest/ui": { + "optional": true + }, + "happy-dom": { + "optional": true + }, + "jsdom": { + "optional": true + }, + "playwright": { + "optional": true + }, + "safaridriver": { + "optional": true + }, + "webdriverio": { + "optional": true + } + } + }, + "node_modules/volar-service-eslint": { + "version": "0.0.16", + "resolved": "https://registry.npmjs.org/volar-service-eslint/-/volar-service-eslint-0.0.16.tgz", + "integrity": "sha512-S3zfl6zLKgGaammiwWRAxNlrH3SyyR06MKL94+7tq9VXTp83cTpMPaaMhrpdpccPZjM8RzwcfSg0LzV3b2mvfQ==", + "dev": true, + "peerDependencies": { + "@volar/language-service": "~1.10.0", + "eslint": "*" + }, + "peerDependenciesMeta": { + "@volar/language-service": { + "optional": true + } + } + }, + "node_modules/vue": { + "version": "3.3.7", + "resolved": "https://registry.npmjs.org/vue/-/vue-3.3.7.tgz", + "integrity": "sha512-YEMDia1ZTv1TeBbnu6VybatmSteGOS3A3YgfINOfraCbf85wdKHzscD6HSS/vB4GAtI7sa1XPX7HcQaJ1l24zA==", + "dependencies": { + "@vue/compiler-dom": "3.3.7", + "@vue/compiler-sfc": "3.3.7", + "@vue/runtime-dom": "3.3.7", + "@vue/server-renderer": "3.3.7", + "@vue/shared": "3.3.7" + }, + "peerDependencies": { + "typescript": "*" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/vue-component-type-helpers": { + "version": "1.8.4", + "resolved": "https://registry.npmjs.org/vue-component-type-helpers/-/vue-component-type-helpers-1.8.4.tgz", + "integrity": "sha512-6bnLkn8O0JJyiFSIF0EfCogzeqNXpnjJ0vW/SZzNHfe6sPx30lTtTXlE5TFs2qhJlAtDFybStVNpL73cPe3OMQ==", + "dev": true + }, + "node_modules/vue-eslint-parser": { + "version": "9.3.1", + "resolved": "https://registry.npmjs.org/vue-eslint-parser/-/vue-eslint-parser-9.3.1.tgz", + "integrity": "sha512-Clr85iD2XFZ3lJ52/ppmUDG/spxQu6+MAeHXjjyI4I1NUYZ9xmenQp4N0oaHJhrA8OOxltCVxMRfANGa70vU0g==", + "dev": true, + "dependencies": { + "debug": "^4.3.4", + "eslint-scope": "^7.1.1", + "eslint-visitor-keys": "^3.3.0", + "espree": "^9.3.1", + "esquery": "^1.4.0", + "lodash": "^4.17.21", + "semver": "^7.3.6" + }, + "engines": { + "node": "^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/mysticatea" + }, + "peerDependencies": { + "eslint": ">=6.0.0" + } + }, + "node_modules/vue-eslint-parser/node_modules/eslint-scope": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-7.1.1.tgz", + "integrity": "sha512-QKQM/UXpIiHcLqJ5AOyIW7XZmzjkzQXYE54n1++wb0u9V/abW3l9uQnxX8Z5Xd18xyKIMTUAyQ0k1e8pz6LUrw==", + "dev": true, + "dependencies": { + "esrecurse": "^4.3.0", + "estraverse": "^5.2.0" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + } + }, + "node_modules/vue-eslint-parser/node_modules/estraverse": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz", + "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==", + "dev": true, + "engines": { + "node": ">=4.0" + } + }, + "node_modules/vue-eslint-parser/node_modules/semver": { + "version": "7.5.4", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.5.4.tgz", + "integrity": "sha512-1bCSESV6Pv+i21Hvpxp3Dx+pSD8lIPt8uVjRrxAUt/nbswYc+tK6Y2btiULjd4+fnq15PX+nqQDC7Oft7WkwcA==", + "dev": true, + "dependencies": { + "lru-cache": "^6.0.0" + }, + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/vue-router": { + "version": "4.2.5", + "resolved": "https://registry.npmjs.org/vue-router/-/vue-router-4.2.5.tgz", + "integrity": "sha512-DIUpKcyg4+PTQKfFPX88UWhlagBEBEfJ5A8XDXRJLUnZOvcpMF8o/dnL90vpVkGaPbjvXazV/rC1qBKrZlFugw==", + "dependencies": { + "@vue/devtools-api": "^6.5.0" + }, + "funding": { + "url": "https://github.com/sponsors/posva" + }, + "peerDependencies": { + "vue": "^3.2.0" + } + }, + "node_modules/vue-template-compiler": { + "version": "2.7.15", + "resolved": "https://registry.npmjs.org/vue-template-compiler/-/vue-template-compiler-2.7.15.tgz", + "integrity": "sha512-yQxjxMptBL7UAog00O8sANud99C6wJF+7kgbcwqkvA38vCGF7HWE66w0ZFnS/kX5gSoJr/PQ4/oS3Ne2pW37Og==", + "dev": true, + "dependencies": { + "de-indent": "^1.0.2", + "he": "^1.2.0" + } + }, + "node_modules/vue-tsc": { + "version": "1.8.22", + "resolved": "https://registry.npmjs.org/vue-tsc/-/vue-tsc-1.8.22.tgz", + "integrity": "sha512-j9P4kHtW6eEE08aS5McFZE/ivmipXy0JzrnTgbomfABMaVKx37kNBw//irL3+LlE3kOo63XpnRigyPC3w7+z+A==", + "dev": true, + "dependencies": { + "@volar/typescript": "~1.10.5", + "@vue/language-core": "1.8.22", + "semver": "^7.5.4" + }, + "bin": { + "vue-tsc": "bin/vue-tsc.js" + }, + "peerDependencies": { + "typescript": "*" + } + }, + "node_modules/vue-tsc/node_modules/semver": { + "version": "7.5.4", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.5.4.tgz", + "integrity": "sha512-1bCSESV6Pv+i21Hvpxp3Dx+pSD8lIPt8uVjRrxAUt/nbswYc+tK6Y2btiULjd4+fnq15PX+nqQDC7Oft7WkwcA==", + "dev": true, + "dependencies": { + "lru-cache": "^6.0.0" + }, + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/w3c-xmlserializer": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/w3c-xmlserializer/-/w3c-xmlserializer-4.0.0.tgz", + "integrity": "sha512-d+BFHzbiCx6zGfz0HyQ6Rg69w9k19nviJspaj4yNscGjrHu94sVP+aRm75yEbCh+r2/yR+7q6hux9LVtbuTGBw==", + "dev": true, + "optional": true, + "peer": true, + "dependencies": { + "xml-name-validator": "^4.0.0" + }, + "engines": { + "node": ">=14" + } + }, + "node_modules/webidl-conversions": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-7.0.0.tgz", + "integrity": "sha512-VwddBukDzu71offAQR975unBIGqfKZpM+8ZX6ySk8nYhVoo5CYaZyzt3YBvYtRtO+aoGlqxPg/B87NGVZ/fu6g==", + "dev": true, + "engines": { + "node": ">=12" + } + }, + "node_modules/whatwg-encoding": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/whatwg-encoding/-/whatwg-encoding-2.0.0.tgz", + "integrity": "sha512-p41ogyeMUrw3jWclHWTQg1k05DSVXPLcVxRTYsXUk+ZooOCZLcoYgPZ/HL/D/N+uQPOtcp1me1WhBEaX02mhWg==", + "dev": true, + "dependencies": { + "iconv-lite": "0.6.3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/whatwg-mimetype": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/whatwg-mimetype/-/whatwg-mimetype-3.0.0.tgz", + "integrity": "sha512-nt+N2dzIutVRxARx1nghPKGv1xHikU7HKdfafKkLNLindmPU/ch3U31NOCGGA/dmPcmb1VlofO0vnKAcsm0o/Q==", + "dev": true, + "engines": { + "node": ">=12" + } + }, + "node_modules/which": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", + "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", + "dev": true, + "dependencies": { + "isexe": "^2.0.0" + }, + "bin": { + "node-which": "bin/node-which" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/which-boxed-primitive": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/which-boxed-primitive/-/which-boxed-primitive-1.0.2.tgz", + "integrity": "sha512-bwZdv0AKLpplFY2KZRX6TvyuN7ojjr7lwkg6ml0roIy9YeuSr7JS372qlNW18UQYzgYK9ziGcerWqZOmEn9VNg==", + "dev": true, + "dependencies": { + "is-bigint": "^1.0.1", + "is-boolean-object": "^1.1.0", + "is-number-object": "^1.0.4", + "is-string": "^1.0.5", + "is-symbol": "^1.0.3" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/which-collection": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/which-collection/-/which-collection-1.0.1.tgz", + "integrity": "sha512-W8xeTUwaln8i3K/cY1nGXzdnVZlidBcagyNFtBdD5kxnb4TvGKR7FfSIS3mYpwWS1QUCutfKz8IY8RjftB0+1A==", + "dev": true, + "dependencies": { + "is-map": "^2.0.1", + "is-set": "^2.0.1", + "is-weakmap": "^2.0.1", + "is-weakset": "^2.0.1" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/which-typed-array": { + "version": "1.1.13", + "resolved": "https://registry.npmjs.org/which-typed-array/-/which-typed-array-1.1.13.tgz", + "integrity": "sha512-P5Nra0qjSncduVPEAr7xhoF5guty49ArDTwzJ/yNuPIbZppyRxFQsRCWrocxIY+CnMVG+qfbU2FmDKyvSGClow==", + "dev": true, + "dependencies": { + "available-typed-arrays": "^1.0.5", + "call-bind": "^1.0.4", + "for-each": "^0.3.3", + "gopd": "^1.0.1", + "has-tostringtag": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/why-is-node-running": { + "version": "2.2.2", + "resolved": "https://registry.npmjs.org/why-is-node-running/-/why-is-node-running-2.2.2.tgz", + "integrity": "sha512-6tSwToZxTOcotxHeA+qGCq1mVzKR3CwcJGmVcY+QE8SHy6TnpFnh8PAvPNHYr7EcuVeG0QSMxtYCuO1ta/G/oA==", + "dev": true, + "dependencies": { + "siginfo": "^2.0.0", + "stackback": "0.0.2" + }, + "bin": { + "why-is-node-running": "cli.js" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/wrap-ansi": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", + "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", + "dev": true, + "dependencies": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/wrap-ansi-cjs": { + "name": "wrap-ansi", + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", + "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", + "dev": true, + "dependencies": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/wrap-ansi-cjs/node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/wrap-ansi-cjs/node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/wrap-ansi-cjs/node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true + }, + "node_modules/wrap-ansi-cjs/node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "dev": true + }, + "node_modules/wrap-ansi-cjs/node_modules/string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "dev": true, + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/wrap-ansi/node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/wrap-ansi/node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/wrap-ansi/node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true + }, + "node_modules/wrap-ansi/node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "dev": true + }, + "node_modules/wrap-ansi/node_modules/string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "dev": true, + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/wrappy": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", + "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", + "dev": true + }, + "node_modules/ws": { + "version": "8.13.0", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.13.0.tgz", + "integrity": "sha512-x9vcZYTrFPC7aSIbj7sRCYo7L/Xb8Iy+pW0ng0wt2vCJv7M9HOMy0UoN3rr+IFC7hb7vXoqS+P9ktyLLLhO+LA==", + "dev": true, + "optional": true, + "peer": true, + "engines": { + "node": ">=10.0.0" + }, + "peerDependencies": { + "bufferutil": "^4.0.1", + "utf-8-validate": ">=5.0.2" + }, + "peerDependenciesMeta": { + "bufferutil": { + "optional": true + }, + "utf-8-validate": { + "optional": true + } + } + }, + "node_modules/xml-name-validator": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/xml-name-validator/-/xml-name-validator-4.0.0.tgz", + "integrity": "sha512-ICP2e+jsHvAj2E2lIHxa5tjXRlKDJo4IdvPvCXbXQGdzSfmSpNVyIKMvoZHjDY9DP0zV17iI85o90vRFXNccRw==", + "dev": true, + "engines": { + "node": ">=12" + } + }, + "node_modules/xmlchars": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/xmlchars/-/xmlchars-2.2.0.tgz", + "integrity": "sha512-JZnDKK8B0RCDw84FNdDAIpZK+JuJw+s7Lz8nksI7SIuU3UXJJslUthsi+uWBUYOwPFwW7W7PRLRfUKpxjtjFCw==", + "dev": true, + "optional": true, + "peer": true + }, + "node_modules/y18n": { + "version": "5.0.8", + "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz", + "integrity": "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==", + "dev": true, + "engines": { + "node": ">=10" + } + }, + "node_modules/yallist": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", + "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", + "dev": true + }, + "node_modules/yargs": { + "version": "16.2.0", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-16.2.0.tgz", + "integrity": "sha512-D1mvvtDG0L5ft/jGWkLpG1+m0eQxOfaBvTNELraWj22wSVUMWxZUvYgJYcKh6jGGIkJFhH4IZPQhR4TKpc8mBw==", + "dev": true, + "dependencies": { + "cliui": "^7.0.2", + "escalade": "^3.1.1", + "get-caller-file": "^2.0.5", + "require-directory": "^2.1.1", + "string-width": "^4.2.0", + "y18n": "^5.0.5", + "yargs-parser": "^20.2.2" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/yargs-parser": { + "version": "20.2.9", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-20.2.9.tgz", + "integrity": "sha512-y11nGElTIV+CT3Zv9t7VKl+Q3hTQoT9a1Qzezhhl6Rp21gJ/IVTW7Z3y9EWXhuUBC2Shnf+DX0antecpAwSP8w==", + "dev": true, + "engines": { + "node": ">=10" + } + }, + "node_modules/yargs/node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "dev": true + }, + "node_modules/yargs/node_modules/string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "dev": true, + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/yn": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/yn/-/yn-3.1.1.tgz", + "integrity": "sha512-Ux4ygGWsu2c7isFWe8Yu1YluJmqVhxqK2cLXNQA5AcC3QfbGNpM7fu0Y8b/z16pXLnFxZYvWhd3fhBY9DLmC6Q==", + "dev": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/yocto-queue": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", + "integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==", + "dev": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/yup": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/yup/-/yup-1.3.2.tgz", + "integrity": "sha512-6KCM971iQtJ+/KUaHdrhVr2LDkfhBtFPRnsG1P8F4q3uUVQ2RfEM9xekpha9aA4GXWJevjM10eDcPQ1FfWlmaQ==", + "dependencies": { + "property-expr": "^2.0.5", + "tiny-case": "^1.0.3", + "toposort": "^2.0.2", + "type-fest": "^2.19.0" + } + }, + "node_modules/yup/node_modules/type-fest": { + "version": "2.19.0", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-2.19.0.tgz", + "integrity": "sha512-RAH822pAdBgcNMAfWnCBU3CFZcfZ/i1eZjwFU/dsLKumyuuP3niueg2UAukXYF0E2AAoc82ZSSf9J0WQBinzHA==", + "engines": { + "node": ">=12.20" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + } + }, + "dependencies": { + "@aashutoshrathi/word-wrap": { + "version": "1.2.6", + "resolved": "https://registry.npmjs.org/@aashutoshrathi/word-wrap/-/word-wrap-1.2.6.tgz", + "integrity": "sha512-1Yjs2SvM8TflER/OD3cOjhWWOZb58A2t7wpE2S9XfBYTiIl+XFhQG2bjy4Pu1I+EAlCNUzRDYDdFwFYUKvXcIA==", + "dev": true + }, + "@ampproject/remapping": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/@ampproject/remapping/-/remapping-2.2.1.tgz", + "integrity": "sha512-lFMjJTrFL3j7L9yBxwYfCq2k6qqwHyzuUl/XBnif78PWTJYyL/dfowQHWE3sp6U6ZzqWiiIZnpTMO96zhkjwtg==", + "dev": true, + "requires": { + "@jridgewell/gen-mapping": "^0.3.0", + "@jridgewell/trace-mapping": "^0.3.9" + } + }, + "@babel/code-frame": { + "version": "7.22.13", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.22.13.tgz", + "integrity": "sha512-XktuhWlJ5g+3TJXc5upd9Ks1HutSArik6jf2eAjYFyIOf4ej3RN+184cZbzDvbPnuTJIUhPKKJE3cIsYTiAT3w==", + "dev": true, + "requires": { + "@babel/highlight": "^7.22.13", + "chalk": "^2.4.2" + } + }, + "@babel/compat-data": { + "version": "7.22.9", + "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.22.9.tgz", + "integrity": "sha512-5UamI7xkUcJ3i9qVDS+KFDEK8/7oJ55/sJMB1Ge7IEapr7KfdfV/HErR+koZwOfd+SgtFKOKRhRakdg++DcJpQ==", + "dev": true + }, + "@babel/core": { + "version": "7.22.11", + "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.22.11.tgz", + "integrity": "sha512-lh7RJrtPdhibbxndr6/xx0w8+CVlY5FJZiaSz908Fpy+G0xkBFTvwLcKJFF4PJxVfGhVWNebikpWGnOoC71juQ==", + "dev": true, + "requires": { + "@ampproject/remapping": "^2.2.0", + "@babel/code-frame": "^7.22.10", + "@babel/generator": "^7.22.10", + "@babel/helper-compilation-targets": "^7.22.10", + "@babel/helper-module-transforms": "^7.22.9", + "@babel/helpers": "^7.22.11", + "@babel/parser": "^7.22.11", + "@babel/template": "^7.22.5", + "@babel/traverse": "^7.22.11", + "@babel/types": "^7.22.11", + "convert-source-map": "^1.7.0", + "debug": "^4.1.0", + "gensync": "^1.0.0-beta.2", + "json5": "^2.2.3", + "semver": "^6.3.1" + } + }, + "@babel/generator": { + "version": "7.23.0", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.23.0.tgz", + "integrity": "sha512-lN85QRR+5IbYrMWM6Y4pE/noaQtg4pNiqeNGX60eqOfo6gtEj6uw/JagelB8vVztSd7R6M5n1+PQkDbHbBRU4g==", + "dev": true, + "requires": { + "@babel/types": "^7.23.0", + "@jridgewell/gen-mapping": "^0.3.2", + "@jridgewell/trace-mapping": "^0.3.17", + "jsesc": "^2.5.1" + } + }, + "@babel/helper-annotate-as-pure": { + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/helper-annotate-as-pure/-/helper-annotate-as-pure-7.22.5.tgz", + "integrity": "sha512-LvBTxu8bQSQkcyKOU+a1btnNFQ1dMAd0R6PyW3arXes06F6QLWLIrd681bxRPIXlrMGR3XYnW9JyML7dP3qgxg==", + "dev": true, + "requires": { + "@babel/types": "^7.22.5" + } + }, + "@babel/helper-compilation-targets": { + "version": "7.22.10", + "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.22.10.tgz", + "integrity": "sha512-JMSwHD4J7SLod0idLq5PKgI+6g/hLD/iuWBq08ZX49xE14VpVEojJ5rHWptpirV2j020MvypRLAXAO50igCJ5Q==", + "dev": true, + "requires": { + "@babel/compat-data": "^7.22.9", + "@babel/helper-validator-option": "^7.22.5", + "browserslist": "^4.21.9", + "lru-cache": "^5.1.1", + "semver": "^6.3.1" + }, + "dependencies": { + "lru-cache": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz", + "integrity": "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==", + "dev": true, + "requires": { + "yallist": "^3.0.2" + } + }, + "yallist": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz", + "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==", + "dev": true + } + } + }, + "@babel/helper-create-class-features-plugin": { + "version": "7.22.11", + "resolved": "https://registry.npmjs.org/@babel/helper-create-class-features-plugin/-/helper-create-class-features-plugin-7.22.11.tgz", + "integrity": "sha512-y1grdYL4WzmUDBRGK0pDbIoFd7UZKoDurDzWEoNMYoj1EL+foGRQNyPWDcC+YyegN5y1DUsFFmzjGijB3nSVAQ==", + "dev": true, + "requires": { + "@babel/helper-annotate-as-pure": "^7.22.5", + "@babel/helper-environment-visitor": "^7.22.5", + "@babel/helper-function-name": "^7.22.5", + "@babel/helper-member-expression-to-functions": "^7.22.5", + "@babel/helper-optimise-call-expression": "^7.22.5", + "@babel/helper-replace-supers": "^7.22.9", + "@babel/helper-skip-transparent-expression-wrappers": "^7.22.5", + "@babel/helper-split-export-declaration": "^7.22.6", + "semver": "^6.3.1" + } + }, + "@babel/helper-environment-visitor": { + "version": "7.22.20", + "resolved": "https://registry.npmjs.org/@babel/helper-environment-visitor/-/helper-environment-visitor-7.22.20.tgz", + "integrity": "sha512-zfedSIzFhat/gFhWfHtgWvlec0nqB9YEIVrpuwjruLlXfUSnA8cJB0miHKwqDnQ7d32aKo2xt88/xZptwxbfhA==", + "dev": true + }, + "@babel/helper-function-name": { + "version": "7.23.0", + "resolved": "https://registry.npmjs.org/@babel/helper-function-name/-/helper-function-name-7.23.0.tgz", + "integrity": "sha512-OErEqsrxjZTJciZ4Oo+eoZqeW9UIiOcuYKRJA4ZAgV9myA+pOXhhmpfNCKjEH/auVfEYVFJ6y1Tc4r0eIApqiw==", + "dev": true, + "requires": { + "@babel/template": "^7.22.15", + "@babel/types": "^7.23.0" + } + }, + "@babel/helper-hoist-variables": { + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/helper-hoist-variables/-/helper-hoist-variables-7.22.5.tgz", + "integrity": "sha512-wGjk9QZVzvknA6yKIUURb8zY3grXCcOZt+/7Wcy8O2uctxhplmUPkOdlgoNhmdVee2c92JXbf1xpMtVNbfoxRw==", + "dev": true, + "requires": { + "@babel/types": "^7.22.5" + } + }, + "@babel/helper-member-expression-to-functions": { + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/helper-member-expression-to-functions/-/helper-member-expression-to-functions-7.22.5.tgz", + "integrity": "sha512-aBiH1NKMG0H2cGZqspNvsaBe6wNGjbJjuLy29aU+eDZjSbbN53BaxlpB02xm9v34pLTZ1nIQPFYn2qMZoa5BQQ==", + "dev": true, + "requires": { + "@babel/types": "^7.22.5" + } + }, + "@babel/helper-module-imports": { + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.22.5.tgz", + "integrity": "sha512-8Dl6+HD/cKifutF5qGd/8ZJi84QeAKh+CEe1sBzz8UayBBGg1dAIJrdHOcOM5b2MpzWL2yuotJTtGjETq0qjXg==", + "dev": true, + "requires": { + "@babel/types": "^7.22.5" + } + }, + "@babel/helper-module-transforms": { + "version": "7.22.9", + "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.22.9.tgz", + "integrity": "sha512-t+WA2Xn5K+rTeGtC8jCsdAH52bjggG5TKRuRrAGNM/mjIbO4GxvlLMFOEz9wXY5I2XQ60PMFsAG2WIcG82dQMQ==", + "dev": true, + "requires": { + "@babel/helper-environment-visitor": "^7.22.5", + "@babel/helper-module-imports": "^7.22.5", + "@babel/helper-simple-access": "^7.22.5", + "@babel/helper-split-export-declaration": "^7.22.6", + "@babel/helper-validator-identifier": "^7.22.5" + } + }, + "@babel/helper-optimise-call-expression": { + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/helper-optimise-call-expression/-/helper-optimise-call-expression-7.22.5.tgz", + "integrity": "sha512-HBwaojN0xFRx4yIvpwGqxiV2tUfl7401jlok564NgB9EHS1y6QT17FmKWm4ztqjeVdXLuC4fSvHc5ePpQjoTbw==", + "dev": true, + "requires": { + "@babel/types": "^7.22.5" + } + }, + "@babel/helper-plugin-utils": { + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.22.5.tgz", + "integrity": "sha512-uLls06UVKgFG9QD4OeFYLEGteMIAa5kpTPcFL28yuCIIzsf6ZyKZMllKVOCZFhiZ5ptnwX4mtKdWCBE/uT4amg==", + "dev": true + }, + "@babel/helper-replace-supers": { + "version": "7.22.9", + "resolved": "https://registry.npmjs.org/@babel/helper-replace-supers/-/helper-replace-supers-7.22.9.tgz", + "integrity": "sha512-LJIKvvpgPOPUThdYqcX6IXRuIcTkcAub0IaDRGCZH0p5GPUp7PhRU9QVgFcDDd51BaPkk77ZjqFwh6DZTAEmGg==", + "dev": true, + "requires": { + "@babel/helper-environment-visitor": "^7.22.5", + "@babel/helper-member-expression-to-functions": "^7.22.5", + "@babel/helper-optimise-call-expression": "^7.22.5" + } + }, + "@babel/helper-simple-access": { + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/helper-simple-access/-/helper-simple-access-7.22.5.tgz", + "integrity": "sha512-n0H99E/K+Bika3++WNL17POvo4rKWZ7lZEp1Q+fStVbUi8nxPQEBOlTmCOxW/0JsS56SKKQ+ojAe2pHKJHN35w==", + "dev": true, + "requires": { + "@babel/types": "^7.22.5" + } + }, + "@babel/helper-skip-transparent-expression-wrappers": { + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/helper-skip-transparent-expression-wrappers/-/helper-skip-transparent-expression-wrappers-7.22.5.tgz", + "integrity": "sha512-tK14r66JZKiC43p8Ki33yLBVJKlQDFoA8GYN67lWCDCqoL6EMMSuM9b+Iff2jHaM/RRFYl7K+iiru7hbRqNx8Q==", + "dev": true, + "requires": { + "@babel/types": "^7.22.5" + } + }, + "@babel/helper-split-export-declaration": { + "version": "7.22.6", + "resolved": "https://registry.npmjs.org/@babel/helper-split-export-declaration/-/helper-split-export-declaration-7.22.6.tgz", + "integrity": "sha512-AsUnxuLhRYsisFiaJwvp1QF+I3KjD5FOxut14q/GzovUe6orHLesW2C7d754kRm53h5gqrz6sFl6sxc4BVtE/g==", + "dev": true, + "requires": { + "@babel/types": "^7.22.5" + } + }, + "@babel/helper-string-parser": { + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.22.5.tgz", + "integrity": "sha512-mM4COjgZox8U+JcXQwPijIZLElkgEpO5rsERVDJTc2qfCDfERyob6k5WegS14SX18IIjv+XD+GrqNumY5JRCDw==", + "dev": true + }, + "@babel/helper-validator-identifier": { + "version": "7.22.20", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.22.20.tgz", + "integrity": "sha512-Y4OZ+ytlatR8AI+8KZfKuL5urKp7qey08ha31L8b3BwewJAoJamTzyvxPR/5D+KkdJCGPq/+8TukHBlY10FX9A==", + "dev": true + }, + "@babel/helper-validator-option": { + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.22.5.tgz", + "integrity": "sha512-R3oB6xlIVKUnxNUxbmgq7pKjxpru24zlimpE8WK47fACIlM0II/Hm1RS8IaOI7NgCr6LNS+jl5l75m20npAziw==", + "dev": true + }, + "@babel/helpers": { + "version": "7.22.11", + "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.22.11.tgz", + "integrity": "sha512-vyOXC8PBWaGc5h7GMsNx68OH33cypkEDJCHvYVVgVbbxJDROYVtexSk0gK5iCF1xNjRIN2s8ai7hwkWDq5szWg==", + "dev": true, + "requires": { + "@babel/template": "^7.22.5", + "@babel/traverse": "^7.22.11", + "@babel/types": "^7.22.11" + } + }, + "@babel/highlight": { + "version": "7.22.13", + "resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.22.13.tgz", + "integrity": "sha512-C/BaXcnnvBCmHTpz/VGZ8jgtE2aYlW4hxDhseJAWZb7gqGM/qtCK6iZUb0TyKFf7BOUsBH7Q7fkRsDRhg1XklQ==", + "dev": true, + "requires": { + "@babel/helper-validator-identifier": "^7.22.5", + "chalk": "^2.4.2", + "js-tokens": "^4.0.0" + } + }, + "@babel/parser": { + "version": "7.23.0", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.23.0.tgz", + "integrity": "sha512-vvPKKdMemU85V9WE/l5wZEmImpCtLqbnTvqDS2U1fJ96KrxoW7KrXhNsNCblQlg8Ck4b85yxdTyelsMUgFUXiw==" + }, + "@babel/plugin-syntax-jsx": { + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-jsx/-/plugin-syntax-jsx-7.22.5.tgz", + "integrity": "sha512-gvyP4hZrgrs/wWMaocvxZ44Hw0b3W8Pe+cMxc8V1ULQ07oh8VNbIRaoD1LRZVTvD+0nieDKjfgKg89sD7rrKrg==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.22.5" + } + }, + "@babel/plugin-syntax-typescript": { + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-typescript/-/plugin-syntax-typescript-7.22.5.tgz", + "integrity": "sha512-1mS2o03i7t1c6VzH6fdQ3OA8tcEIxwG18zIPRp+UY1Ihv6W+XZzBCVxExF9upussPXJ0xE9XRHwMoNs1ep/nRQ==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.22.5" + } + }, + "@babel/plugin-transform-typescript": { + "version": "7.22.11", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-typescript/-/plugin-transform-typescript-7.22.11.tgz", + "integrity": "sha512-0E4/L+7gfvHub7wsbTv03oRtD69X31LByy44fGmFzbZScpupFByMcgCJ0VbBTkzyjSJKuRoGN8tcijOWKTmqOA==", + "dev": true, + "requires": { + "@babel/helper-annotate-as-pure": "^7.22.5", + "@babel/helper-create-class-features-plugin": "^7.22.11", + "@babel/helper-plugin-utils": "^7.22.5", + "@babel/plugin-syntax-typescript": "^7.22.5" + } + }, + "@babel/runtime": { + "version": "7.23.2", + "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.23.2.tgz", + "integrity": "sha512-mM8eg4yl5D6i3lu2QKPuPH4FArvJ8KhTofbE7jwMUv9KX5mBvwPAqnV3MlyBNqdp9RyRKP6Yck8TrfYrPvX3bg==", + "requires": { + "regenerator-runtime": "^0.14.0" + } + }, + "@babel/template": { + "version": "7.22.15", + "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.22.15.tgz", + "integrity": "sha512-QPErUVm4uyJa60rkI73qneDacvdvzxshT3kksGqlGWYdOTIUOwJ7RDUL8sGqslY1uXWSL6xMFKEXDS3ox2uF0w==", + "dev": true, + "requires": { + "@babel/code-frame": "^7.22.13", + "@babel/parser": "^7.22.15", + "@babel/types": "^7.22.15" + } + }, + "@babel/traverse": { + "version": "7.23.2", + "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.23.2.tgz", + "integrity": "sha512-azpe59SQ48qG6nu2CzcMLbxUudtN+dOM9kDbUqGq3HXUJRlo7i8fvPoxQUzYgLZ4cMVmuZgm8vvBpNeRhd6XSw==", + "dev": true, + "requires": { + "@babel/code-frame": "^7.22.13", + "@babel/generator": "^7.23.0", + "@babel/helper-environment-visitor": "^7.22.20", + "@babel/helper-function-name": "^7.23.0", + "@babel/helper-hoist-variables": "^7.22.5", + "@babel/helper-split-export-declaration": "^7.22.6", + "@babel/parser": "^7.23.0", + "@babel/types": "^7.23.0", + "debug": "^4.1.0", + "globals": "^11.1.0" + } + }, + "@babel/types": { + "version": "7.23.0", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.23.0.tgz", + "integrity": "sha512-0oIyUfKoI3mSqMvsxBdclDwxXKXAUA8v/apZbc+iSyARYou1o8ZGDxbUYyLFoW2arqS2jDGqJuZvv1d/io1axg==", + "dev": true, + "requires": { + "@babel/helper-string-parser": "^7.22.5", + "@babel/helper-validator-identifier": "^7.22.20", + "to-fast-properties": "^2.0.0" + } + }, + "@bcgov/bc-sans": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/@bcgov/bc-sans/-/bc-sans-2.1.0.tgz", + "integrity": "sha512-1MesF4NAVpM5dywoJ68wNcBylHbPqg1dDV/FNuQm0BbspETGlPmfX8LG8rtrCjCAPhWuL2qRT/lBYDUMvFTUnw==" + }, + "@bcoe/v8-coverage": { + "version": "0.2.3", + "resolved": "https://registry.npmjs.org/@bcoe/v8-coverage/-/v8-coverage-0.2.3.tgz", + "integrity": "sha512-0hYQ8SB4Db5zvZB4axdMHGwEaQjkZzFjQiN9LVYvIFB2nSUHW9tYpxWriPrWDASIxiaXax83REcLxuSdnGPZtw==", + "dev": true + }, + "@cspotcode/source-map-support": { + "version": "0.8.1", + "resolved": "https://registry.npmjs.org/@cspotcode/source-map-support/-/source-map-support-0.8.1.tgz", + "integrity": "sha512-IchNf6dN4tHoMFIn/7OE8LWZ19Y6q/67Bmf6vnGREv8RSbBVb9LPJxEcnwrcwX6ixSvaiGoomAUvu4YSxXrVgw==", + "dev": true, + "requires": { + "@jridgewell/trace-mapping": "0.3.9" + }, + "dependencies": { + "@jridgewell/trace-mapping": { + "version": "0.3.9", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.9.tgz", + "integrity": "sha512-3Belt6tdc8bPgAtbcmdtNJlirVoTmEb5e2gC94PnkwEW9jI6CAHUeoG85tjWP5WquqfavoMtMwiG4P926ZKKuQ==", + "dev": true, + "requires": { + "@jridgewell/resolve-uri": "^3.0.3", + "@jridgewell/sourcemap-codec": "^1.4.10" + } + } + } + }, + "@esbuild/android-arm": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.18.20.tgz", + "integrity": "sha512-fyi7TDI/ijKKNZTUJAQqiG5T7YjJXgnzkURqmGj13C6dCqckZBLdl4h7bkhHt/t0WP+zO9/zwroDvANaOqO5Sw==", + "dev": true, + "optional": true + }, + "@esbuild/android-arm64": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.18.20.tgz", + "integrity": "sha512-Nz4rJcchGDtENV0eMKUNa6L12zz2zBDXuhj/Vjh18zGqB44Bi7MBMSXjgunJgjRhCmKOjnPuZp4Mb6OKqtMHLQ==", + "dev": true, + "optional": true + }, + "@esbuild/android-x64": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.18.20.tgz", + "integrity": "sha512-8GDdlePJA8D6zlZYJV/jnrRAi6rOiNaCC/JclcXpB+KIuvfBN4owLtgzY2bsxnx666XjJx2kDPUmnTtR8qKQUg==", + "dev": true, + "optional": true + }, + "@esbuild/darwin-arm64": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.18.20.tgz", + "integrity": "sha512-bxRHW5kHU38zS2lPTPOyuyTm+S+eobPUnTNkdJEfAddYgEcll4xkT8DB9d2008DtTbl7uJag2HuE5NZAZgnNEA==", + "dev": true, + "optional": true + }, + "@esbuild/darwin-x64": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.18.20.tgz", + "integrity": "sha512-pc5gxlMDxzm513qPGbCbDukOdsGtKhfxD1zJKXjCCcU7ju50O7MeAZ8c4krSJcOIJGFR+qx21yMMVYwiQvyTyQ==", + "dev": true, + "optional": true + }, + "@esbuild/freebsd-arm64": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.18.20.tgz", + "integrity": "sha512-yqDQHy4QHevpMAaxhhIwYPMv1NECwOvIpGCZkECn8w2WFHXjEwrBn3CeNIYsibZ/iZEUemj++M26W3cNR5h+Tw==", + "dev": true, + "optional": true + }, + "@esbuild/freebsd-x64": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.18.20.tgz", + "integrity": "sha512-tgWRPPuQsd3RmBZwarGVHZQvtzfEBOreNuxEMKFcd5DaDn2PbBxfwLcj4+aenoh7ctXcbXmOQIn8HI6mCSw5MQ==", + "dev": true, + "optional": true + }, + "@esbuild/linux-arm": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.18.20.tgz", + "integrity": "sha512-/5bHkMWnq1EgKr1V+Ybz3s1hWXok7mDFUMQ4cG10AfW3wL02PSZi5kFpYKrptDsgb2WAJIvRcDm+qIvXf/apvg==", + "dev": true, + "optional": true + }, + "@esbuild/linux-arm64": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.18.20.tgz", + "integrity": "sha512-2YbscF+UL7SQAVIpnWvYwM+3LskyDmPhe31pE7/aoTMFKKzIc9lLbyGUpmmb8a8AixOL61sQ/mFh3jEjHYFvdA==", + "dev": true, + "optional": true + }, + "@esbuild/linux-ia32": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.18.20.tgz", + "integrity": "sha512-P4etWwq6IsReT0E1KHU40bOnzMHoH73aXp96Fs8TIT6z9Hu8G6+0SHSw9i2isWrD2nbx2qo5yUqACgdfVGx7TA==", + "dev": true, + "optional": true + }, + "@esbuild/linux-loong64": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.18.20.tgz", + "integrity": "sha512-nXW8nqBTrOpDLPgPY9uV+/1DjxoQ7DoB2N8eocyq8I9XuqJ7BiAMDMf9n1xZM9TgW0J8zrquIb/A7s3BJv7rjg==", + "dev": true, + "optional": true + }, + "@esbuild/linux-mips64el": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.18.20.tgz", + "integrity": "sha512-d5NeaXZcHp8PzYy5VnXV3VSd2D328Zb+9dEq5HE6bw6+N86JVPExrA6O68OPwobntbNJ0pzCpUFZTo3w0GyetQ==", + "dev": true, + "optional": true + }, + "@esbuild/linux-ppc64": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.18.20.tgz", + "integrity": "sha512-WHPyeScRNcmANnLQkq6AfyXRFr5D6N2sKgkFo2FqguP44Nw2eyDlbTdZwd9GYk98DZG9QItIiTlFLHJHjxP3FA==", + "dev": true, + "optional": true + }, + "@esbuild/linux-riscv64": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.18.20.tgz", + "integrity": "sha512-WSxo6h5ecI5XH34KC7w5veNnKkju3zBRLEQNY7mv5mtBmrP/MjNBCAlsM2u5hDBlS3NGcTQpoBvRzqBcRtpq1A==", + "dev": true, + "optional": true + }, + "@esbuild/linux-s390x": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.18.20.tgz", + "integrity": "sha512-+8231GMs3mAEth6Ja1iK0a1sQ3ohfcpzpRLH8uuc5/KVDFneH6jtAJLFGafpzpMRO6DzJ6AvXKze9LfFMrIHVQ==", + "dev": true, + "optional": true + }, + "@esbuild/linux-x64": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.18.20.tgz", + "integrity": "sha512-UYqiqemphJcNsFEskc73jQ7B9jgwjWrSayxawS6UVFZGWrAAtkzjxSqnoclCXxWtfwLdzU+vTpcNYhpn43uP1w==", + "dev": true, + "optional": true + }, + "@esbuild/netbsd-x64": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.18.20.tgz", + "integrity": "sha512-iO1c++VP6xUBUmltHZoMtCUdPlnPGdBom6IrO4gyKPFFVBKioIImVooR5I83nTew5UOYrk3gIJhbZh8X44y06A==", + "dev": true, + "optional": true + }, + "@esbuild/openbsd-x64": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.18.20.tgz", + "integrity": "sha512-e5e4YSsuQfX4cxcygw/UCPIEP6wbIL+se3sxPdCiMbFLBWu0eiZOJ7WoD+ptCLrmjZBK1Wk7I6D/I3NglUGOxg==", + "dev": true, + "optional": true + }, + "@esbuild/sunos-x64": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.18.20.tgz", + "integrity": "sha512-kDbFRFp0YpTQVVrqUd5FTYmWo45zGaXe0X8E1G/LKFC0v8x0vWrhOWSLITcCn63lmZIxfOMXtCfti/RxN/0wnQ==", + "dev": true, + "optional": true + }, + "@esbuild/win32-arm64": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.18.20.tgz", + "integrity": "sha512-ddYFR6ItYgoaq4v4JmQQaAI5s7npztfV4Ag6NrhiaW0RrnOXqBkgwZLofVTlq1daVTQNhtI5oieTvkRPfZrePg==", + "dev": true, + "optional": true + }, + "@esbuild/win32-ia32": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.18.20.tgz", + "integrity": "sha512-Wv7QBi3ID/rROT08SABTS7eV4hX26sVduqDOTe1MvGMjNd3EjOz4b7zeexIR62GTIEKrfJXKL9LFxTYgkyeu7g==", + "dev": true, + "optional": true + }, + "@esbuild/win32-x64": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.18.20.tgz", + "integrity": "sha512-kTdfRcSiDfQca/y9QIkng02avJ+NCaQvrMejlsB3RRv5sE9rRoeBPISaZpKxHELzRxZyLvNts1P27W3wV+8geQ==", + "dev": true, + "optional": true + }, + "@eslint-community/eslint-utils": { + "version": "4.4.0", + "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.4.0.tgz", + "integrity": "sha512-1/sA4dwrzBAyeUoQ6oxahHKmrZvsnLCg4RfxW3ZFGGmQkSNQPFNLV9CUEFQP1x9EYXHTo5p6xdhZM1Ne9p/AfA==", + "dev": true, + "requires": { + "eslint-visitor-keys": "^3.3.0" + } + }, + "@eslint-community/regexpp": { + "version": "4.8.0", + "resolved": "https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.8.0.tgz", + "integrity": "sha512-JylOEEzDiOryeUnFbQz+oViCXS0KsvR1mvHkoMiu5+UiBvy+RYX7tzlIIIEstF/gVa2tj9AQXk3dgnxv6KxhFg==", + "dev": true + }, + "@eslint/eslintrc": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-2.1.2.tgz", + "integrity": "sha512-+wvgpDsrB1YqAMdEUCcnTlpfVBH7Vqn6A/NT3D8WVXFIaKMlErPIZT3oCIAVCOtarRpMtelZLqJeU3t7WY6X6g==", + "dev": true, + "requires": { + "ajv": "^6.12.4", + "debug": "^4.3.2", + "espree": "^9.6.0", + "globals": "^13.19.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" + }, + "dependencies": { + "globals": { + "version": "13.21.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-13.21.0.tgz", + "integrity": "sha512-ybyme3s4yy/t/3s35bewwXKOf7cvzfreG2lH0lZl0JB7I4GxRP2ghxOK/Nb9EkRXdbBXZLfq/p/0W2JUONB/Gg==", + "dev": true, + "requires": { + "type-fest": "^0.20.2" + } + } + } + }, + "@eslint/js": { + "version": "8.52.0", + "resolved": "https://registry.npmjs.org/@eslint/js/-/js-8.52.0.tgz", + "integrity": "sha512-mjZVbpaeMZludF2fsWLD0Z9gCref1Tk4i9+wddjRvpUNqqcndPkBD09N/Mapey0b3jaXbLm2kICwFv2E64QinA==", + "dev": true + }, + "@fortawesome/fontawesome-common-types": { + "version": "6.4.2", + "resolved": "https://registry.npmjs.org/@fortawesome/fontawesome-common-types/-/fontawesome-common-types-6.4.2.tgz", + "integrity": "sha512-1DgP7f+XQIJbLFCTX1V2QnxVmpLdKdzzo2k8EmvDOePfchaIGQ9eCHj2up3/jNEbZuBqel5OxiaOJf37TWauRA==" + }, + "@fortawesome/fontawesome-svg-core": { + "version": "6.4.2", + "resolved": "https://registry.npmjs.org/@fortawesome/fontawesome-svg-core/-/fontawesome-svg-core-6.4.2.tgz", + "integrity": "sha512-gjYDSKv3TrM2sLTOKBc5rH9ckje8Wrwgx1CxAPbN5N3Fm4prfi7NsJVWd1jklp7i5uSCVwhZS5qlhMXqLrpAIg==", + "requires": { + "@fortawesome/fontawesome-common-types": "6.4.2" + } + }, + "@fortawesome/free-solid-svg-icons": { + "version": "6.4.2", + "resolved": "https://registry.npmjs.org/@fortawesome/free-solid-svg-icons/-/free-solid-svg-icons-6.4.2.tgz", + "integrity": "sha512-sYwXurXUEQS32fZz9hVCUUv/xu49PEJEyUOsA51l6PU/qVgfbTb2glsTEaJngVVT8VqBATRIdh7XVgV1JF1LkA==", + "requires": { + "@fortawesome/fontawesome-common-types": "6.4.2" + } + }, + "@fortawesome/vue-fontawesome": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/@fortawesome/vue-fontawesome/-/vue-fontawesome-3.0.3.tgz", + "integrity": "sha512-KCPHi9QemVXGMrfuwf3nNnNo129resAIQWut9QTAMXmXqL2ErABC6ohd2yY5Ipq0CLWNbKHk8TMdTXL/Zf3ZhA==", + "requires": {} + }, + "@humanwhocodes/config-array": { + "version": "0.11.13", + "resolved": "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.11.13.tgz", + "integrity": "sha512-JSBDMiDKSzQVngfRjOdFXgFfklaXI4K9nLF49Auh21lmBWRLIK3+xTErTWD4KU54pb6coM6ESE7Awz/FNU3zgQ==", + "dev": true, + "requires": { + "@humanwhocodes/object-schema": "^2.0.1", + "debug": "^4.1.1", + "minimatch": "^3.0.5" + } + }, + "@humanwhocodes/module-importer": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@humanwhocodes/module-importer/-/module-importer-1.0.1.tgz", + "integrity": "sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==", + "dev": true + }, + "@humanwhocodes/object-schema": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/@humanwhocodes/object-schema/-/object-schema-2.0.1.tgz", + "integrity": "sha512-dvuCeX5fC9dXgJn9t+X5atfmgQAzUOWqS1254Gh0m6i8wKd10ebXkfNKiRK+1GWi/yTvvLDHpoxLr0xxxeslWw==", + "dev": true + }, + "@isaacs/cliui": { + "version": "8.0.2", + "resolved": "https://registry.npmjs.org/@isaacs/cliui/-/cliui-8.0.2.tgz", + "integrity": "sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==", + "dev": true, + "requires": { + "string-width": "^5.1.2", + "string-width-cjs": "npm:string-width@^4.2.0", + "strip-ansi": "^7.0.1", + "strip-ansi-cjs": "npm:strip-ansi@^6.0.1", + "wrap-ansi": "^8.1.0", + "wrap-ansi-cjs": "npm:wrap-ansi@^7.0.0" + }, + "dependencies": { + "ansi-regex": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.0.1.tgz", + "integrity": "sha512-n5M855fKb2SsfMIiFFoVrABHJC8QtHwVx+mHWP3QcEqBHYienj5dHSgjbxtC0WEZXYt4wcD6zrQElDPhFuZgfA==", + "dev": true + }, + "ansi-styles": { + "version": "6.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.1.tgz", + "integrity": "sha512-bN798gFfQX+viw3R7yrGWRqnrN2oRkEkUjjl4JNn4E8GxxbjtG3FbrEIIY3l8/hrwUwIeCZvi4QuOTP4MErVug==", + "dev": true + }, + "strip-ansi": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.0.tgz", + "integrity": "sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ==", + "dev": true, + "requires": { + "ansi-regex": "^6.0.1" + } + }, + "wrap-ansi": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-8.1.0.tgz", + "integrity": "sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ==", + "dev": true, + "requires": { + "ansi-styles": "^6.1.0", + "string-width": "^5.0.1", + "strip-ansi": "^7.0.1" + } + } + } + }, + "@istanbuljs/schema": { + "version": "0.1.3", + "resolved": "https://registry.npmjs.org/@istanbuljs/schema/-/schema-0.1.3.tgz", + "integrity": "sha512-ZXRY4jNvVgSVQ8DL3LTcakaAtXwTVUxE81hslsyD2AtoXW/wVob10HkOJ1X/pAlcI7D+2YoZKg5do8G/w6RYgA==", + "dev": true + }, + "@jest/schemas": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/@jest/schemas/-/schemas-29.6.3.tgz", + "integrity": "sha512-mo5j5X+jIZmJQveBKeS/clAueipV7KgiX1vMgCxam1RNYiqE1w62n0/tJJnHtjW8ZHcQco5gY85jA3mi0L+nSA==", + "dev": true, + "requires": { + "@sinclair/typebox": "^0.27.8" + } + }, + "@jridgewell/gen-mapping": { + "version": "0.3.3", + "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.3.tgz", + "integrity": "sha512-HLhSWOLRi875zjjMG/r+Nv0oCW8umGb0BgEhyX3dDX3egwZtB8PqLnjz3yedt8R5StBrzcg4aBpnh8UA9D1BoQ==", + "dev": true, + "requires": { + "@jridgewell/set-array": "^1.0.1", + "@jridgewell/sourcemap-codec": "^1.4.10", + "@jridgewell/trace-mapping": "^0.3.9" + } + }, + "@jridgewell/resolve-uri": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.0.tgz", + "integrity": "sha512-F2msla3tad+Mfht5cJq7LSXcdudKTWCVYUgw6pLFOOHSTtZlj6SWNYAp+AhuqLmWdBO2X5hPrLcu8cVP8fy28w==", + "dev": true + }, + "@jridgewell/set-array": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@jridgewell/set-array/-/set-array-1.1.2.tgz", + "integrity": "sha512-xnkseuNADM0gt2bs+BvhO0p78Mk762YnZdsuzFV018NoG1Sj1SCQvpSqa7XUaTam5vAGasABV9qXASMKnFMwMw==", + "dev": true + }, + "@jridgewell/sourcemap-codec": { + "version": "1.4.14", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.4.14.tgz", + "integrity": "sha512-XPSJHWmi394fuUuzDnGz1wiKqWfo1yXecHQMRf2l6hztTO+nPru658AyDngaBe7isIxEkRsPR3FZh+s7iVa4Uw==", + "dev": true + }, + "@jridgewell/trace-mapping": { + "version": "0.3.17", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.17.tgz", + "integrity": "sha512-MCNzAp77qzKca9+W/+I0+sEpaUnZoeasnghNeVc41VZCEKaCH73Vq3BZZ/SzWIgrqE4H4ceI+p+b6C0mHf9T4g==", + "dev": true, + "requires": { + "@jridgewell/resolve-uri": "3.1.0", + "@jridgewell/sourcemap-codec": "1.4.14" + } + }, + "@nodelib/fs.scandir": { + "version": "2.1.5", + "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", + "integrity": "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==", + "dev": true, + "requires": { + "@nodelib/fs.stat": "2.0.5", + "run-parallel": "^1.1.9" + } + }, + "@nodelib/fs.stat": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz", + "integrity": "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==", + "dev": true + }, + "@nodelib/fs.walk": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/@nodelib/fs.walk/-/fs.walk-1.2.8.tgz", + "integrity": "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==", + "dev": true, + "requires": { + "@nodelib/fs.scandir": "2.1.5", + "fastq": "^1.6.0" + } + }, + "@one-ini/wasm": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/@one-ini/wasm/-/wasm-0.1.1.tgz", + "integrity": "sha512-XuySG1E38YScSJoMlqovLru4KTUNSjgVTIjyh7qMX6aNN5HY5Ct5LhRJdxO79JtTzKfzV/bnWpz+zquYrISsvw==", + "dev": true + }, + "@pinia/testing": { + "version": "0.1.3", + "resolved": "https://registry.npmjs.org/@pinia/testing/-/testing-0.1.3.tgz", + "integrity": "sha512-D2Ds2s69kKFaRf2KCcP1NhNZEg5+we59aRyQalwRm7ygWfLM25nDH66267U3hNvRUOTx8ofL24GzodZkOmB5xw==", + "dev": true, + "requires": { + "vue-demi": ">=0.14.5" + }, + "dependencies": { + "vue-demi": { + "version": "0.14.5", + "resolved": "https://registry.npmjs.org/vue-demi/-/vue-demi-0.14.5.tgz", + "integrity": "sha512-o9NUVpl/YlsGJ7t+xuqJKx8EBGf1quRhCiT6D/J0pfwmk9zUwYkC7yrF4SZCe6fETvSM3UNL2edcbYrSyc4QHA==", + "dev": true, + "requires": {} + } + } + }, + "@pkgjs/parseargs": { + "version": "0.11.0", + "resolved": "https://registry.npmjs.org/@pkgjs/parseargs/-/parseargs-0.11.0.tgz", + "integrity": "sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==", + "dev": true, + "optional": true + }, + "@sinclair/typebox": { + "version": "0.27.8", + "resolved": "https://registry.npmjs.org/@sinclair/typebox/-/typebox-0.27.8.tgz", + "integrity": "sha512-+Fj43pSMwJs4KRrH/938Uf+uAELIgVBmQzg/q1YG10djyfA3TnrU8N8XzqCh/okZdszqBQTZf96idMfE5lnwTA==", + "dev": true + }, + "@testing-library/dom": { + "version": "9.3.3", + "resolved": "https://registry.npmjs.org/@testing-library/dom/-/dom-9.3.3.tgz", + "integrity": "sha512-fB0R+fa3AUqbLHWyxXa2kGVtf1Fe1ZZFr0Zp6AIbIAzXb2mKbEXl+PCQNUOaq5lbTab5tfctfXRNsWXxa2f7Aw==", + "dev": true, + "requires": { + "@babel/code-frame": "^7.10.4", + "@babel/runtime": "^7.12.5", + "@types/aria-query": "^5.0.1", + "aria-query": "5.1.3", + "chalk": "^4.1.0", + "dom-accessibility-api": "^0.5.9", + "lz-string": "^1.5.0", + "pretty-format": "^27.0.2" + }, + "dependencies": { + "ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "requires": { + "color-convert": "^2.0.1" + } + }, + "chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, + "requires": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + } + }, + "color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "requires": { + "color-name": "~1.1.4" + } + }, + "color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true + }, + "has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true + }, + "supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "requires": { + "has-flag": "^4.0.0" + } + } + } + }, + "@testing-library/vue": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/@testing-library/vue/-/vue-8.0.0.tgz", + "integrity": "sha512-SP0qEY/SGpdT9+bPuHxYD3P/HCG0ZY8GlGJocVqdLn9EojbdQu69x06trJi1V7RW9tAZai/wwy+ZFcRsTp47kg==", + "dev": true, + "requires": { + "@babel/runtime": "^7.23.2", + "@testing-library/dom": "^9.3.3", + "@vue/test-utils": "^2.4.1" + } + }, + "@tootallnate/once": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/@tootallnate/once/-/once-2.0.0.tgz", + "integrity": "sha512-XCuKFP5PS55gnMVu3dty8KPatLqUoy/ZYzDzAGCQ8JNFCkLXzmI7vNHCR+XpbZaMWQK/vQubr7PkYq8g470J/A==", + "dev": true, + "optional": true, + "peer": true + }, + "@tsconfig/node10": { + "version": "1.0.9", + "resolved": "https://registry.npmjs.org/@tsconfig/node10/-/node10-1.0.9.tgz", + "integrity": "sha512-jNsYVVxU8v5g43Erja32laIDHXeoNvFEpX33OK4d6hljo3jDhCBDhx5dhCCTMWUojscpAagGiRkBKxpdl9fxqA==", + "dev": true + }, + "@tsconfig/node12": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/@tsconfig/node12/-/node12-1.0.11.tgz", + "integrity": "sha512-cqefuRsh12pWyGsIoBKJA9luFu3mRxCA+ORZvA4ktLSzIuCUtWVxGIuXigEwO5/ywWFMZ2QEGKWvkZG1zDMTag==", + "dev": true + }, + "@tsconfig/node14": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@tsconfig/node14/-/node14-1.0.3.tgz", + "integrity": "sha512-ysT8mhdixWK6Hw3i1V2AeRqZ5WfXg1G43mqoYlM2nc6388Fq5jcXyr5mRsqViLx/GJYdoL0bfXD8nmF+Zn/Iow==", + "dev": true + }, + "@tsconfig/node16": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@tsconfig/node16/-/node16-1.0.3.tgz", + "integrity": "sha512-yOlFc+7UtL/89t2ZhjPvvB/DeAr3r+Dq58IgzsFkOAvVC6NMJXmCGjbptdXdR9qsX7pKcTL+s87FtYREi2dEEQ==", + "dev": true + }, + "@tsconfig/node18": { + "version": "18.2.2", + "resolved": "https://registry.npmjs.org/@tsconfig/node18/-/node18-18.2.2.tgz", + "integrity": "sha512-d6McJeGsuoRlwWZmVIeE8CUA27lu6jLjvv1JzqmpsytOYYbVi1tHZEnwCNVOXnj4pyLvneZlFlpXUK+X9wBWyw==", + "dev": true + }, + "@types/aria-query": { + "version": "5.0.3", + "resolved": "https://registry.npmjs.org/@types/aria-query/-/aria-query-5.0.3.tgz", + "integrity": "sha512-0Z6Tr7wjKJIk4OUEjVUQMtyunLDy339vcMaj38Kpj6jM2OE1p3S4kXExKZ7a3uXQAPCoy3sbrP1wibDKaf39oA==", + "dev": true + }, + "@types/chai": { + "version": "4.3.5", + "resolved": "https://registry.npmjs.org/@types/chai/-/chai-4.3.5.tgz", + "integrity": "sha512-mEo1sAde+UCE6b2hxn332f1g1E8WfYRu6p5SvTKr2ZKC1f7gFJXk4h5PyGP9Dt6gCaG8y8XhwnXWC6Iy2cmBng==", + "dev": true + }, + "@types/chai-subset": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/@types/chai-subset/-/chai-subset-1.3.3.tgz", + "integrity": "sha512-frBecisrNGz+F4T6bcc+NLeolfiojh5FxW2klu669+8BARtyQv2C/GkNW6FUodVe4BroGMP/wER/YDGc7rEllw==", + "dev": true, + "requires": { + "@types/chai": "*" + } + }, + "@types/istanbul-lib-coverage": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/@types/istanbul-lib-coverage/-/istanbul-lib-coverage-2.0.4.tgz", + "integrity": "sha512-z/QT1XN4K4KYuslS23k62yDIDLwLFkzxOuMplDtObz0+y7VqJCaO2o+SPwHCvLFZh7xazvvoor2tA/hPz9ee7g==", + "dev": true + }, + "@types/json-schema": { + "version": "7.0.14", + "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.14.tgz", + "integrity": "sha512-U3PUjAudAdJBeC2pgN8uTIKgxrb4nlDF3SF0++EldXQvQBGkpFZMSnwQiIoDU77tv45VgNkl/L4ouD+rEomujw==", + "dev": true + }, + "@types/node": { + "version": "20.8.10", + "resolved": "https://registry.npmjs.org/@types/node/-/node-20.8.10.tgz", + "integrity": "sha512-TlgT8JntpcbmKUFzjhsyhGfP2fsiz1Mv56im6enJ905xG1DAYesxJaeSbGqQmAw8OWPdhyJGhGSQGKRNJ45u9w==", + "dev": true, + "requires": { + "undici-types": "~5.26.4" + } + }, + "@types/semver": { + "version": "7.5.4", + "resolved": "https://registry.npmjs.org/@types/semver/-/semver-7.5.4.tgz", + "integrity": "sha512-MMzuxN3GdFwskAnb6fz0orFvhfqi752yjaXylr0Rp4oDg5H0Zn1IuyRhDVvYOwAXoJirx2xuS16I3WjxnAIHiQ==", + "dev": true + }, + "@typescript-eslint/eslint-plugin": { + "version": "6.9.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-6.9.0.tgz", + "integrity": "sha512-lgX7F0azQwRPB7t7WAyeHWVfW1YJ9NIgd9mvGhfQpRY56X6AVf8mwM8Wol+0z4liE7XX3QOt8MN1rUKCfSjRIA==", + "dev": true, + "requires": { + "@eslint-community/regexpp": "^4.5.1", + "@typescript-eslint/scope-manager": "6.9.0", + "@typescript-eslint/type-utils": "6.9.0", + "@typescript-eslint/utils": "6.9.0", + "@typescript-eslint/visitor-keys": "6.9.0", + "debug": "^4.3.4", + "graphemer": "^1.4.0", + "ignore": "^5.2.4", + "natural-compare": "^1.4.0", + "semver": "^7.5.4", + "ts-api-utils": "^1.0.1" + }, + "dependencies": { + "semver": { + "version": "7.5.4", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.5.4.tgz", + "integrity": "sha512-1bCSESV6Pv+i21Hvpxp3Dx+pSD8lIPt8uVjRrxAUt/nbswYc+tK6Y2btiULjd4+fnq15PX+nqQDC7Oft7WkwcA==", + "dev": true, + "requires": { + "lru-cache": "^6.0.0" + } + } + } + }, + "@typescript-eslint/parser": { + "version": "6.9.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-6.9.0.tgz", + "integrity": "sha512-GZmjMh4AJ/5gaH4XF2eXA8tMnHWP+Pm1mjQR2QN4Iz+j/zO04b9TOvJYOX2sCNIQHtRStKTxRY1FX7LhpJT4Gw==", + "dev": true, + "requires": { + "@typescript-eslint/scope-manager": "6.9.0", + "@typescript-eslint/types": "6.9.0", + "@typescript-eslint/typescript-estree": "6.9.0", + "@typescript-eslint/visitor-keys": "6.9.0", + "debug": "^4.3.4" + } + }, + "@typescript-eslint/scope-manager": { + "version": "6.9.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-6.9.0.tgz", + "integrity": "sha512-1R8A9Mc39n4pCCz9o79qRO31HGNDvC7UhPhv26TovDsWPBDx+Sg3rOZdCELIA3ZmNoWAuxaMOT7aWtGRSYkQxw==", + "dev": true, + "requires": { + "@typescript-eslint/types": "6.9.0", + "@typescript-eslint/visitor-keys": "6.9.0" + } + }, + "@typescript-eslint/type-utils": { + "version": "6.9.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-6.9.0.tgz", + "integrity": "sha512-XXeahmfbpuhVbhSOROIzJ+b13krFmgtc4GlEuu1WBT+RpyGPIA4Y/eGnXzjbDj5gZLzpAXO/sj+IF/x2GtTMjQ==", + "dev": true, + "requires": { + "@typescript-eslint/typescript-estree": "6.9.0", + "@typescript-eslint/utils": "6.9.0", + "debug": "^4.3.4", + "ts-api-utils": "^1.0.1" + } + }, + "@typescript-eslint/types": { + "version": "6.9.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-6.9.0.tgz", + "integrity": "sha512-+KB0lbkpxBkBSiVCuQvduqMJy+I1FyDbdwSpM3IoBS7APl4Bu15lStPjgBIdykdRqQNYqYNMa8Kuidax6phaEw==", + "dev": true + }, + "@typescript-eslint/typescript-estree": { + "version": "6.9.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-6.9.0.tgz", + "integrity": "sha512-NJM2BnJFZBEAbCfBP00zONKXvMqihZCrmwCaik0UhLr0vAgb6oguXxLX1k00oQyD+vZZ+CJn3kocvv2yxm4awQ==", + "dev": true, + "requires": { + "@typescript-eslint/types": "6.9.0", + "@typescript-eslint/visitor-keys": "6.9.0", + "debug": "^4.3.4", + "globby": "^11.1.0", + "is-glob": "^4.0.3", + "semver": "^7.5.4", + "ts-api-utils": "^1.0.1" + }, + "dependencies": { + "semver": { + "version": "7.5.4", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.5.4.tgz", + "integrity": "sha512-1bCSESV6Pv+i21Hvpxp3Dx+pSD8lIPt8uVjRrxAUt/nbswYc+tK6Y2btiULjd4+fnq15PX+nqQDC7Oft7WkwcA==", + "dev": true, + "requires": { + "lru-cache": "^6.0.0" + } + } + } + }, + "@typescript-eslint/utils": { + "version": "6.9.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-6.9.0.tgz", + "integrity": "sha512-5Wf+Jsqya7WcCO8me504FBigeQKVLAMPmUzYgDbWchINNh1KJbxCgVya3EQ2MjvJMVeXl3pofRmprqX6mfQkjQ==", + "dev": true, + "requires": { + "@eslint-community/eslint-utils": "^4.4.0", + "@types/json-schema": "^7.0.12", + "@types/semver": "^7.5.0", + "@typescript-eslint/scope-manager": "6.9.0", + "@typescript-eslint/types": "6.9.0", + "@typescript-eslint/typescript-estree": "6.9.0", + "semver": "^7.5.4" + }, + "dependencies": { + "semver": { + "version": "7.5.4", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.5.4.tgz", + "integrity": "sha512-1bCSESV6Pv+i21Hvpxp3Dx+pSD8lIPt8uVjRrxAUt/nbswYc+tK6Y2btiULjd4+fnq15PX+nqQDC7Oft7WkwcA==", + "dev": true, + "requires": { + "lru-cache": "^6.0.0" + } + } + } + }, + "@typescript-eslint/visitor-keys": { + "version": "6.9.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-6.9.0.tgz", + "integrity": "sha512-dGtAfqjV6RFOtIP8I0B4ZTBRrlTT8NHHlZZSchQx3qReaoDeXhYM++M4So2AgFK9ZB0emRPA6JI1HkafzA2Ibg==", + "dev": true, + "requires": { + "@typescript-eslint/types": "6.9.0", + "eslint-visitor-keys": "^3.4.1" + } + }, + "@ungap/structured-clone": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/@ungap/structured-clone/-/structured-clone-1.2.0.tgz", + "integrity": "sha512-zuVdFrMJiuCDQUMCzQaD6KL28MjnqqN8XnAqiEq9PNm/hCPTSGfrXCOfwj1ow4LFb/tNymJPwsNbVePc1xFqrQ==", + "dev": true + }, + "@vitejs/plugin-vue": { + "version": "4.4.0", + "resolved": "https://registry.npmjs.org/@vitejs/plugin-vue/-/plugin-vue-4.4.0.tgz", + "integrity": "sha512-xdguqb+VUwiRpSg+nsc2HtbAUSGak25DXYvpQQi4RVU1Xq1uworyoH/md9Rfd8zMmPR/pSghr309QNcftUVseg==", + "dev": true, + "requires": {} + }, + "@vitejs/plugin-vue-jsx": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/@vitejs/plugin-vue-jsx/-/plugin-vue-jsx-3.0.2.tgz", + "integrity": "sha512-obF26P2Z4Ogy3cPp07B4VaW6rpiu0ue4OT2Y15UxT5BZZ76haUY9guOsZV3uWh/I6xc+VeiW+ZVabRE82FyzWw==", + "dev": true, + "requires": { + "@babel/core": "^7.22.10", + "@babel/plugin-transform-typescript": "^7.22.10", + "@vue/babel-plugin-jsx": "^1.1.5" + } + }, + "@vitest/coverage-c8": { + "version": "0.33.0", + "resolved": "https://registry.npmjs.org/@vitest/coverage-c8/-/coverage-c8-0.33.0.tgz", + "integrity": "sha512-DaF1zJz4dcOZS4k/neiQJokmOWqsGXwhthfmUdPGorXIQHjdPvV6JQSYhQDI41MyI8c+IieQUdIDs5XAMHtDDw==", + "dev": true, + "requires": { + "@ampproject/remapping": "^2.2.1", + "c8": "^7.14.0", + "magic-string": "^0.30.1", + "picocolors": "^1.0.0", + "std-env": "^3.3.3" + } + }, + "@vitest/coverage-istanbul": { + "version": "0.34.6", + "resolved": "https://registry.npmjs.org/@vitest/coverage-istanbul/-/coverage-istanbul-0.34.6.tgz", + "integrity": "sha512-5KaBNZPDSk2ybavC3rZ1pWGniw7sJ5usuwVGRUYzJwiBfWvnLpuUer7bjw7qUCRGdKJXrBgb/Dsgif9rkwMX/A==", + "dev": true, + "requires": { + "istanbul-lib-coverage": "^3.2.0", + "istanbul-lib-instrument": "^6.0.0", + "istanbul-lib-report": "^3.0.1", + "istanbul-lib-source-maps": "^4.0.1", + "istanbul-reports": "^3.1.5", + "picocolors": "^1.0.0", + "test-exclude": "^6.0.0" + } + }, + "@vitest/expect": { + "version": "0.34.6", + "resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-0.34.6.tgz", + "integrity": "sha512-QUzKpUQRc1qC7qdGo7rMK3AkETI7w18gTCUrsNnyjjJKYiuUB9+TQK3QnR1unhCnWRC0AbKv2omLGQDF/mIjOw==", + "dev": true, + "requires": { + "@vitest/spy": "0.34.6", + "@vitest/utils": "0.34.6", + "chai": "^4.3.10" + } + }, + "@vitest/runner": { + "version": "0.34.6", + "resolved": "https://registry.npmjs.org/@vitest/runner/-/runner-0.34.6.tgz", + "integrity": "sha512-1CUQgtJSLF47NnhN+F9X2ycxUP0kLHQ/JWvNHbeBfwW8CzEGgeskzNnHDyv1ieKTltuR6sdIHV+nmR6kPxQqzQ==", + "dev": true, + "requires": { + "@vitest/utils": "0.34.6", + "p-limit": "^4.0.0", + "pathe": "^1.1.1" + }, + "dependencies": { + "p-limit": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-4.0.0.tgz", + "integrity": "sha512-5b0R4txpzjPWVw/cXXUResoD4hb6U/x9BH08L7nw+GN1sezDzPdxeRvpc9c433fZhBan/wusjbCsqwqm4EIBIQ==", + "dev": true, + "requires": { + "yocto-queue": "^1.0.0" + } + }, + "yocto-queue": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-1.0.0.tgz", + "integrity": "sha512-9bnSc/HEW2uRy67wc+T8UwauLuPJVn28jb+GtJY16iiKWyvmYJRXVT4UamsAEGQfPohgr2q4Tq0sQbQlxTfi1g==", + "dev": true + } + } + }, + "@vitest/snapshot": { + "version": "0.34.6", + "resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-0.34.6.tgz", + "integrity": "sha512-B3OZqYn6k4VaN011D+ve+AA4whM4QkcwcrwaKwAbyyvS/NB1hCWjFIBQxAQQSQir9/RtyAAGuq+4RJmbn2dH4w==", + "dev": true, + "requires": { + "magic-string": "^0.30.1", + "pathe": "^1.1.1", + "pretty-format": "^29.5.0" + }, + "dependencies": { + "ansi-styles": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz", + "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==", + "dev": true + }, + "pretty-format": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-29.7.0.tgz", + "integrity": "sha512-Pdlw/oPxN+aXdmM9R00JVC9WVFoCLTKJvDVLgmJ+qAffBMxsV85l/Lu7sNx4zSzPyoL2euImuEwHhOXdEgNFZQ==", + "dev": true, + "requires": { + "@jest/schemas": "^29.6.3", + "ansi-styles": "^5.0.0", + "react-is": "^18.0.0" + } + }, + "react-is": { + "version": "18.2.0", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-18.2.0.tgz", + "integrity": "sha512-xWGDIW6x921xtzPkhiULtthJHoJvBbF3q26fzloPCK0hsvxtPVelvftw3zjbHWSkR2km9Z+4uxbDDK/6Zw9B8w==", + "dev": true + } + } + }, + "@vitest/spy": { + "version": "0.34.6", + "resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-0.34.6.tgz", + "integrity": "sha512-xaCvneSaeBw/cz8ySmF7ZwGvL0lBjfvqc1LpQ/vcdHEvpLn3Ff1vAvjw+CoGn0802l++5L/pxb7whwcWAw+DUQ==", + "dev": true, + "requires": { + "tinyspy": "^2.1.1" + } + }, + "@vitest/utils": { + "version": "0.34.6", + "resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-0.34.6.tgz", + "integrity": "sha512-IG5aDD8S6zlvloDsnzHw0Ut5xczlF+kv2BOTo+iXfPr54Yhi5qbVOgGB1hZaVq4iJ4C/MZ2J0y15IlsV/ZcI0A==", + "dev": true, + "requires": { + "diff-sequences": "^29.4.3", + "loupe": "^2.3.6", + "pretty-format": "^29.5.0" + }, + "dependencies": { + "ansi-styles": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz", + "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==", + "dev": true + }, + "pretty-format": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-29.7.0.tgz", + "integrity": "sha512-Pdlw/oPxN+aXdmM9R00JVC9WVFoCLTKJvDVLgmJ+qAffBMxsV85l/Lu7sNx4zSzPyoL2euImuEwHhOXdEgNFZQ==", + "dev": true, + "requires": { + "@jest/schemas": "^29.6.3", + "ansi-styles": "^5.0.0", + "react-is": "^18.0.0" + } + }, + "react-is": { + "version": "18.2.0", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-18.2.0.tgz", + "integrity": "sha512-xWGDIW6x921xtzPkhiULtthJHoJvBbF3q26fzloPCK0hsvxtPVelvftw3zjbHWSkR2km9Z+4uxbDDK/6Zw9B8w==", + "dev": true + } + } + }, + "@volar/language-core": { + "version": "1.10.5", + "resolved": "https://registry.npmjs.org/@volar/language-core/-/language-core-1.10.5.tgz", + "integrity": "sha512-xD71j4Ee0Ycq8WsiAE6H/aCThGdTobiZZeD+jFD+bvmbopa1Az296pqJysr3Ck8c7n5+GGF+xlKCS3WxRFYgSQ==", + "dev": true, + "requires": { + "@volar/source-map": "1.10.5" + } + }, + "@volar/source-map": { + "version": "1.10.5", + "resolved": "https://registry.npmjs.org/@volar/source-map/-/source-map-1.10.5.tgz", + "integrity": "sha512-s4kgo66SA1kMzYvF9HFE6Vc1rxtXLUmcLrT2WKnchPDvLne+97Kw+xoR2NxJFmsvHoL18vmu/YGXYcN+Q5re1g==", + "dev": true, + "requires": { + "muggle-string": "^0.3.1" + } + }, + "@volar/typescript": { + "version": "1.10.5", + "resolved": "https://registry.npmjs.org/@volar/typescript/-/typescript-1.10.5.tgz", + "integrity": "sha512-kfDehpeLJku9i1BgsFOYIczPmFFH4herl+GZrLGdvX5urTqeCKsKYlF36iNmFaADzjMb9WlENcUZzPjK8MxNrQ==", + "dev": true, + "requires": { + "@volar/language-core": "1.10.5" + } + }, + "@vue/babel-helper-vue-transform-on": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/@vue/babel-helper-vue-transform-on/-/babel-helper-vue-transform-on-1.1.5.tgz", + "integrity": "sha512-SgUymFpMoAyWeYWLAY+MkCK3QEROsiUnfaw5zxOVD/M64KQs8D/4oK6Q5omVA2hnvEOE0SCkH2TZxs/jnnUj7w==", + "dev": true + }, + "@vue/babel-plugin-jsx": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/@vue/babel-plugin-jsx/-/babel-plugin-jsx-1.1.5.tgz", + "integrity": "sha512-nKs1/Bg9U1n3qSWnsHhCVQtAzI6aQXqua8j/bZrau8ywT1ilXQbK4FwEJGmU8fV7tcpuFvWmmN7TMmV1OBma1g==", + "dev": true, + "requires": { + "@babel/helper-module-imports": "^7.22.5", + "@babel/plugin-syntax-jsx": "^7.22.5", + "@babel/template": "^7.22.5", + "@babel/traverse": "^7.22.5", + "@babel/types": "^7.22.5", + "@vue/babel-helper-vue-transform-on": "^1.1.5", + "camelcase": "^6.3.0", + "html-tags": "^3.3.1", + "svg-tags": "^1.0.0" + } + }, + "@vue/compiler-core": { + "version": "3.3.7", + "resolved": "https://registry.npmjs.org/@vue/compiler-core/-/compiler-core-3.3.7.tgz", + "integrity": "sha512-pACdY6YnTNVLXsB86YD8OF9ihwpolzhhtdLVHhBL6do/ykr6kKXNYABRtNMGrsQXpEXXyAdwvWWkuTbs4MFtPQ==", + "requires": { + "@babel/parser": "^7.23.0", + "@vue/shared": "3.3.7", + "estree-walker": "^2.0.2", + "source-map-js": "^1.0.2" + } + }, + "@vue/compiler-dom": { + "version": "3.3.7", + "resolved": "https://registry.npmjs.org/@vue/compiler-dom/-/compiler-dom-3.3.7.tgz", + "integrity": "sha512-0LwkyJjnUPssXv/d1vNJ0PKfBlDoQs7n81CbO6Q0zdL7H1EzqYRrTVXDqdBVqro0aJjo/FOa1qBAPVI4PGSHBw==", + "requires": { + "@vue/compiler-core": "3.3.7", + "@vue/shared": "3.3.7" + } + }, + "@vue/compiler-sfc": { + "version": "3.3.7", + "resolved": "https://registry.npmjs.org/@vue/compiler-sfc/-/compiler-sfc-3.3.7.tgz", + "integrity": "sha512-7pfldWy/J75U/ZyYIXRVqvLRw3vmfxDo2YLMwVtWVNew8Sm8d6wodM+OYFq4ll/UxfqVr0XKiVwti32PCrruAw==", + "requires": { + "@babel/parser": "^7.23.0", + "@vue/compiler-core": "3.3.7", + "@vue/compiler-dom": "3.3.7", + "@vue/compiler-ssr": "3.3.7", + "@vue/reactivity-transform": "3.3.7", + "@vue/shared": "3.3.7", + "estree-walker": "^2.0.2", + "magic-string": "^0.30.5", + "postcss": "^8.4.31", + "source-map-js": "^1.0.2" + } + }, + "@vue/compiler-ssr": { + "version": "3.3.7", + "resolved": "https://registry.npmjs.org/@vue/compiler-ssr/-/compiler-ssr-3.3.7.tgz", + "integrity": "sha512-TxOfNVVeH3zgBc82kcUv+emNHo+vKnlRrkv8YvQU5+Y5LJGJwSNzcmLUoxD/dNzv0bhQ/F0s+InlgV0NrApJZg==", + "requires": { + "@vue/compiler-dom": "3.3.7", + "@vue/shared": "3.3.7" + } + }, + "@vue/devtools-api": { + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/@vue/devtools-api/-/devtools-api-6.5.0.tgz", + "integrity": "sha512-o9KfBeaBmCKl10usN4crU53fYtC1r7jJwdGKjPT24t348rHxgfpZ0xL3Xm/gLUYnc0oTp8LAmrxOeLyu6tbk2Q==" + }, + "@vue/eslint-config-typescript": { + "version": "12.0.0", + "resolved": "https://registry.npmjs.org/@vue/eslint-config-typescript/-/eslint-config-typescript-12.0.0.tgz", + "integrity": "sha512-StxLFet2Qe97T8+7L8pGlhYBBr8Eg05LPuTDVopQV6il+SK6qqom59BA/rcFipUef2jD8P2X44Vd8tMFytfvlg==", + "dev": true, + "requires": { + "@typescript-eslint/eslint-plugin": "^6.7.0", + "@typescript-eslint/parser": "^6.7.0", + "vue-eslint-parser": "^9.3.1" + } + }, + "@vue/language-core": { + "version": "1.8.22", + "resolved": "https://registry.npmjs.org/@vue/language-core/-/language-core-1.8.22.tgz", + "integrity": "sha512-bsMoJzCrXZqGsxawtUea1cLjUT9dZnDsy5TuZ+l1fxRMzUGQUG9+Ypq4w//CqpWmrx7nIAJpw2JVF/t258miRw==", + "dev": true, + "requires": { + "@volar/language-core": "~1.10.5", + "@volar/source-map": "~1.10.5", + "@vue/compiler-dom": "^3.3.0", + "@vue/shared": "^3.3.0", + "computeds": "^0.0.1", + "minimatch": "^9.0.3", + "muggle-string": "^0.3.1", + "vue-template-compiler": "^2.7.14" + }, + "dependencies": { + "brace-expansion": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", + "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", + "dev": true, + "requires": { + "balanced-match": "^1.0.0" + } + }, + "minimatch": { + "version": "9.0.3", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.3.tgz", + "integrity": "sha512-RHiac9mvaRw0x3AYRgDC1CxAP7HTcNrrECeA8YYJeWnpo+2Q5CegtZjaotWTWxDG3UeGA1coE05iH1mPjT/2mg==", + "dev": true, + "requires": { + "brace-expansion": "^2.0.1" + } + } + } + }, + "@vue/reactivity": { + "version": "3.3.7", + "resolved": "https://registry.npmjs.org/@vue/reactivity/-/reactivity-3.3.7.tgz", + "integrity": "sha512-cZNVjWiw00708WqT0zRpyAgduG79dScKEPYJXq2xj/aMtk3SKvL3FBt2QKUlh6EHBJ1m8RhBY+ikBUzwc7/khg==", + "requires": { + "@vue/shared": "3.3.7" + } + }, + "@vue/reactivity-transform": { + "version": "3.3.7", + "resolved": "https://registry.npmjs.org/@vue/reactivity-transform/-/reactivity-transform-3.3.7.tgz", + "integrity": "sha512-APhRmLVbgE1VPGtoLQoWBJEaQk4V8JUsqrQihImVqKT+8U6Qi3t5ATcg4Y9wGAPb3kIhetpufyZ1RhwbZCIdDA==", + "requires": { + "@babel/parser": "^7.23.0", + "@vue/compiler-core": "3.3.7", + "@vue/shared": "3.3.7", + "estree-walker": "^2.0.2", + "magic-string": "^0.30.5" + } + }, + "@vue/runtime-core": { + "version": "3.3.7", + "resolved": "https://registry.npmjs.org/@vue/runtime-core/-/runtime-core-3.3.7.tgz", + "integrity": "sha512-LHq9du3ubLZFdK/BP0Ysy3zhHqRfBn80Uc+T5Hz3maFJBGhci1MafccnL3rpd5/3wVfRHAe6c+PnlO2PAavPTQ==", + "requires": { + "@vue/reactivity": "3.3.7", + "@vue/shared": "3.3.7" + } + }, + "@vue/runtime-dom": { + "version": "3.3.7", + "resolved": "https://registry.npmjs.org/@vue/runtime-dom/-/runtime-dom-3.3.7.tgz", + "integrity": "sha512-PFQU1oeJxikdDmrfoNQay5nD4tcPNYixUBruZzVX/l0eyZvFKElZUjW4KctCcs52nnpMGO6UDK+jF5oV4GT5Lw==", + "requires": { + "@vue/runtime-core": "3.3.7", + "@vue/shared": "3.3.7", + "csstype": "^3.1.2" + } + }, + "@vue/server-renderer": { + "version": "3.3.7", + "resolved": "https://registry.npmjs.org/@vue/server-renderer/-/server-renderer-3.3.7.tgz", + "integrity": "sha512-UlpKDInd1hIZiNuVVVvLgxpfnSouxKQOSE2bOfQpBuGwxRV/JqqTCyyjXUWiwtVMyeRaZhOYYqntxElk8FhBhw==", + "requires": { + "@vue/compiler-ssr": "3.3.7", + "@vue/shared": "3.3.7" + } + }, + "@vue/shared": { + "version": "3.3.7", + "resolved": "https://registry.npmjs.org/@vue/shared/-/shared-3.3.7.tgz", + "integrity": "sha512-N/tbkINRUDExgcPTBvxNkvHGu504k8lzlNQRITVnm6YjOjwa4r0nnbd4Jb01sNpur5hAllyRJzSK5PvB9PPwRg==" + }, + "@vue/test-utils": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/@vue/test-utils/-/test-utils-2.4.1.tgz", + "integrity": "sha512-VO8nragneNzUZUah6kOjiFmD/gwRjUauG9DROh6oaOeFwX1cZRUNHhdeogE8635cISigXFTtGLUQWx5KCb0xeg==", + "dev": true, + "requires": { + "js-beautify": "1.14.9", + "vue-component-type-helpers": "1.8.4" + } + }, + "@vue/tsconfig": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/@vue/tsconfig/-/tsconfig-0.4.0.tgz", + "integrity": "sha512-CPuIReonid9+zOG/CGTT05FXrPYATEqoDGNrEaqS4hwcw5BUNM2FguC0mOwJD4Jr16UpRVl9N0pY3P+srIbqmg==", + "dev": true + }, + "abab": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/abab/-/abab-2.0.6.tgz", + "integrity": "sha512-j2afSsaIENvHZN2B8GOpF566vZ5WVk5opAiMTvWgaQT8DkbOqsTfvNAvHoRGU2zzP8cPoqys+xHTRDWW8L+/BA==", + "dev": true, + "optional": true, + "peer": true + }, + "abbrev": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/abbrev/-/abbrev-1.1.1.tgz", + "integrity": "sha512-nne9/IiQ/hzIhY6pdDnbBtz7DjPTKrY00P/zvPSm5pOFkl6xuGrGnXn/VtTNNfNtAfZ9/1RtehkszU9qcTii0Q==", + "dev": true + }, + "acorn": { + "version": "8.10.0", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.10.0.tgz", + "integrity": "sha512-F0SAmZ8iUtS//m8DmCTA0jlh6TDKkHQyK6xc6V4KDTyZKA9dnvX9/3sRTVQrWm79glUAZbnmmNcdYwUIHWVybw==", + "dev": true + }, + "acorn-jsx": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.3.2.tgz", + "integrity": "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==", + "dev": true, + "requires": {} + }, + "acorn-walk": { + "version": "8.2.0", + "resolved": "https://registry.npmjs.org/acorn-walk/-/acorn-walk-8.2.0.tgz", + "integrity": "sha512-k+iyHEuPgSw6SbuDpGQM+06HQUa04DZ3o+F6CSzXMvvI5KMvnaEqXe+YVe555R9nn6GPt404fos4wcgpw12SDA==", + "dev": true + }, + "agent-base": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-6.0.2.tgz", + "integrity": "sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ==", + "dev": true, + "optional": true, + "peer": true, + "requires": { + "debug": "4" + } + }, + "ajv": { + "version": "6.12.6", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", + "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", + "dev": true, + "requires": { + "fast-deep-equal": "^3.1.1", + "fast-json-stable-stringify": "^2.0.0", + "json-schema-traverse": "^0.4.1", + "uri-js": "^4.2.2" + } + }, + "ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "dev": true + }, + "ansi-styles": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz", + "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==", + "dev": true, + "requires": { + "color-convert": "^1.9.0" + } + }, + "anymatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.2.tgz", + "integrity": "sha512-P43ePfOAIupkguHUycrc4qJ9kz8ZiuOUijaETwX7THt0Y/GNK7v0aa8rY816xWjZ7rJdA5XdMcpVFTKMq+RvWg==", + "dev": true, + "requires": { + "normalize-path": "^3.0.0", + "picomatch": "^2.0.4" + } + }, + "arg": { + "version": "4.1.3", + "resolved": "https://registry.npmjs.org/arg/-/arg-4.1.3.tgz", + "integrity": "sha512-58S9QDqG0Xx27YwPSt9fJxivjYl432YCwfDMfZ+71RAqUrZef7LrKQZ3LHLOwCS4FLNBplP533Zx895SeOCHvA==", + "dev": true + }, + "argparse": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", + "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", + "dev": true + }, + "aria-query": { + "version": "5.1.3", + "resolved": "https://registry.npmjs.org/aria-query/-/aria-query-5.1.3.tgz", + "integrity": "sha512-R5iJ5lkuHybztUfuOAznmboyjWq8O6sqNqtK7CLOqdydi54VNbORp49mb14KbWgG1QD3JFO9hJdZ+y4KutfdOQ==", + "dev": true, + "requires": { + "deep-equal": "^2.0.5" + } + }, + "array-buffer-byte-length": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/array-buffer-byte-length/-/array-buffer-byte-length-1.0.0.tgz", + "integrity": "sha512-LPuwb2P+NrQw3XhxGc36+XSvuBPopovXYTR9Ew++Du9Yb/bx5AzBfrIsBoj0EZUifjQU+sHL21sseZ3jerWO/A==", + "dev": true, + "requires": { + "call-bind": "^1.0.2", + "is-array-buffer": "^3.0.1" + } + }, + "array-union": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/array-union/-/array-union-2.1.0.tgz", + "integrity": "sha512-HGyxoOTYUyCM6stUe6EJgnd4EoewAI7zMdfqO+kGjnlZmBDz/cR5pf8r/cR4Wq60sL/p0IkcjUEEPwS3GFrIyw==", + "dev": true + }, + "assertion-error": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/assertion-error/-/assertion-error-1.1.0.tgz", + "integrity": "sha512-jgsaNduz+ndvGyFt3uSuWqvy4lCnIJiovtouQN5JZHOKCS2QuhEdbcQHFhVksz2N2U9hXJo8odG7ETyWlEeuDw==", + "dev": true + }, + "asynckit": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", + "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==" + }, + "available-typed-arrays": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/available-typed-arrays/-/available-typed-arrays-1.0.5.tgz", + "integrity": "sha512-DMD0KiN46eipeziST1LPP/STfDU0sufISXmjSgvVsoU2tqxctQeASejWcfNtxYKqETM1UxQ8sp2OrSBWpHY6sw==", + "dev": true + }, + "axios": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/axios/-/axios-1.6.0.tgz", + "integrity": "sha512-EZ1DYihju9pwVB+jg67ogm+Tmqc6JmhamRN6I4Zt8DfZu5lbcQGw3ozH9lFejSJgs/ibaef3A9PMXPLeefFGJg==", + "requires": { + "follow-redirects": "^1.15.0", + "form-data": "^4.0.0", + "proxy-from-env": "^1.1.0" + } + }, + "balanced-match": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", + "dev": true + }, + "binary-extensions": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.2.0.tgz", + "integrity": "sha512-jDctJ/IVQbZoJykoeHbhXpOlNBqGNcwXJKJog42E5HDPUwQTSdjCHdihjj0DlnheQ7blbT6dHOafNAiS8ooQKA==", + "dev": true + }, + "boolbase": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/boolbase/-/boolbase-1.0.0.tgz", + "integrity": "sha512-JZOSA7Mo9sNGB8+UjSgzdLtokWAky1zbztM3WRLCbZ70/3cTANmQmOdR7y2g+J0e2WXywy1yS468tY+IruqEww==", + "dev": true + }, + "brace-expansion": { + "version": "1.1.11", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", + "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "dev": true, + "requires": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "braces": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.2.tgz", + "integrity": "sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A==", + "dev": true, + "requires": { + "fill-range": "^7.0.1" + } + }, + "browserslist": { + "version": "4.21.10", + "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.21.10.tgz", + "integrity": "sha512-bipEBdZfVH5/pwrvqc+Ub0kUPVfGUhlKxbvfD+z1BDnPEO/X98ruXGA1WP5ASpAFKan7Qr6j736IacbZQuAlKQ==", + "dev": true, + "requires": { + "caniuse-lite": "^1.0.30001517", + "electron-to-chromium": "^1.4.477", + "node-releases": "^2.0.13", + "update-browserslist-db": "^1.0.11" + } + }, + "c8": { + "version": "7.14.0", + "resolved": "https://registry.npmjs.org/c8/-/c8-7.14.0.tgz", + "integrity": "sha512-i04rtkkcNcCf7zsQcSv/T9EbUn4RXQ6mropeMcjFOsQXQ0iGLAr/xT6TImQg4+U9hmNpN9XdvPkjUL1IzbgxJw==", + "dev": true, + "requires": { + "@bcoe/v8-coverage": "^0.2.3", + "@istanbuljs/schema": "^0.1.3", + "find-up": "^5.0.0", + "foreground-child": "^2.0.0", + "istanbul-lib-coverage": "^3.2.0", + "istanbul-lib-report": "^3.0.0", + "istanbul-reports": "^3.1.4", + "rimraf": "^3.0.2", + "test-exclude": "^6.0.0", + "v8-to-istanbul": "^9.0.0", + "yargs": "^16.2.0", + "yargs-parser": "^20.2.9" + }, + "dependencies": { + "rimraf": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz", + "integrity": "sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==", + "dev": true, + "requires": { + "glob": "^7.1.3" + } + } + } + }, + "cac": { + "version": "6.7.14", + "resolved": "https://registry.npmjs.org/cac/-/cac-6.7.14.tgz", + "integrity": "sha512-b6Ilus+c3RrdDk+JhLKUAQfzzgLEPy6wcXqS7f/xe1EETvsDP6GORG7SFuOs6cID5YkqchW/LXZbX5bc8j7ZcQ==", + "dev": true + }, + "call-bind": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.5.tgz", + "integrity": "sha512-C3nQxfFZxFRVoJoGKKI8y3MOEo129NQ+FgQ08iye+Mk4zNZZGdjfs06bVTr+DBSlA66Q2VEcMki/cUCP4SercQ==", + "dev": true, + "requires": { + "function-bind": "^1.1.2", + "get-intrinsic": "^1.2.1", + "set-function-length": "^1.1.1" + } + }, + "callsites": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz", + "integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==", + "dev": true + }, + "camelcase": { + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-6.3.0.tgz", + "integrity": "sha512-Gmy6FhYlCY7uOElZUSbxo2UCDH8owEk996gkbrpsgGtrJLM3J7jGxl9Ic7Qwwj4ivOE5AWZWRMecDdF7hqGjFA==", + "dev": true + }, + "caniuse-lite": { + "version": "1.0.30001524", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001524.tgz", + "integrity": "sha512-Jj917pJtYg9HSJBF95HVX3Cdr89JUyLT4IZ8SvM5aDRni95swKgYi3TgYLH5hnGfPE/U1dg6IfZ50UsIlLkwSA==", + "dev": true + }, + "chai": { + "version": "4.3.10", + "resolved": "https://registry.npmjs.org/chai/-/chai-4.3.10.tgz", + "integrity": "sha512-0UXG04VuVbruMUYbJ6JctvH0YnC/4q3/AkT18q4NaITo91CUm0liMS9VqzT9vZhVQ/1eqPanMWjBM+Juhfb/9g==", + "dev": true, + "requires": { + "assertion-error": "^1.1.0", + "check-error": "^1.0.3", + "deep-eql": "^4.1.3", + "get-func-name": "^2.0.2", + "loupe": "^2.3.6", + "pathval": "^1.1.1", + "type-detect": "^4.0.8" + } + }, + "chalk": { + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", + "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==", + "dev": true, + "requires": { + "ansi-styles": "^3.2.1", + "escape-string-regexp": "^1.0.5", + "supports-color": "^5.3.0" + } + }, + "check-error": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/check-error/-/check-error-1.0.3.tgz", + "integrity": "sha512-iKEoDYaRmd1mxM90a2OEfWhjsjPpYPuQ+lMYsoxB126+t8fw7ySEO48nmDg5COTjxDI65/Y2OWpeEHk3ZOe8zg==", + "dev": true, + "requires": { + "get-func-name": "^2.0.2" + } + }, + "chokidar": { + "version": "3.5.3", + "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.5.3.tgz", + "integrity": "sha512-Dr3sfKRP6oTcjf2JmUmFJfeVMvXBdegxB0iVQ5eb2V10uFJUCAS8OByZdVAyVb8xXNz3GjjTgj9kLWsZTqE6kw==", + "dev": true, + "requires": { + "anymatch": "~3.1.2", + "braces": "~3.0.2", + "fsevents": "~2.3.2", + "glob-parent": "~5.1.2", + "is-binary-path": "~2.1.0", + "is-glob": "~4.0.1", + "normalize-path": "~3.0.0", + "readdirp": "~3.6.0" + }, + "dependencies": { + "glob-parent": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", + "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", + "dev": true, + "requires": { + "is-glob": "^4.0.1" + } + } + } + }, + "cliui": { + "version": "7.0.4", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-7.0.4.tgz", + "integrity": "sha512-OcRE68cOsVMXp1Yvonl/fzkQOyjLSu/8bhPDfQt0e0/Eb283TKP20Fs2MqoPsr9SwA595rRCA+QMzYc9nBP+JQ==", + "dev": true, + "requires": { + "string-width": "^4.2.0", + "strip-ansi": "^6.0.0", + "wrap-ansi": "^7.0.0" + }, + "dependencies": { + "emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "dev": true + }, + "string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "dev": true, + "requires": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + } + } + } + }, + "color-convert": { + "version": "1.9.3", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz", + "integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==", + "dev": true, + "requires": { + "color-name": "1.1.3" + } + }, + "color-name": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz", + "integrity": "sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw==", + "dev": true + }, + "combined-stream": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", + "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", + "requires": { + "delayed-stream": "~1.0.0" + } + }, + "commander": { + "version": "10.0.1", + "resolved": "https://registry.npmjs.org/commander/-/commander-10.0.1.tgz", + "integrity": "sha512-y4Mg2tXshplEbSGzx7amzPwKKOCGuoSRP/CjEdwwk0FOGlUbq6lKuoyDZTNZkmxHdJtp54hdfY/JUrdL7Xfdug==", + "dev": true + }, + "computeds": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/computeds/-/computeds-0.0.1.tgz", + "integrity": "sha512-7CEBgcMjVmitjYo5q8JTJVra6X5mQ20uTThdK+0kR7UEaDrAWEQcRiBtWJzga4eRpP6afNwwLsX2SET2JhVB1Q==", + "dev": true + }, + "concat-map": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", + "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==", + "dev": true + }, + "config-chain": { + "version": "1.1.13", + "resolved": "https://registry.npmjs.org/config-chain/-/config-chain-1.1.13.tgz", + "integrity": "sha512-qj+f8APARXHrM0hraqXYb2/bOVSV4PvJQlNZ/DVj0QrmNM2q2euizkeuVckQ57J+W0mRH6Hvi+k50M4Jul2VRQ==", + "dev": true, + "requires": { + "ini": "^1.3.4", + "proto-list": "~1.2.1" + } + }, + "convert-source-map": { + "version": "1.9.0", + "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-1.9.0.tgz", + "integrity": "sha512-ASFBup0Mz1uyiIjANan1jzLQami9z1PoYSZCiiYW2FczPbenXc45FZdBZLzOT+r6+iciuEModtmCti+hjaAk0A==", + "dev": true + }, + "create-require": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/create-require/-/create-require-1.1.1.tgz", + "integrity": "sha512-dcKFX3jn0MpIaXjisoRvexIJVEKzaq7z2rZKxf+MSr9TkdmHmsU4m2lcLojrj/FHl8mk5VxMmYA+ftRkP/3oKQ==", + "dev": true + }, + "cross-spawn": { + "version": "7.0.3", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.3.tgz", + "integrity": "sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w==", + "dev": true, + "requires": { + "path-key": "^3.1.0", + "shebang-command": "^2.0.0", + "which": "^2.0.1" + } + }, + "crypto-js": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/crypto-js/-/crypto-js-4.2.0.tgz", + "integrity": "sha512-KALDyEYgpY+Rlob/iriUtjV6d5Eq+Y191A5g4UqLAi8CyGP9N1+FdVbkc1SxKc2r4YAYqG8JzO2KGL+AizD70Q==" + }, + "css.escape": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/css.escape/-/css.escape-1.5.1.tgz", + "integrity": "sha512-YUifsXXuknHlUsmlgyY0PKzgPOr7/FjCePfHNt0jxm83wHZi44VDMQ7/fGNkjY3/jV1MC+1CmZbaHzugyeRtpg==", + "dev": true + }, + "cssesc": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/cssesc/-/cssesc-3.0.0.tgz", + "integrity": "sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg==", + "dev": true + }, + "cssstyle": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/cssstyle/-/cssstyle-3.0.0.tgz", + "integrity": "sha512-N4u2ABATi3Qplzf0hWbVCdjenim8F3ojEXpBDF5hBpjzW182MjNGLqfmQ0SkSPeQ+V86ZXgeH8aXj6kayd4jgg==", + "dev": true, + "optional": true, + "peer": true, + "requires": { + "rrweb-cssom": "^0.6.0" + } + }, + "csstype": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.2.tgz", + "integrity": "sha512-I7K1Uu0MBPzaFKg4nI5Q7Vs2t+3gWWW648spaF+Rg7pI9ds18Ugn+lvg4SHczUdKlHI5LWBXyqfS8+DufyBsgQ==" + }, + "data-urls": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/data-urls/-/data-urls-4.0.0.tgz", + "integrity": "sha512-/mMTei/JXPqvFqQtfyTowxmJVwr2PVAeCcDxyFf6LhoOu/09TX2OX3kb2wzi4DMXcfj4OItwDOnhl5oziPnT6g==", + "dev": true, + "optional": true, + "peer": true, + "requires": { + "abab": "^2.0.6", + "whatwg-mimetype": "^3.0.0", + "whatwg-url": "^12.0.0" + }, + "dependencies": { + "tr46": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/tr46/-/tr46-4.1.1.tgz", + "integrity": "sha512-2lv/66T7e5yNyhAAC4NaKe5nVavzuGJQVVtRYLyQ2OI8tsJ61PMLlelehb0wi2Hx6+hT/OJUWZcw8MjlSRnxvw==", + "dev": true, + "optional": true, + "peer": true, + "requires": { + "punycode": "^2.3.0" + } + }, + "whatwg-url": { + "version": "12.0.1", + "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-12.0.1.tgz", + "integrity": "sha512-Ed/LrqB8EPlGxjS+TrsXcpUond1mhccS3pchLhzSgPCnTimUCKj3IZE75pAs5m6heB2U2TMerKFUXheyHY+VDQ==", + "dev": true, + "optional": true, + "peer": true, + "requires": { + "tr46": "^4.1.1", + "webidl-conversions": "^7.0.0" + } + } + } + }, + "date-fns": { + "version": "2.30.0", + "resolved": "https://registry.npmjs.org/date-fns/-/date-fns-2.30.0.tgz", + "integrity": "sha512-fnULvOpxnC5/Vg3NCiWelDsLiUc9bRwAPs/+LfTLNvetFCtCTN+yQz15C/fs4AwX1R9K5GLtLfn8QW+dWisaAw==", + "requires": { + "@babel/runtime": "^7.21.0" + } + }, + "de-indent": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/de-indent/-/de-indent-1.0.2.tgz", + "integrity": "sha512-e/1zu3xH5MQryN2zdVaF0OrdNLUbvWxzMbi+iNA6Bky7l1RoP8a2fIbRocyHclXt/arDrrR6lL3TqFD9pMQTsg==", + "dev": true + }, + "debug": { + "version": "4.3.4", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", + "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==", + "dev": true, + "requires": { + "ms": "2.1.2" + } + }, + "decimal.js": { + "version": "10.4.3", + "resolved": "https://registry.npmjs.org/decimal.js/-/decimal.js-10.4.3.tgz", + "integrity": "sha512-VBBaLc1MgL5XpzgIP7ny5Z6Nx3UrRkIViUkPUdtl9aya5amy3De1gsUUSB1g3+3sExYNjCAsAznmukyxCb1GRA==", + "dev": true, + "optional": true, + "peer": true + }, + "deep-eql": { + "version": "4.1.3", + "resolved": "https://registry.npmjs.org/deep-eql/-/deep-eql-4.1.3.tgz", + "integrity": "sha512-WaEtAOpRA1MQ0eohqZjpGD8zdI0Ovsm8mmFhaDN8dvDZzyoUMcYDnf5Y6iu7HTXxf8JDS23qWa4a+hKCDyOPzw==", + "dev": true, + "requires": { + "type-detect": "^4.0.0" + } + }, + "deep-equal": { + "version": "2.2.2", + "resolved": "https://registry.npmjs.org/deep-equal/-/deep-equal-2.2.2.tgz", + "integrity": "sha512-xjVyBf0w5vH0I42jdAZzOKVldmPgSulmiyPRywoyq7HXC9qdgo17kxJE+rdnif5Tz6+pIrpJI8dCpMNLIGkUiA==", + "dev": true, + "requires": { + "array-buffer-byte-length": "^1.0.0", + "call-bind": "^1.0.2", + "es-get-iterator": "^1.1.3", + "get-intrinsic": "^1.2.1", + "is-arguments": "^1.1.1", + "is-array-buffer": "^3.0.2", + "is-date-object": "^1.0.5", + "is-regex": "^1.1.4", + "is-shared-array-buffer": "^1.0.2", + "isarray": "^2.0.5", + "object-is": "^1.1.5", + "object-keys": "^1.1.1", + "object.assign": "^4.1.4", + "regexp.prototype.flags": "^1.5.0", + "side-channel": "^1.0.4", + "which-boxed-primitive": "^1.0.2", + "which-collection": "^1.0.1", + "which-typed-array": "^1.1.9" + } + }, + "deep-is": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz", + "integrity": "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==", + "dev": true + }, + "define-data-property": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/define-data-property/-/define-data-property-1.1.1.tgz", + "integrity": "sha512-E7uGkTzkk1d0ByLeSc6ZsFS79Axg+m1P/VsgYsxHgiuc3tFSj+MjMIwe90FC4lOAZzNBdY7kkO2P2wKdsQ1vgQ==", + "dev": true, + "requires": { + "get-intrinsic": "^1.2.1", + "gopd": "^1.0.1", + "has-property-descriptors": "^1.0.0" + } + }, + "define-properties": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/define-properties/-/define-properties-1.2.1.tgz", + "integrity": "sha512-8QmQKqEASLd5nx0U1B1okLElbUuuttJ/AnYmRXbbbGDWh6uS208EjD4Xqq/I9wK7u0v6O08XhTWnt5XtEbR6Dg==", + "dev": true, + "requires": { + "define-data-property": "^1.0.1", + "has-property-descriptors": "^1.0.0", + "object-keys": "^1.1.1" + } + }, + "delayed-stream": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", + "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==" + }, + "diff": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/diff/-/diff-4.0.2.tgz", + "integrity": "sha512-58lmxKSA4BNyLz+HHMUzlOEpg09FV+ev6ZMe3vJihgdxzgcwZ8VoEEPmALCZG9LmqfVoNMMKpttIYTVG6uDY7A==", + "dev": true + }, + "diff-sequences": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/diff-sequences/-/diff-sequences-29.6.3.tgz", + "integrity": "sha512-EjePK1srD3P08o2j4f0ExnylqRs5B9tJjcp9t1krH2qRi8CCdsYfwe9JgSLurFBWwq4uOlipzfk5fHNvwFKr8Q==", + "dev": true + }, + "dir-glob": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/dir-glob/-/dir-glob-3.0.1.tgz", + "integrity": "sha512-WkrWp9GR4KXfKGYzOLmTuGVi1UWFfws377n9cc55/tb6DuqyF6pcQ5AbiHEshaDpY9v6oaSr2XCDidGmMwdzIA==", + "dev": true, + "requires": { + "path-type": "^4.0.0" + } + }, + "doctrine": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/doctrine/-/doctrine-3.0.0.tgz", + "integrity": "sha512-yS+Q5i3hBf7GBkd4KG8a7eBNNWNGLTaEwwYWUijIYM7zrlYDM0BFXHjjPWlWZ1Rg7UaddZeIDmi9jF3HmqiQ2w==", + "dev": true, + "requires": { + "esutils": "^2.0.2" + } + }, + "dom-accessibility-api": { + "version": "0.5.16", + "resolved": "https://registry.npmjs.org/dom-accessibility-api/-/dom-accessibility-api-0.5.16.tgz", + "integrity": "sha512-X7BJ2yElsnOJ30pZF4uIIDfBEVgF4XEBxL9Bxhy6dnrm5hkzqmsWHGTiHqRiITNhMyFLyAiWndIJP7Z1NTteDg==", + "dev": true + }, + "domexception": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/domexception/-/domexception-4.0.0.tgz", + "integrity": "sha512-A2is4PLG+eeSfoTMA95/s4pvAoSo2mKtiM5jlHkAVewmiO8ISFTFKZjH7UAM1Atli/OT/7JHOrJRJiMKUZKYBw==", + "dev": true, + "optional": true, + "peer": true, + "requires": { + "webidl-conversions": "^7.0.0" + } + }, + "eastasianwidth": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/eastasianwidth/-/eastasianwidth-0.2.0.tgz", + "integrity": "sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==", + "dev": true + }, + "editorconfig": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/editorconfig/-/editorconfig-1.0.4.tgz", + "integrity": "sha512-L9Qe08KWTlqYMVvMcTIvMAdl1cDUubzRNYL+WfA4bLDMHe4nemKkpmYzkznE1FwLKu0EEmy6obgQKzMJrg4x9Q==", + "dev": true, + "requires": { + "@one-ini/wasm": "0.1.1", + "commander": "^10.0.0", + "minimatch": "9.0.1", + "semver": "^7.5.3" + }, + "dependencies": { + "brace-expansion": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", + "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", + "dev": true, + "requires": { + "balanced-match": "^1.0.0" + } + }, + "minimatch": { + "version": "9.0.1", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.1.tgz", + "integrity": "sha512-0jWhJpD/MdhPXwPuiRkCbfYfSKp2qnn2eOc279qI7f+osl/l+prKSrvhg157zSYvx/1nmgn2NqdT6k2Z7zSH9w==", + "dev": true, + "requires": { + "brace-expansion": "^2.0.1" + } + }, + "semver": { + "version": "7.5.4", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.5.4.tgz", + "integrity": "sha512-1bCSESV6Pv+i21Hvpxp3Dx+pSD8lIPt8uVjRrxAUt/nbswYc+tK6Y2btiULjd4+fnq15PX+nqQDC7Oft7WkwcA==", + "dev": true, + "requires": { + "lru-cache": "^6.0.0" + } + } + } + }, + "electron-to-chromium": { + "version": "1.4.505", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.4.505.tgz", + "integrity": "sha512-0A50eL5BCCKdxig2SsCXhpuztnB9PfUgRMojj5tMvt8O54lbwz3t6wNgnpiTRosw5QjlJB7ixhVyeg8daLQwSQ==", + "dev": true + }, + "emoji-regex": { + "version": "9.2.2", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-9.2.2.tgz", + "integrity": "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==", + "dev": true + }, + "entities": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/entities/-/entities-4.5.0.tgz", + "integrity": "sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==", + "dev": true + }, + "es-get-iterator": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/es-get-iterator/-/es-get-iterator-1.1.3.tgz", + "integrity": "sha512-sPZmqHBe6JIiTfN5q2pEi//TwxmAFHwj/XEuYjTuse78i8KxaqMTTzxPoFKuzRpDpTJ+0NAbpfenkmH2rePtuw==", + "dev": true, + "requires": { + "call-bind": "^1.0.2", + "get-intrinsic": "^1.1.3", + "has-symbols": "^1.0.3", + "is-arguments": "^1.1.1", + "is-map": "^2.0.2", + "is-set": "^2.0.2", + "is-string": "^1.0.7", + "isarray": "^2.0.5", + "stop-iteration-iterator": "^1.0.0" + } + }, + "esbuild": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.18.20.tgz", + "integrity": "sha512-ceqxoedUrcayh7Y7ZX6NdbbDzGROiyVBgC4PriJThBKSVPWnnFHZAkfI1lJT8QFkOwH4qOS2SJkS4wvpGl8BpA==", + "dev": true, + "requires": { + "@esbuild/android-arm": "0.18.20", + "@esbuild/android-arm64": "0.18.20", + "@esbuild/android-x64": "0.18.20", + "@esbuild/darwin-arm64": "0.18.20", + "@esbuild/darwin-x64": "0.18.20", + "@esbuild/freebsd-arm64": "0.18.20", + "@esbuild/freebsd-x64": "0.18.20", + "@esbuild/linux-arm": "0.18.20", + "@esbuild/linux-arm64": "0.18.20", + "@esbuild/linux-ia32": "0.18.20", + "@esbuild/linux-loong64": "0.18.20", + "@esbuild/linux-mips64el": "0.18.20", + "@esbuild/linux-ppc64": "0.18.20", + "@esbuild/linux-riscv64": "0.18.20", + "@esbuild/linux-s390x": "0.18.20", + "@esbuild/linux-x64": "0.18.20", + "@esbuild/netbsd-x64": "0.18.20", + "@esbuild/openbsd-x64": "0.18.20", + "@esbuild/sunos-x64": "0.18.20", + "@esbuild/win32-arm64": "0.18.20", + "@esbuild/win32-ia32": "0.18.20", + "@esbuild/win32-x64": "0.18.20" + } + }, + "escalade": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.1.1.tgz", + "integrity": "sha512-k0er2gUkLf8O0zKJiAhmkTnJlTvINGv7ygDNPbeIsX/TJjGJZHuh9B2UxbsaEkmlEo9MfhrSzmhIlhRlI2GXnw==", + "dev": true + }, + "escape-string-regexp": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", + "integrity": "sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg==", + "dev": true + }, + "eslint": { + "version": "8.52.0", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-8.52.0.tgz", + "integrity": "sha512-zh/JHnaixqHZsolRB/w9/02akBk9EPrOs9JwcTP2ek7yL5bVvXuRariiaAjjoJ5DvuwQ1WAE/HsMz+w17YgBCg==", + "dev": true, + "requires": { + "@eslint-community/eslint-utils": "^4.2.0", + "@eslint-community/regexpp": "^4.6.1", + "@eslint/eslintrc": "^2.1.2", + "@eslint/js": "8.52.0", + "@humanwhocodes/config-array": "^0.11.13", + "@humanwhocodes/module-importer": "^1.0.1", + "@nodelib/fs.walk": "^1.2.8", + "@ungap/structured-clone": "^1.2.0", + "ajv": "^6.12.4", + "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.2.2", + "eslint-visitor-keys": "^3.4.3", + "espree": "^9.6.1", + "esquery": "^1.4.2", + "esutils": "^2.0.2", + "fast-deep-equal": "^3.1.3", + "file-entry-cache": "^6.0.1", + "find-up": "^5.0.0", + "glob-parent": "^6.0.2", + "globals": "^13.19.0", + "graphemer": "^1.4.0", + "ignore": "^5.2.0", + "imurmurhash": "^0.1.4", + "is-glob": "^4.0.0", + "is-path-inside": "^3.0.3", + "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.3", + "strip-ansi": "^6.0.1", + "text-table": "^0.2.0" + }, + "dependencies": { + "ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "requires": { + "color-convert": "^2.0.1" + } + }, + "chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, + "requires": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + } + }, + "color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "requires": { + "color-name": "~1.1.4" + } + }, + "color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true + }, + "escape-string-regexp": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", + "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==", + "dev": true + }, + "eslint-scope": { + "version": "7.2.2", + "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-7.2.2.tgz", + "integrity": "sha512-dOt21O7lTMhDM+X9mB4GX+DZrZtCUJPL/wlcTqxyrx5IvO0IYtILdtrQGQp+8n5S0gwSVmOf9NQrjMOgfQZlIg==", + "dev": true, + "requires": { + "esrecurse": "^4.3.0", + "estraverse": "^5.2.0" + } + }, + "estraverse": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz", + "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==", + "dev": true + }, + "globals": { + "version": "13.20.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-13.20.0.tgz", + "integrity": "sha512-Qg5QtVkCy/kv3FUSlu4ukeZDVf9ee0iXLAUYX13gbR17bnejFTzr4iS9bY7kwCf1NztRNm1t91fjOiyx4CSwPQ==", + "dev": true, + "requires": { + "type-fest": "^0.20.2" + } + }, + "has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true + }, + "supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "requires": { + "has-flag": "^4.0.0" + } + } + } + }, + "eslint-config-prettier": { + "version": "9.0.0", + "resolved": "https://registry.npmjs.org/eslint-config-prettier/-/eslint-config-prettier-9.0.0.tgz", + "integrity": "sha512-IcJsTkJae2S35pRsRAwoCE+925rJJStOdkKnLVgtE+tEpqU0EVVM7OqrwxqgptKdX29NUwC82I5pXsGFIgSevw==", + "dev": true, + "requires": {} + }, + "eslint-plugin-vitest-globals": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/eslint-plugin-vitest-globals/-/eslint-plugin-vitest-globals-1.4.0.tgz", + "integrity": "sha512-WE+YlK9X9s4vf5EaYRU0Scw7WItDZStm+PapFSYlg2ABNtaQ4zIG7wEqpoUB3SlfM+SgkhgmzR0TeJOO5k3/Nw==", + "dev": true + }, + "eslint-plugin-vue": { + "version": "9.18.1", + "resolved": "https://registry.npmjs.org/eslint-plugin-vue/-/eslint-plugin-vue-9.18.1.tgz", + "integrity": "sha512-7hZFlrEgg9NIzuVik2I9xSnJA5RsmOfueYgsUGUokEDLJ1LHtxO0Pl4duje1BriZ/jDWb+44tcIlC3yi0tdlZg==", + "dev": true, + "requires": { + "@eslint-community/eslint-utils": "^4.4.0", + "natural-compare": "^1.4.0", + "nth-check": "^2.1.1", + "postcss-selector-parser": "^6.0.13", + "semver": "^7.5.4", + "vue-eslint-parser": "^9.3.1", + "xml-name-validator": "^4.0.0" + }, + "dependencies": { + "semver": { + "version": "7.5.4", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.5.4.tgz", + "integrity": "sha512-1bCSESV6Pv+i21Hvpxp3Dx+pSD8lIPt8uVjRrxAUt/nbswYc+tK6Y2btiULjd4+fnq15PX+nqQDC7Oft7WkwcA==", + "dev": true, + "requires": { + "lru-cache": "^6.0.0" + } + } + } + }, + "eslint-visitor-keys": { + "version": "3.4.3", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.4.3.tgz", + "integrity": "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==", + "dev": true + }, + "espree": { + "version": "9.6.1", + "resolved": "https://registry.npmjs.org/espree/-/espree-9.6.1.tgz", + "integrity": "sha512-oruZaFkjorTpF32kDSI5/75ViwGeZginGGy2NoOSg3Q9bnwlnmDm4HLnkl0RE3n+njDXR037aY1+x58Z/zFdwQ==", + "dev": true, + "requires": { + "acorn": "^8.9.0", + "acorn-jsx": "^5.3.2", + "eslint-visitor-keys": "^3.4.1" + } + }, + "esquery": { + "version": "1.4.2", + "resolved": "https://registry.npmjs.org/esquery/-/esquery-1.4.2.tgz", + "integrity": "sha512-JVSoLdTlTDkmjFmab7H/9SL9qGSyjElT3myyKp7krqjVFQCDLmj1QFaCLRFBszBKI0XVZaiiXvuPIX3ZwHe1Ng==", + "dev": true, + "requires": { + "estraverse": "^5.1.0" + }, + "dependencies": { + "estraverse": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz", + "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==", + "dev": true + } + } + }, + "esrecurse": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/esrecurse/-/esrecurse-4.3.0.tgz", + "integrity": "sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==", + "dev": true, + "requires": { + "estraverse": "^5.2.0" + }, + "dependencies": { + "estraverse": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz", + "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==", + "dev": true + } + } + }, + "estree-walker": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-2.0.2.tgz", + "integrity": "sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w==" + }, + "esutils": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz", + "integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==", + "dev": true + }, + "fast-deep-equal": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", + "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", + "dev": true + }, + "fast-glob": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.1.tgz", + "integrity": "sha512-kNFPyjhh5cKjrUltxs+wFx+ZkbRaxxmZ+X0ZU31SOsxCEtP9VPgtq2teZw1DebupL5GmDaNQ6yKMMVcM41iqDg==", + "dev": true, + "requires": { + "@nodelib/fs.stat": "^2.0.2", + "@nodelib/fs.walk": "^1.2.3", + "glob-parent": "^5.1.2", + "merge2": "^1.3.0", + "micromatch": "^4.0.4" + }, + "dependencies": { + "glob-parent": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", + "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", + "dev": true, + "requires": { + "is-glob": "^4.0.1" + } + } + } + }, + "fast-json-stable-stringify": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", + "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==", + "dev": true + }, + "fast-levenshtein": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz", + "integrity": "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==", + "dev": true + }, + "fastq": { + "version": "1.13.0", + "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.13.0.tgz", + "integrity": "sha512-YpkpUnK8od0o1hmeSc7UUs/eB/vIPWJYjKck2QKIzAf71Vm1AAQ3EbuZB3g2JIy+pg+ERD0vqI79KyZiB2e2Nw==", + "dev": true, + "requires": { + "reusify": "^1.0.4" + } + }, + "file-entry-cache": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-6.0.1.tgz", + "integrity": "sha512-7Gps/XWymbLk2QLYK4NzpMOrYjMhdIxXuIvy2QBsLE6ljuodKvdkWs/cpyJJ3CVIVpH0Oi1Hvg1ovbMzLdFBBg==", + "dev": true, + "requires": { + "flat-cache": "^3.0.4" + } + }, + "filesize": { + "version": "10.1.0", + "resolved": "https://registry.npmjs.org/filesize/-/filesize-10.1.0.tgz", + "integrity": "sha512-GTLKYyBSDz3nPhlLVPjPWZCnhkd9TrrRArNcy8Z+J2cqScB7h2McAzR6NBX6nYOoWafql0roY8hrocxnZBv9CQ==" + }, + "fill-range": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.0.1.tgz", + "integrity": "sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ==", + "dev": true, + "requires": { + "to-regex-range": "^5.0.1" + } + }, + "find-up": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz", + "integrity": "sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==", + "dev": true, + "requires": { + "locate-path": "^6.0.0", + "path-exists": "^4.0.0" + } + }, + "flat-cache": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-3.0.4.tgz", + "integrity": "sha512-dm9s5Pw7Jc0GvMYbshN6zchCA9RgQlzzEZX3vylR9IqFfS8XciblUXOKfW6SiuJ0e13eDYZoZV5wdrev7P3Nwg==", + "dev": true, + "requires": { + "flatted": "^3.1.0", + "rimraf": "^3.0.2" + }, + "dependencies": { + "rimraf": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz", + "integrity": "sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==", + "dev": true, + "requires": { + "glob": "^7.1.3" + } + } + } + }, + "flatted": { + "version": "3.2.7", + "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.2.7.tgz", + "integrity": "sha512-5nqDSxl8nn5BSNxyR3n4I6eDmbolI6WT+QqR547RwxQapgjQBmtktdP+HTBb/a/zLsbzERTONyUB5pefh5TtjQ==", + "dev": true + }, + "follow-redirects": { + "version": "1.15.2", + "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.2.tgz", + "integrity": "sha512-VQLG33o04KaQ8uYi2tVNbdrWp1QWxNNea+nmIB4EVM28v0hmP17z7aG1+wAkNzVq4KeXTq3221ye5qTJP91JwA==" + }, + "for-each": { + "version": "0.3.3", + "resolved": "https://registry.npmjs.org/for-each/-/for-each-0.3.3.tgz", + "integrity": "sha512-jqYfLp7mo9vIyQf8ykW2v7A+2N4QjeCeI5+Dz9XraiO1ign81wjiH7Fb9vSOWvQfNtmSa4H2RoQTrrXivdUZmw==", + "dev": true, + "requires": { + "is-callable": "^1.1.3" + } + }, + "foreground-child": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/foreground-child/-/foreground-child-2.0.0.tgz", + "integrity": "sha512-dCIq9FpEcyQyXKCkyzmlPTFNgrCzPudOe+mhvJU5zAtlBnGVy2yKxtfsxK2tQBThwq225jcvBjpw1Gr40uzZCA==", + "dev": true, + "requires": { + "cross-spawn": "^7.0.0", + "signal-exit": "^3.0.2" + } + }, + "form-data": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.0.tgz", + "integrity": "sha512-ETEklSGi5t0QMZuiXoA/Q6vcnxcLQP5vdugSpuAyi6SVGi2clPPp+xgEhuMaHC+zGgn31Kd235W35f7Hykkaww==", + "requires": { + "asynckit": "^0.4.0", + "combined-stream": "^1.0.8", + "mime-types": "^2.1.12" + } + }, + "fs.realpath": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", + "integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==", + "dev": true + }, + "fsevents": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz", + "integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==", + "dev": true, + "optional": true + }, + "function-bind": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", + "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", + "dev": true + }, + "functions-have-names": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/functions-have-names/-/functions-have-names-1.2.3.tgz", + "integrity": "sha512-xckBUXyTIqT97tq2x2AMb+g163b5JFysYk0x4qxNFwbfQkmNZoiRHb6sPzI9/QV33WeuvVYBUIiD4NzNIyqaRQ==", + "dev": true + }, + "gensync": { + "version": "1.0.0-beta.2", + "resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz", + "integrity": "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==", + "dev": true + }, + "get-caller-file": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz", + "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==", + "dev": true + }, + "get-func-name": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/get-func-name/-/get-func-name-2.0.2.tgz", + "integrity": "sha512-8vXOvuE167CtIc3OyItco7N/dpRtBbYOsPsXCz7X/PMnlGjYjSGuZJgM1Y7mmew7BKf9BqvLX2tnOVy1BBUsxQ==", + "dev": true + }, + "get-intrinsic": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.2.2.tgz", + "integrity": "sha512-0gSo4ml/0j98Y3lngkFEot/zhiCeWsbYIlZ+uZOVgzLyLaUw7wxUL+nCTP0XJvJg1AXulJRI3UJi8GsbDuxdGA==", + "dev": true, + "requires": { + "function-bind": "^1.1.2", + "has-proto": "^1.0.1", + "has-symbols": "^1.0.3", + "hasown": "^2.0.0" + } + }, + "glob": { + "version": "7.2.3", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", + "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", + "dev": true, + "requires": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.1.1", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + } + }, + "glob-parent": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz", + "integrity": "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==", + "dev": true, + "requires": { + "is-glob": "^4.0.3" + } + }, + "globals": { + "version": "11.12.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-11.12.0.tgz", + "integrity": "sha512-WOBp/EEGUiIsJSp7wcv/y6MO+lV9UoncWqxuFfm8eBwzWNgyfBd6Gz+IeKQ9jCmyhoH99g15M3T+QaVHFjizVA==", + "dev": true + }, + "globby": { + "version": "11.1.0", + "resolved": "https://registry.npmjs.org/globby/-/globby-11.1.0.tgz", + "integrity": "sha512-jhIXaOzy1sb8IyocaruWSn1TjmnBVs8Ayhcy83rmxNJ8q2uWKCAj3CnJY+KpGSXCueAPc0i05kVvVKtP1t9S3g==", + "dev": true, + "requires": { + "array-union": "^2.1.0", + "dir-glob": "^3.0.1", + "fast-glob": "^3.2.9", + "ignore": "^5.2.0", + "merge2": "^1.4.1", + "slash": "^3.0.0" + } + }, + "gopd": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.0.1.tgz", + "integrity": "sha512-d65bNlIadxvpb/A2abVdlqKqV563juRnZ1Wtk6s1sIR8uNsXR70xqIzVqxVf1eTqDunwT2MkczEeaezCKTZhwA==", + "dev": true, + "requires": { + "get-intrinsic": "^1.1.3" + } + }, + "graphemer": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/graphemer/-/graphemer-1.4.0.tgz", + "integrity": "sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag==", + "dev": true + }, + "happy-dom": { + "version": "12.10.3", + "resolved": "https://registry.npmjs.org/happy-dom/-/happy-dom-12.10.3.tgz", + "integrity": "sha512-JzUXOh0wdNGY54oKng5hliuBkq/+aT1V3YpTM+lrN/GoLQTANZsMaIvmHiHe612rauHvPJnDZkZ+5GZR++1Abg==", + "dev": true, + "requires": { + "css.escape": "^1.5.1", + "entities": "^4.5.0", + "iconv-lite": "^0.6.3", + "webidl-conversions": "^7.0.0", + "whatwg-encoding": "^2.0.0", + "whatwg-mimetype": "^3.0.0" + } + }, + "has-bigints": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/has-bigints/-/has-bigints-1.0.2.tgz", + "integrity": "sha512-tSvCKtBr9lkF0Ex0aQiP9N+OpV4zi2r/Nee5VkRDbaqv35RLYMzbwQfFSZZH0kR+Rd6302UJZ2p/bJCEoR3VoQ==", + "dev": true + }, + "has-flag": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", + "integrity": "sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==", + "dev": true + }, + "has-property-descriptors": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/has-property-descriptors/-/has-property-descriptors-1.0.1.tgz", + "integrity": "sha512-VsX8eaIewvas0xnvinAe9bw4WfIeODpGYikiWYLH+dma0Jw6KHYqWiWfhQlgOVK8D6PvjubK5Uc4P0iIhIcNVg==", + "dev": true, + "requires": { + "get-intrinsic": "^1.2.2" + } + }, + "has-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/has-proto/-/has-proto-1.0.1.tgz", + "integrity": "sha512-7qE+iP+O+bgF9clE5+UoBFzE65mlBiVj3tKCrlNQ0Ogwm0BjpT/gK4SlLYDMybDh5I3TCTKnPPa0oMG7JDYrhg==", + "dev": true + }, + "has-symbols": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.0.3.tgz", + "integrity": "sha512-l3LCuF6MgDNwTDKkdYGEihYjt5pRPbEg46rtlmnSPlUbgmB8LOIrKJbYYFBSbnPaJexMKtiPO8hmeRjRz2Td+A==", + "dev": true + }, + "has-tostringtag": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.0.tgz", + "integrity": "sha512-kFjcSNhnlGV1kyoGk7OXKSawH5JOb/LzUc5w9B02hOTO0dfFRjbHQKvg1d6cf3HbeUmtU9VbbV3qzZ2Teh97WQ==", + "dev": true, + "requires": { + "has-symbols": "^1.0.2" + } + }, + "hasown": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.0.tgz", + "integrity": "sha512-vUptKVTpIJhcczKBbgnS+RtcuYMB8+oNzPK2/Hp3hanz8JmpATdmmgLgSaadVREkDm+e2giHwY3ZRkyjSIDDFA==", + "dev": true, + "requires": { + "function-bind": "^1.1.2" + } + }, + "he": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/he/-/he-1.2.0.tgz", + "integrity": "sha512-F/1DnUGPopORZi0ni+CvrCgHQ5FyEAHRLSApuYWMmrbSwoN2Mn/7k+Gl38gJnR7yyDZk6WLXwiGod1JOWNDKGw==", + "dev": true + }, + "html-encoding-sniffer": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/html-encoding-sniffer/-/html-encoding-sniffer-3.0.0.tgz", + "integrity": "sha512-oWv4T4yJ52iKrufjnyZPkrN0CH3QnrUqdB6In1g5Fe1mia8GmF36gnfNySxoZtxD5+NmYw1EElVXiBk93UeskA==", + "dev": true, + "optional": true, + "peer": true, + "requires": { + "whatwg-encoding": "^2.0.0" + } + }, + "html-escaper": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/html-escaper/-/html-escaper-2.0.2.tgz", + "integrity": "sha512-H2iMtd0I4Mt5eYiapRdIDjp+XzelXQ0tFE4JS7YFwFevXXMmOp9myNrUvCg0D6ws8iqkRPBfKHgbwig1SmlLfg==", + "dev": true + }, + "html-tags": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/html-tags/-/html-tags-3.3.1.tgz", + "integrity": "sha512-ztqyC3kLto0e9WbNp0aeP+M3kTt+nbaIveGmUxAtZa+8iFgKLUOD4YKM5j+f3QD89bra7UeumolZHKuOXnTmeQ==", + "dev": true + }, + "http-proxy-agent": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/http-proxy-agent/-/http-proxy-agent-5.0.0.tgz", + "integrity": "sha512-n2hY8YdoRE1i7r6M0w9DIw5GgZN0G25P8zLCRQ8rjXtTU3vsNFBI/vWK/UIeE6g5MUUz6avwAPXmL6Fy9D/90w==", + "dev": true, + "optional": true, + "peer": true, + "requires": { + "@tootallnate/once": "2", + "agent-base": "6", + "debug": "4" + } + }, + "https-proxy-agent": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-5.0.1.tgz", + "integrity": "sha512-dFcAjpTQFgoLMzC2VwU+C/CbS7uRL0lWmxDITmqm7C+7F0Odmj6s9l6alZc6AELXhrnggM2CeWSXHGOdX2YtwA==", + "dev": true, + "optional": true, + "peer": true, + "requires": { + "agent-base": "6", + "debug": "4" + } + }, + "iconv-lite": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz", + "integrity": "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==", + "dev": true, + "requires": { + "safer-buffer": ">= 2.1.2 < 3.0.0" + } + }, + "ignore": { + "version": "5.2.4", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.2.4.tgz", + "integrity": "sha512-MAb38BcSbH0eHNBxn7ql2NH/kX33OkB3lZ1BNdh7ENeRChHTYsTvWrMubiIAMNS2llXEEgZ1MUOBtXChP3kaFQ==", + "dev": true + }, + "immutable": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/immutable/-/immutable-4.1.0.tgz", + "integrity": "sha512-oNkuqVTA8jqG1Q6c+UglTOD1xhC1BtjKI7XkCXRkZHrN5m18/XsnUp8Q89GkQO/z+0WjonSvl0FLhDYftp46nQ==", + "dev": true + }, + "import-fresh": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.0.tgz", + "integrity": "sha512-veYYhQa+D1QBKznvhUHxb8faxlrwUnxseDAbAp457E0wLNio2bOSKnjYDhMj+YiAq61xrMGhQk9iXVk5FzgQMw==", + "dev": true, + "requires": { + "parent-module": "^1.0.0", + "resolve-from": "^4.0.0" + } + }, + "imurmurhash": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz", + "integrity": "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==", + "dev": true + }, + "inflight": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", + "integrity": "sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==", + "dev": true, + "requires": { + "once": "^1.3.0", + "wrappy": "1" + } + }, + "inherits": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", + "dev": true + }, + "ini": { + "version": "1.3.8", + "resolved": "https://registry.npmjs.org/ini/-/ini-1.3.8.tgz", + "integrity": "sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew==", + "dev": true + }, + "internal-slot": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/internal-slot/-/internal-slot-1.0.6.tgz", + "integrity": "sha512-Xj6dv+PsbtwyPpEflsejS+oIZxmMlV44zAhG479uYu89MsjcYOhCFnNyKrkJrihbsiasQyY0afoCl/9BLR65bg==", + "dev": true, + "requires": { + "get-intrinsic": "^1.2.2", + "hasown": "^2.0.0", + "side-channel": "^1.0.4" + } + }, + "is-arguments": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/is-arguments/-/is-arguments-1.1.1.tgz", + "integrity": "sha512-8Q7EARjzEnKpt/PCD7e1cgUS0a6X8u5tdSiMqXhojOdoV9TsMsiO+9VLC5vAmO8N7/GmXn7yjR8qnA6bVAEzfA==", + "dev": true, + "requires": { + "call-bind": "^1.0.2", + "has-tostringtag": "^1.0.0" + } + }, + "is-array-buffer": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/is-array-buffer/-/is-array-buffer-3.0.2.tgz", + "integrity": "sha512-y+FyyR/w8vfIRq4eQcM1EYgSTnmHXPqaF+IgzgraytCFq5Xh8lllDVmAZolPJiZttZLeFSINPYMaEJ7/vWUa1w==", + "dev": true, + "requires": { + "call-bind": "^1.0.2", + "get-intrinsic": "^1.2.0", + "is-typed-array": "^1.1.10" + } + }, + "is-bigint": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/is-bigint/-/is-bigint-1.0.4.tgz", + "integrity": "sha512-zB9CruMamjym81i2JZ3UMn54PKGsQzsJeo6xvN3HJJ4CAsQNB6iRutp2To77OfCNuoxspsIhzaPoO1zyCEhFOg==", + "dev": true, + "requires": { + "has-bigints": "^1.0.1" + } + }, + "is-binary-path": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz", + "integrity": "sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==", + "dev": true, + "requires": { + "binary-extensions": "^2.0.0" + } + }, + "is-boolean-object": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/is-boolean-object/-/is-boolean-object-1.1.2.tgz", + "integrity": "sha512-gDYaKHJmnj4aWxyj6YHyXVpdQawtVLHU5cb+eztPGczf6cjuTdwve5ZIEfgXqH4e57An1D1AKf8CZ3kYrQRqYA==", + "dev": true, + "requires": { + "call-bind": "^1.0.2", + "has-tostringtag": "^1.0.0" + } + }, + "is-callable": { + "version": "1.2.7", + "resolved": "https://registry.npmjs.org/is-callable/-/is-callable-1.2.7.tgz", + "integrity": "sha512-1BC0BVFhS/p0qtw6enp8e+8OD0UrK0oFLztSjNzhcKA3WDuJxxAPXzPuPtKkjEY9UUoEWlX/8fgKeu2S8i9JTA==", + "dev": true + }, + "is-date-object": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/is-date-object/-/is-date-object-1.0.5.tgz", + "integrity": "sha512-9YQaSxsAiSwcvS33MBk3wTCVnWK+HhF8VZR2jRxehM16QcVOdHqPn4VPHmRK4lSr38n9JriurInLcP90xsYNfQ==", + "dev": true, + "requires": { + "has-tostringtag": "^1.0.0" + } + }, + "is-extglob": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", + "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", + "dev": true + }, + "is-fullwidth-code-point": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", + "dev": true + }, + "is-glob": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", + "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", + "dev": true, + "requires": { + "is-extglob": "^2.1.1" + } + }, + "is-map": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/is-map/-/is-map-2.0.2.tgz", + "integrity": "sha512-cOZFQQozTha1f4MxLFzlgKYPTyj26picdZTx82hbc/Xf4K/tZOOXSCkMvU4pKioRXGDLJRn0GM7Upe7kR721yg==", + "dev": true + }, + "is-number": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", + "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", + "dev": true + }, + "is-number-object": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/is-number-object/-/is-number-object-1.0.7.tgz", + "integrity": "sha512-k1U0IRzLMo7ZlYIfzRu23Oh6MiIFasgpb9X76eqfFZAqwH44UI4KTBvBYIZ1dSL9ZzChTB9ShHfLkR4pdW5krQ==", + "dev": true, + "requires": { + "has-tostringtag": "^1.0.0" + } + }, + "is-path-inside": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/is-path-inside/-/is-path-inside-3.0.3.tgz", + "integrity": "sha512-Fd4gABb+ycGAmKou8eMftCupSir5lRxqf4aD/vd0cD2qc4HL07OjCeuHMr8Ro4CoMaeCKDB0/ECBOVWjTwUvPQ==", + "dev": true + }, + "is-potential-custom-element-name": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/is-potential-custom-element-name/-/is-potential-custom-element-name-1.0.1.tgz", + "integrity": "sha512-bCYeRA2rVibKZd+s2625gGnGF/t7DSqDs4dP7CrLA1m7jKWz6pps0LpYLJN8Q64HtmPKJ1hrN3nzPNKFEKOUiQ==", + "dev": true, + "optional": true, + "peer": true + }, + "is-regex": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/is-regex/-/is-regex-1.1.4.tgz", + "integrity": "sha512-kvRdxDsxZjhzUX07ZnLydzS1TU/TJlTUHHY4YLL87e37oUA49DfkLqgy+VjFocowy29cKvcSiu+kIv728jTTVg==", + "dev": true, + "requires": { + "call-bind": "^1.0.2", + "has-tostringtag": "^1.0.0" + } + }, + "is-set": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/is-set/-/is-set-2.0.2.tgz", + "integrity": "sha512-+2cnTEZeY5z/iXGbLhPrOAaK/Mau5k5eXq9j14CpRTftq0pAJu2MwVRSZhyZWBzx3o6X795Lz6Bpb6R0GKf37g==", + "dev": true + }, + "is-shared-array-buffer": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/is-shared-array-buffer/-/is-shared-array-buffer-1.0.2.tgz", + "integrity": "sha512-sqN2UDu1/0y6uvXyStCOzyhAjCSlHceFoMKJW8W9EU9cvic/QdsZ0kEU93HEy3IUEFZIiH/3w+AH/UQbPHNdhA==", + "dev": true, + "requires": { + "call-bind": "^1.0.2" + } + }, + "is-string": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/is-string/-/is-string-1.0.7.tgz", + "integrity": "sha512-tE2UXzivje6ofPW7l23cjDOMa09gb7xlAqG6jG5ej6uPV32TlWP3NKPigtaGeHNu9fohccRYvIiZMfOOnOYUtg==", + "dev": true, + "requires": { + "has-tostringtag": "^1.0.0" + } + }, + "is-symbol": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/is-symbol/-/is-symbol-1.0.4.tgz", + "integrity": "sha512-C/CPBqKWnvdcxqIARxyOh4v1UUEOCHpgDa0WYgpKDFMszcrPcffg5uhwSgPCLD2WWxmq6isisz87tzT01tuGhg==", + "dev": true, + "requires": { + "has-symbols": "^1.0.2" + } + }, + "is-typed-array": { + "version": "1.1.12", + "resolved": "https://registry.npmjs.org/is-typed-array/-/is-typed-array-1.1.12.tgz", + "integrity": "sha512-Z14TF2JNG8Lss5/HMqt0//T9JeHXttXy5pH/DBU4vi98ozO2btxzq9MwYDZYnKwU8nRsz/+GVFVRDq3DkVuSPg==", + "dev": true, + "requires": { + "which-typed-array": "^1.1.11" + } + }, + "is-weakmap": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/is-weakmap/-/is-weakmap-2.0.1.tgz", + "integrity": "sha512-NSBR4kH5oVj1Uwvv970ruUkCV7O1mzgVFO4/rev2cLRda9Tm9HrL70ZPut4rOHgY0FNrUu9BCbXA2sdQ+x0chA==", + "dev": true + }, + "is-weakset": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/is-weakset/-/is-weakset-2.0.2.tgz", + "integrity": "sha512-t2yVvttHkQktwnNNmBQ98AhENLdPUTDTE21uPqAQ0ARwQfGeQKRVS0NNurH7bTf7RrvcVn1OOge45CnBeHCSmg==", + "dev": true, + "requires": { + "call-bind": "^1.0.2", + "get-intrinsic": "^1.1.1" + } + }, + "isarray": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-2.0.5.tgz", + "integrity": "sha512-xHjhDr3cNBK0BzdUJSPXZntQUx/mwMS5Rw4A7lPJ90XGAO6ISP/ePDNuo0vhqOZU+UD5JoodwCAAoZQd3FeAKw==", + "dev": true + }, + "isexe": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", + "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", + "dev": true + }, + "istanbul-lib-coverage": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/istanbul-lib-coverage/-/istanbul-lib-coverage-3.2.0.tgz", + "integrity": "sha512-eOeJ5BHCmHYvQK7xt9GkdHuzuCGS1Y6g9Gvnx3Ym33fz/HpLRYxiS0wHNr+m/MBC8B647Xt608vCDEvhl9c6Mw==", + "dev": true + }, + "istanbul-lib-instrument": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/istanbul-lib-instrument/-/istanbul-lib-instrument-6.0.0.tgz", + "integrity": "sha512-x58orMzEVfzPUKqlbLd1hXCnySCxKdDKa6Rjg97CwuLLRI4g3FHTdnExu1OqffVFay6zeMW+T6/DowFLndWnIw==", + "dev": true, + "requires": { + "@babel/core": "^7.12.3", + "@babel/parser": "^7.14.7", + "@istanbuljs/schema": "^0.1.2", + "istanbul-lib-coverage": "^3.2.0", + "semver": "^7.5.4" + }, + "dependencies": { + "semver": { + "version": "7.5.4", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.5.4.tgz", + "integrity": "sha512-1bCSESV6Pv+i21Hvpxp3Dx+pSD8lIPt8uVjRrxAUt/nbswYc+tK6Y2btiULjd4+fnq15PX+nqQDC7Oft7WkwcA==", + "dev": true, + "requires": { + "lru-cache": "^6.0.0" + } + } + } + }, + "istanbul-lib-report": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/istanbul-lib-report/-/istanbul-lib-report-3.0.1.tgz", + "integrity": "sha512-GCfE1mtsHGOELCU8e/Z7YWzpmybrx/+dSTfLrvY8qRmaY6zXTKWn6WQIjaAFw069icm6GVMNkgu0NzI4iPZUNw==", + "dev": true, + "requires": { + "istanbul-lib-coverage": "^3.0.0", + "make-dir": "^4.0.0", + "supports-color": "^7.1.0" + }, + "dependencies": { + "has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true + }, + "supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "requires": { + "has-flag": "^4.0.0" + } + } + } + }, + "istanbul-lib-source-maps": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/istanbul-lib-source-maps/-/istanbul-lib-source-maps-4.0.1.tgz", + "integrity": "sha512-n3s8EwkdFIJCG3BPKBYvskgXGoy88ARzvegkitk60NxRdwltLOTaH7CUiMRXvwYorl0Q712iEjcWB+fK/MrWVw==", + "dev": true, + "requires": { + "debug": "^4.1.1", + "istanbul-lib-coverage": "^3.0.0", + "source-map": "^0.6.1" + } + }, + "istanbul-reports": { + "version": "3.1.5", + "resolved": "https://registry.npmjs.org/istanbul-reports/-/istanbul-reports-3.1.5.tgz", + "integrity": "sha512-nUsEMa9pBt/NOHqbcbeJEgqIlY/K7rVWUX6Lql2orY5e9roQOthbR3vtY4zzf2orPELg80fnxxk9zUyPlgwD1w==", + "dev": true, + "requires": { + "html-escaper": "^2.0.0", + "istanbul-lib-report": "^3.0.0" + } + }, + "jackspeak": { + "version": "2.3.6", + "resolved": "https://registry.npmjs.org/jackspeak/-/jackspeak-2.3.6.tgz", + "integrity": "sha512-N3yCS/NegsOBokc8GAdM8UcmfsKiSS8cipheD/nivzr700H+nsMOxJjQnvwOcRYVuFkdH0wGUvW2WbXGmrZGbQ==", + "dev": true, + "requires": { + "@isaacs/cliui": "^8.0.2", + "@pkgjs/parseargs": "^0.11.0" + } + }, + "js-beautify": { + "version": "1.14.9", + "resolved": "https://registry.npmjs.org/js-beautify/-/js-beautify-1.14.9.tgz", + "integrity": "sha512-coM7xq1syLcMyuVGyToxcj2AlzhkDjmfklL8r0JgJ7A76wyGMpJ1oA35mr4APdYNO/o/4YY8H54NQIJzhMbhBg==", + "dev": true, + "requires": { + "config-chain": "^1.1.13", + "editorconfig": "^1.0.3", + "glob": "^8.1.0", + "nopt": "^6.0.0" + }, + "dependencies": { + "brace-expansion": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", + "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", + "dev": true, + "requires": { + "balanced-match": "^1.0.0" + } + }, + "glob": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/glob/-/glob-8.1.0.tgz", + "integrity": "sha512-r8hpEjiQEYlF2QU0df3dS+nxxSIreXQS1qRhMJM0Q5NDdR386C7jb7Hwwod8Fgiuex+k0GFjgft18yvxm5XoCQ==", + "dev": true, + "requires": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^5.0.1", + "once": "^1.3.0" + } + }, + "minimatch": { + "version": "5.1.6", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-5.1.6.tgz", + "integrity": "sha512-lKwV/1brpG6mBUFHtb7NUmtABCb2WZZmm2wNiOA5hAb8VdCS4B3dtMWyvcoViccwAW/COERjXLt0zP1zXUN26g==", + "dev": true, + "requires": { + "brace-expansion": "^2.0.1" + } + } + } + }, + "js-tokens": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", + "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", + "dev": true + }, + "js-yaml": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz", + "integrity": "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==", + "dev": true, + "requires": { + "argparse": "^2.0.1" + } + }, + "jsdom": { + "version": "22.1.0", + "resolved": "https://registry.npmjs.org/jsdom/-/jsdom-22.1.0.tgz", + "integrity": "sha512-/9AVW7xNbsBv6GfWho4TTNjEo9fe6Zhf9O7s0Fhhr3u+awPwAJMKwAMXnkk5vBxflqLW9hTHX/0cs+P3gW+cQw==", + "dev": true, + "optional": true, + "peer": true, + "requires": { + "abab": "^2.0.6", + "cssstyle": "^3.0.0", + "data-urls": "^4.0.0", + "decimal.js": "^10.4.3", + "domexception": "^4.0.0", + "form-data": "^4.0.0", + "html-encoding-sniffer": "^3.0.0", + "http-proxy-agent": "^5.0.0", + "https-proxy-agent": "^5.0.1", + "is-potential-custom-element-name": "^1.0.1", + "nwsapi": "^2.2.4", + "parse5": "^7.1.2", + "rrweb-cssom": "^0.6.0", + "saxes": "^6.0.0", + "symbol-tree": "^3.2.4", + "tough-cookie": "^4.1.2", + "w3c-xmlserializer": "^4.0.0", + "webidl-conversions": "^7.0.0", + "whatwg-encoding": "^2.0.0", + "whatwg-mimetype": "^3.0.0", + "whatwg-url": "^12.0.1", + "ws": "^8.13.0", + "xml-name-validator": "^4.0.0" + }, + "dependencies": { + "tr46": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/tr46/-/tr46-4.1.1.tgz", + "integrity": "sha512-2lv/66T7e5yNyhAAC4NaKe5nVavzuGJQVVtRYLyQ2OI8tsJ61PMLlelehb0wi2Hx6+hT/OJUWZcw8MjlSRnxvw==", + "dev": true, + "optional": true, + "peer": true, + "requires": { + "punycode": "^2.3.0" + } + }, + "whatwg-url": { + "version": "12.0.1", + "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-12.0.1.tgz", + "integrity": "sha512-Ed/LrqB8EPlGxjS+TrsXcpUond1mhccS3pchLhzSgPCnTimUCKj3IZE75pAs5m6heB2U2TMerKFUXheyHY+VDQ==", + "dev": true, + "optional": true, + "peer": true, + "requires": { + "tr46": "^4.1.1", + "webidl-conversions": "^7.0.0" + } + } + } + }, + "jsesc": { + "version": "2.5.2", + "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-2.5.2.tgz", + "integrity": "sha512-OYu7XEzjkCQ3C5Ps3QIZsQfNpqoJyZZA99wd9aWd05NCtC5pWOkShK2mkL6HXQR6/Cy2lbNdPlZBpuQHXE63gA==", + "dev": true + }, + "json-schema-traverse": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", + "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", + "dev": true + }, + "json-stable-stringify-without-jsonify": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz", + "integrity": "sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==", + "dev": true + }, + "json5": { + "version": "2.2.3", + "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz", + "integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==", + "dev": true + }, + "jsonc-parser": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/jsonc-parser/-/jsonc-parser-3.2.0.tgz", + "integrity": "sha512-gfFQZrcTc8CnKXp6Y4/CBT3fTc0OVuDofpre4aEeEpSBPV5X5v4+Vmx+8snU7RLPrNHPKSgLxGo9YuQzz20o+w==", + "dev": true + }, + "jwt-decode": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/jwt-decode/-/jwt-decode-3.1.2.tgz", + "integrity": "sha512-UfpWE/VZn0iP50d8cz9NrZLM9lSWhcJ+0Gt/nm4by88UL+J1SiKN8/5dkjMmbEzwL2CAe+67GsegCbIKtbp75A==" + }, + "levn": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/levn/-/levn-0.4.1.tgz", + "integrity": "sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==", + "dev": true, + "requires": { + "prelude-ls": "^1.2.1", + "type-check": "~0.4.0" + } + }, + "local-pkg": { + "version": "0.4.3", + "resolved": "https://registry.npmjs.org/local-pkg/-/local-pkg-0.4.3.tgz", + "integrity": "sha512-SFppqq5p42fe2qcZQqqEOiVRXl+WCP1MdT6k7BDEW1j++sp5fIY+/fdRQitvKgB5BrBcmrs5m/L0v2FrU5MY1g==", + "dev": true + }, + "locate-path": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz", + "integrity": "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==", + "dev": true, + "requires": { + "p-locate": "^5.0.0" + } + }, + "lodash": { + "version": "4.17.21", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz", + "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==", + "dev": true + }, + "lodash.merge": { + "version": "4.6.2", + "resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz", + "integrity": "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==", + "dev": true + }, + "loupe": { + "version": "2.3.7", + "resolved": "https://registry.npmjs.org/loupe/-/loupe-2.3.7.tgz", + "integrity": "sha512-zSMINGVYkdpYSOBmLi0D1Uo7JU9nVdQKrHxC8eYlV+9YKK9WePqAlL7lSlorG/U2Fw1w0hTBmaa/jrQ3UbPHtA==", + "dev": true, + "requires": { + "get-func-name": "^2.0.1" + } + }, + "lru-cache": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", + "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==", + "dev": true, + "requires": { + "yallist": "^4.0.0" + } + }, + "lz-string": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/lz-string/-/lz-string-1.5.0.tgz", + "integrity": "sha512-h5bgJWpxJNswbU7qCrV0tIKQCaS3blPDrqKWx+QxzuzL1zGUzij9XCWLrSLsJPu5t+eWA/ycetzYAO5IOMcWAQ==", + "dev": true + }, + "magic-string": { + "version": "0.30.5", + "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.5.tgz", + "integrity": "sha512-7xlpfBaQaP/T6Vh8MO/EqXSW5En6INHEvEXQiuff7Gku0PWjU3uf6w/j9o7O+SpB5fOAkrI5HeoNgwjEO0pFsA==", + "requires": { + "@jridgewell/sourcemap-codec": "^1.4.15" + }, + "dependencies": { + "@jridgewell/sourcemap-codec": { + "version": "1.4.15", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.4.15.tgz", + "integrity": "sha512-eF2rxCRulEKXHTRiDrDy6erMYWqNw4LPdQ8UQA4huuxaQsVeRPFl2oM8oDGxMFhJUWZf9McpLtJasDDZb/Bpeg==" + } + } + }, + "make-dir": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-4.0.0.tgz", + "integrity": "sha512-hXdUTZYIVOt1Ex//jAQi+wTZZpUpwBj/0QsOzqegb3rGMMeJiSEu5xLHnYfBrRV4RH2+OCSOO95Is/7x1WJ4bw==", + "dev": true, + "requires": { + "semver": "^7.5.3" + }, + "dependencies": { + "semver": { + "version": "7.5.4", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.5.4.tgz", + "integrity": "sha512-1bCSESV6Pv+i21Hvpxp3Dx+pSD8lIPt8uVjRrxAUt/nbswYc+tK6Y2btiULjd4+fnq15PX+nqQDC7Oft7WkwcA==", + "dev": true, + "requires": { + "lru-cache": "^6.0.0" + } + } + } + }, + "make-error": { + "version": "1.3.6", + "resolved": "https://registry.npmjs.org/make-error/-/make-error-1.3.6.tgz", + "integrity": "sha512-s8UhlNe7vPKomQhC1qFelMokr/Sc3AgNbso3n74mVPA5LTZwkB9NlXf4XPamLxJE8h0gh73rM94xvwRT2CVInw==", + "dev": true + }, + "merge2": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz", + "integrity": "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==", + "dev": true + }, + "micromatch": { + "version": "4.0.5", + "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.5.tgz", + "integrity": "sha512-DMy+ERcEW2q8Z2Po+WNXuw3c5YaUSFjAO5GsJqfEl7UjvtIuFKO6ZrKvcItdy98dwFI2N1tg3zNIdKaQT+aNdA==", + "dev": true, + "requires": { + "braces": "^3.0.2", + "picomatch": "^2.3.1" + } + }, + "mime-db": { + "version": "1.52.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", + "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==" + }, + "mime-types": { + "version": "2.1.35", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", + "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", + "requires": { + "mime-db": "1.52.0" + } + }, + "minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "dev": true, + "requires": { + "brace-expansion": "^1.1.7" + } + }, + "minipass": { + "version": "7.0.4", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.0.4.tgz", + "integrity": "sha512-jYofLM5Dam9279rdkWzqHozUo4ybjdZmCsDHePy5V/PbBcVMiSZR97gmAy45aqi8CK1lG2ECd356FU86avfwUQ==", + "dev": true + }, + "mlly": { + "version": "1.4.2", + "resolved": "https://registry.npmjs.org/mlly/-/mlly-1.4.2.tgz", + "integrity": "sha512-i/Ykufi2t1EZ6NaPLdfnZk2AX8cs0d+mTzVKuPfqPKPatxLApaBoxJQ9x1/uckXtrS/U5oisPMDkNs0yQTaBRg==", + "dev": true, + "requires": { + "acorn": "^8.10.0", + "pathe": "^1.1.1", + "pkg-types": "^1.0.3", + "ufo": "^1.3.0" + } + }, + "ms": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", + "dev": true + }, + "muggle-string": { + "version": "0.3.1", + "resolved": "https://registry.npmjs.org/muggle-string/-/muggle-string-0.3.1.tgz", + "integrity": "sha512-ckmWDJjphvd/FvZawgygcUeQCxzvohjFO5RxTjj4eq8kw359gFF3E1brjfI+viLMxss5JrHTDRHZvu2/tuy0Qg==", + "dev": true + }, + "nanoid": { + "version": "3.3.6", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.6.tgz", + "integrity": "sha512-BGcqMMJuToF7i1rt+2PWSNVnWIkGCU78jBG3RxO/bZlnZPK2Cmi2QaffxGO/2RvWi9sL+FAiRiXMgsyxQ1DIDA==" + }, + "natural-compare": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz", + "integrity": "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==", + "dev": true + }, + "node-releases": { + "version": "2.0.13", + "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.13.tgz", + "integrity": "sha512-uYr7J37ae/ORWdZeQ1xxMJe3NtdmqMC/JZK+geofDrkLUApKRHPd18/TxtBOJ4A0/+uUIliorNrfYV6s1b02eQ==", + "dev": true + }, + "nopt": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/nopt/-/nopt-6.0.0.tgz", + "integrity": "sha512-ZwLpbTgdhuZUnZzjd7nb1ZV+4DoiC6/sfiVKok72ym/4Tlf+DFdlHYmT2JPmcNNWV6Pi3SDf1kT+A4r9RTuT9g==", + "dev": true, + "requires": { + "abbrev": "^1.0.0" + } + }, + "normalize-path": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", + "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==", + "dev": true + }, + "nth-check": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/nth-check/-/nth-check-2.1.1.tgz", + "integrity": "sha512-lqjrjmaOoAnWfMmBPL+XNnynZh2+swxiX3WUE0s4yEHI6m+AwrK2UZOimIRl3X/4QctVqS8AiZjFqyOGrMXb/w==", + "dev": true, + "requires": { + "boolbase": "^1.0.0" + } + }, + "nwsapi": { + "version": "2.2.4", + "resolved": "https://registry.npmjs.org/nwsapi/-/nwsapi-2.2.4.tgz", + "integrity": "sha512-NHj4rzRo0tQdijE9ZqAx6kYDcoRwYwSYzCA8MY3JzfxlrvEU0jhnhJT9BhqhJs7I/dKcrDm6TyulaRqZPIhN5g==", + "dev": true, + "optional": true, + "peer": true + }, + "object-inspect": { + "version": "1.13.1", + "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.1.tgz", + "integrity": "sha512-5qoj1RUiKOMsCCNLV1CBiPYE10sziTsnmNxkAI/rZhiD63CF7IqdFGC/XzjWjpSgLf0LxXX3bDFIh0E18f6UhQ==", + "dev": true + }, + "object-is": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/object-is/-/object-is-1.1.5.tgz", + "integrity": "sha512-3cyDsyHgtmi7I7DfSSI2LDp6SK2lwvtbg0p0R1e0RvTqF5ceGx+K2dfSjm1bKDMVCFEDAQvy+o8c6a7VujOddw==", + "dev": true, + "requires": { + "call-bind": "^1.0.2", + "define-properties": "^1.1.3" + } + }, + "object-keys": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/object-keys/-/object-keys-1.1.1.tgz", + "integrity": "sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA==", + "dev": true + }, + "object.assign": { + "version": "4.1.4", + "resolved": "https://registry.npmjs.org/object.assign/-/object.assign-4.1.4.tgz", + "integrity": "sha512-1mxKf0e58bvyjSCtKYY4sRe9itRk3PJpquJOjeIkz885CczcI4IvJJDLPS72oowuSh+pBxUFROpX+TU++hxhZQ==", + "dev": true, + "requires": { + "call-bind": "^1.0.2", + "define-properties": "^1.1.4", + "has-symbols": "^1.0.3", + "object-keys": "^1.1.1" + } + }, + "oidc-client-ts": { + "version": "2.4.0", + "resolved": "https://registry.npmjs.org/oidc-client-ts/-/oidc-client-ts-2.4.0.tgz", + "integrity": "sha512-WijhkTrlXK2VvgGoakWJiBdfIsVGz6CFzgjNNqZU1hPKV2kyeEaJgLs7RwuiSp2WhLfWBQuLvr2SxVlZnk3N1w==", + "requires": { + "crypto-js": "^4.2.0", + "jwt-decode": "^3.1.2" + } + }, + "once": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", + "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", + "dev": true, + "requires": { + "wrappy": "1" + } + }, + "optionator": { + "version": "0.9.3", + "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.3.tgz", + "integrity": "sha512-JjCoypp+jKn1ttEFExxhetCKeJt9zhAgAve5FXHixTvFDW/5aEktX9bufBKLRRMdU7bNtpLfcGu94B3cdEJgjg==", + "dev": true, + "requires": { + "@aashutoshrathi/word-wrap": "^1.2.3", + "deep-is": "^0.1.3", + "fast-levenshtein": "^2.0.6", + "levn": "^0.4.1", + "prelude-ls": "^1.2.1", + "type-check": "^0.4.0" + } + }, + "p-limit": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", + "integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==", + "dev": true, + "requires": { + "yocto-queue": "^0.1.0" + } + }, + "p-locate": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-5.0.0.tgz", + "integrity": "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==", + "dev": true, + "requires": { + "p-limit": "^3.0.2" + } + }, + "parent-module": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz", + "integrity": "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==", + "dev": true, + "requires": { + "callsites": "^3.0.0" + } + }, + "parse5": { + "version": "7.1.2", + "resolved": "https://registry.npmjs.org/parse5/-/parse5-7.1.2.tgz", + "integrity": "sha512-Czj1WaSVpaoj0wbhMzLmWD69anp2WH7FXMB9n1Sy8/ZFF9jolSQVMu1Ij5WIyGmcBmhk7EOndpO4mIpihVqAXw==", + "dev": true, + "optional": true, + "peer": true, + "requires": { + "entities": "^4.4.0" + } + }, + "path-exists": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", + "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", + "dev": true + }, + "path-is-absolute": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", + "integrity": "sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==", + "dev": true + }, + "path-key": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", + "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", + "dev": true + }, + "path-scurry": { + "version": "1.10.1", + "resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-1.10.1.tgz", + "integrity": "sha512-MkhCqzzBEpPvxxQ71Md0b1Kk51W01lrYvlMzSUaIzNsODdd7mqhiimSZlr+VegAz5Z6Vzt9Xg2ttE//XBhH3EQ==", + "dev": true, + "requires": { + "lru-cache": "^9.1.1 || ^10.0.0", + "minipass": "^5.0.0 || ^6.0.2 || ^7.0.0" + }, + "dependencies": { + "lru-cache": { + "version": "10.0.1", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.0.1.tgz", + "integrity": "sha512-IJ4uwUTi2qCccrioU6g9g/5rvvVl13bsdczUUcqbciD9iLr095yj8DQKdObriEvuNSx325N1rV1O0sJFszx75g==", + "dev": true + } + } + }, + "path-type": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-type/-/path-type-4.0.0.tgz", + "integrity": "sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==", + "dev": true + }, + "pathe": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/pathe/-/pathe-1.1.1.tgz", + "integrity": "sha512-d+RQGp0MAYTIaDBIMmOfMwz3E+LOZnxx1HZd5R18mmCZY0QBlK0LDZfPc8FW8Ed2DlvsuE6PRjroDY+wg4+j/Q==", + "dev": true + }, + "pathval": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/pathval/-/pathval-1.1.1.tgz", + "integrity": "sha512-Dp6zGqpTdETdR63lehJYPeIOqpiNBNtc7BpWSLrOje7UaIsE5aY92r/AunQA7rsXvet3lrJ3JnZX29UPTKXyKQ==", + "dev": true + }, + "picocolors": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.0.0.tgz", + "integrity": "sha512-1fygroTLlHu66zi26VoTDv8yRgm0Fccecssto+MhsZ0D/DGW2sm8E8AjW7NU5VVTRt5GxbeZ5qBuJr+HyLYkjQ==" + }, + "picomatch": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", + "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", + "dev": true + }, + "pinia": { + "version": "2.1.7", + "resolved": "https://registry.npmjs.org/pinia/-/pinia-2.1.7.tgz", + "integrity": "sha512-+C2AHFtcFqjPih0zpYuvof37SFxMQ7OEG2zV9jRI12i9BOy3YQVAHwdKtyyc8pDcDyIc33WCIsZaCFWU7WWxGQ==", + "requires": { + "@vue/devtools-api": "^6.5.0", + "vue-demi": ">=0.14.5" + }, + "dependencies": { + "vue-demi": { + "version": "0.14.5", + "resolved": "https://registry.npmjs.org/vue-demi/-/vue-demi-0.14.5.tgz", + "integrity": "sha512-o9NUVpl/YlsGJ7t+xuqJKx8EBGf1quRhCiT6D/J0pfwmk9zUwYkC7yrF4SZCe6fETvSM3UNL2edcbYrSyc4QHA==", + "requires": {} + } + } + }, + "pinia-plugin-persistedstate": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/pinia-plugin-persistedstate/-/pinia-plugin-persistedstate-3.2.0.tgz", + "integrity": "sha512-tZbNGf2vjAQcIm7alK40sE51Qu/m9oWr+rEgNm/2AWr1huFxj72CjvpQcIQzMknDBJEkQznCLAGtJTIcLKrKdw==", + "requires": {} + }, + "pkg-types": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/pkg-types/-/pkg-types-1.0.3.tgz", + "integrity": "sha512-nN7pYi0AQqJnoLPC9eHFQ8AcyaixBUOwvqc5TDnIKCMEE6I0y8P7OKA7fPexsXGCGxQDl/cmrLAp26LhcwxZ4A==", + "dev": true, + "requires": { + "jsonc-parser": "^3.2.0", + "mlly": "^1.2.0", + "pathe": "^1.1.0" + } + }, + "postcss": { + "version": "8.4.31", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.31.tgz", + "integrity": "sha512-PS08Iboia9mts/2ygV3eLpY5ghnUcfLV/EXTOW1E2qYxJKGGBUtNjN76FYHnMs36RmARn41bC0AZmn+rR0OVpQ==", + "requires": { + "nanoid": "^3.3.6", + "picocolors": "^1.0.0", + "source-map-js": "^1.0.2" + } + }, + "postcss-selector-parser": { + "version": "6.0.13", + "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-6.0.13.tgz", + "integrity": "sha512-EaV1Gl4mUEV4ddhDnv/xtj7sxwrwxdetHdWUGnT4VJQf+4d05v6lHYZr8N573k5Z0BViss7BDhfWtKS3+sfAqQ==", + "dev": true, + "requires": { + "cssesc": "^3.0.0", + "util-deprecate": "^1.0.2" + } + }, + "prelude-ls": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz", + "integrity": "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==", + "dev": true + }, + "prettier": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.0.3.tgz", + "integrity": "sha512-L/4pUDMxcNa8R/EthV08Zt42WBO4h1rarVtK0K+QJG0X187OLo7l699jWw0GKuwzkPQ//jMFA/8Xm6Fh3J/DAg==", + "dev": true + }, + "pretty-format": { + "version": "27.5.1", + "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-27.5.1.tgz", + "integrity": "sha512-Qb1gy5OrP5+zDf2Bvnzdl3jsTf1qXVMazbvCoKhtKqVs4/YK4ozX4gKQJJVyNe+cajNPn0KoC0MC3FUmaHWEmQ==", + "dev": true, + "requires": { + "ansi-regex": "^5.0.1", + "ansi-styles": "^5.0.0", + "react-is": "^17.0.1" + }, + "dependencies": { + "ansi-styles": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz", + "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==", + "dev": true + } + } + }, + "primeflex": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/primeflex/-/primeflex-3.3.1.tgz", + "integrity": "sha512-zaOq3YvcOYytbAmKv3zYc+0VNS9Wg5d37dfxZnveKBFPr7vEIwfV5ydrpiouTft8MVW6qNjfkaQphHSnvgQbpQ==" + }, + "primeicons": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/primeicons/-/primeicons-6.0.1.tgz", + "integrity": "sha512-KDeO94CbWI4pKsPnYpA1FPjo79EsY9I+M8ywoPBSf9XMXoe/0crjbUK7jcQEDHuc0ZMRIZsxH3TYLv4TUtHmAA==" + }, + "primevue": { + "version": "3.34.1", + "resolved": "https://registry.npmjs.org/primevue/-/primevue-3.34.1.tgz", + "integrity": "sha512-5QPy8I+TMYSQgC0Bs/9vINsOVjgCOQFAr6uz49Wzcj8u04qJ2mG/z6OhAana+f4yKTTHVwHLVnuGkrIp/nI9DA==", + "requires": {} + }, + "property-expr": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/property-expr/-/property-expr-2.0.5.tgz", + "integrity": "sha512-IJUkICM5dP5znhCckHSv30Q4b5/JA5enCtkRHYaOVOAocnH/1BQEYTC5NMfT3AVl/iXKdr3aqQbQn9DxyWknwA==" + }, + "proto-list": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/proto-list/-/proto-list-1.2.4.tgz", + "integrity": "sha512-vtK/94akxsTMhe0/cbfpR+syPuszcuwhqVjJq26CuNDgFGj682oRBXOP5MJpv2r7JtE8MsiepGIqvvOTBwn2vA==", + "dev": true + }, + "proxy-from-env": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz", + "integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==" + }, + "psl": { + "version": "1.9.0", + "resolved": "https://registry.npmjs.org/psl/-/psl-1.9.0.tgz", + "integrity": "sha512-E/ZsdU4HLs/68gYzgGTkMicWTLPdAftJLfJFlLUAAKZGkStNU72sZjT66SnMDVOfOWY/YAoiD7Jxa9iHvngcag==", + "dev": true, + "optional": true, + "peer": true + }, + "punycode": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.0.tgz", + "integrity": "sha512-rRV+zQD8tVFys26lAGR9WUuS4iUAngJScM+ZRSKtvl5tKeZ2t5bvdNFdNHBW9FWR4guGHlgmsZ1G7BSm2wTbuA==", + "dev": true + }, + "qrcode.vue": { + "version": "3.4.1", + "resolved": "https://registry.npmjs.org/qrcode.vue/-/qrcode.vue-3.4.1.tgz", + "integrity": "sha512-wq/zHsifH4FJ1GXQi8/wNxD1KfQkckIpjK1KPTc/qwYU5/Bkd4me0w4xZSg6EXk6xLBkVDE0zxVagewv5EMAVA==", + "requires": {} + }, + "querystringify": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/querystringify/-/querystringify-2.2.0.tgz", + "integrity": "sha512-FIqgj2EUvTa7R50u0rGsyTftzjYmv/a3hO345bZNrqabNqjtgiDMgmo4mkUjd+nzU5oF3dClKqFIPUKybUyqoQ==", + "dev": true, + "optional": true, + "peer": true + }, + "queue-microtask": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz", + "integrity": "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==", + "dev": true + }, + "react-is": { + "version": "17.0.2", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-17.0.2.tgz", + "integrity": "sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w==", + "dev": true + }, + "readdirp": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz", + "integrity": "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==", + "dev": true, + "requires": { + "picomatch": "^2.2.1" + } + }, + "regenerator-runtime": { + "version": "0.14.0", + "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.14.0.tgz", + "integrity": "sha512-srw17NI0TUWHuGa5CFGGmhfNIeja30WMBfbslPNhf6JrqQlLN5gcrvig1oqPxiVaXb0oW0XRKtH6Nngs5lKCIA==" + }, + "regexp.prototype.flags": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/regexp.prototype.flags/-/regexp.prototype.flags-1.5.1.tgz", + "integrity": "sha512-sy6TXMN+hnP/wMy+ISxg3krXx7BAtWVO4UouuCN/ziM9UEne0euamVNafDfvC83bRNr95y0V5iijeDQFUNpvrg==", + "dev": true, + "requires": { + "call-bind": "^1.0.2", + "define-properties": "^1.2.0", + "set-function-name": "^2.0.0" + } + }, + "require-directory": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", + "integrity": "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==", + "dev": true + }, + "requires-port": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/requires-port/-/requires-port-1.0.0.tgz", + "integrity": "sha512-KigOCHcocU3XODJxsu8i/j8T9tzT4adHiecwORRQ0ZZFcp7ahwXuRU1m+yuO90C5ZUyGeGfocHDI14M3L3yDAQ==", + "dev": true, + "optional": true, + "peer": true + }, + "resolve-from": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz", + "integrity": "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==", + "dev": true + }, + "reusify": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.0.4.tgz", + "integrity": "sha512-U9nH88a3fc/ekCF1l0/UP1IosiuIjyTh7hBvXVMHYgVcfGvt897Xguj2UOLDeI5BG2m7/uwyaLVT6fbtCwTyzw==", + "dev": true + }, + "rimraf": { + "version": "5.0.5", + "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-5.0.5.tgz", + "integrity": "sha512-CqDakW+hMe/Bz202FPEymy68P+G50RfMQK+Qo5YUqc9SPipvbGjCGKd0RSKEelbsfQuw3g5NZDSrlZZAJurH1A==", + "dev": true, + "requires": { + "glob": "^10.3.7" + }, + "dependencies": { + "brace-expansion": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", + "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", + "dev": true, + "requires": { + "balanced-match": "^1.0.0" + } + }, + "foreground-child": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/foreground-child/-/foreground-child-3.1.1.tgz", + "integrity": "sha512-TMKDUnIte6bfb5nWv7V/caI169OHgvwjb7V4WkeUvbQQdjr5rWKqHFiKWb/fcOwB+CzBT+qbWjvj+DVwRskpIg==", + "dev": true, + "requires": { + "cross-spawn": "^7.0.0", + "signal-exit": "^4.0.1" + } + }, + "glob": { + "version": "10.3.10", + "resolved": "https://registry.npmjs.org/glob/-/glob-10.3.10.tgz", + "integrity": "sha512-fa46+tv1Ak0UPK1TOy/pZrIybNNt4HCv7SDzwyfiOZkvZLEbjsZkJBPtDHVshZjbecAoAGSC20MjLDG/qr679g==", + "dev": true, + "requires": { + "foreground-child": "^3.1.0", + "jackspeak": "^2.3.5", + "minimatch": "^9.0.1", + "minipass": "^5.0.0 || ^6.0.2 || ^7.0.0", + "path-scurry": "^1.10.1" + } + }, + "minimatch": { + "version": "9.0.3", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.3.tgz", + "integrity": "sha512-RHiac9mvaRw0x3AYRgDC1CxAP7HTcNrrECeA8YYJeWnpo+2Q5CegtZjaotWTWxDG3UeGA1coE05iH1mPjT/2mg==", + "dev": true, + "requires": { + "brace-expansion": "^2.0.1" + } + }, + "signal-exit": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz", + "integrity": "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==", + "dev": true + } + } + }, + "rollup": { + "version": "3.28.1", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-3.28.1.tgz", + "integrity": "sha512-R9OMQmIHJm9znrU3m3cpE8uhN0fGdXiawME7aZIpQqvpS/85+Vt1Hq1/yVIcYfOmaQiHjvXkQAoJukvLpau6Yw==", + "dev": true, + "requires": { + "fsevents": "~2.3.2" + } + }, + "rrweb-cssom": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/rrweb-cssom/-/rrweb-cssom-0.6.0.tgz", + "integrity": "sha512-APM0Gt1KoXBz0iIkkdB/kfvGOwC4UuJFeG/c+yV7wSc7q96cG/kJ0HiYCnzivD9SB53cLV1MlHFNfOuPaadYSw==", + "dev": true, + "optional": true, + "peer": true + }, + "run-parallel": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz", + "integrity": "sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==", + "dev": true, + "requires": { + "queue-microtask": "^1.2.2" + } + }, + "safer-buffer": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", + "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", + "dev": true + }, + "sass": { + "version": "1.69.5", + "resolved": "https://registry.npmjs.org/sass/-/sass-1.69.5.tgz", + "integrity": "sha512-qg2+UCJibLr2LCVOt3OlPhr/dqVHWOa9XtZf2OjbLs/T4VPSJ00udtgJxH3neXZm+QqX8B+3cU7RaLqp1iVfcQ==", + "dev": true, + "requires": { + "chokidar": ">=3.0.0 <4.0.0", + "immutable": "^4.0.0", + "source-map-js": ">=0.6.2 <2.0.0" + } + }, + "saxes": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/saxes/-/saxes-6.0.0.tgz", + "integrity": "sha512-xAg7SOnEhrm5zI3puOOKyy1OMcMlIJZYNJY7xLBwSze0UjhPLnWfj2GF2EpT0jmzaJKIWKHLsaSSajf35bcYnA==", + "dev": true, + "optional": true, + "peer": true, + "requires": { + "xmlchars": "^2.2.0" + } + }, + "semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "dev": true + }, + "set-function-length": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/set-function-length/-/set-function-length-1.1.1.tgz", + "integrity": "sha512-VoaqjbBJKiWtg4yRcKBQ7g7wnGnLV3M8oLvVWwOk2PdYY6PEFegR1vezXR0tw6fZGF9csVakIRjrJiy2veSBFQ==", + "dev": true, + "requires": { + "define-data-property": "^1.1.1", + "get-intrinsic": "^1.2.1", + "gopd": "^1.0.1", + "has-property-descriptors": "^1.0.0" + } + }, + "set-function-name": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/set-function-name/-/set-function-name-2.0.1.tgz", + "integrity": "sha512-tMNCiqYVkXIZgc2Hnoy2IvC/f8ezc5koaRFkCjrpWzGpCd3qbZXPzVy9MAZzK1ch/X0jvSkojys3oqJN0qCmdA==", + "dev": true, + "requires": { + "define-data-property": "^1.0.1", + "functions-have-names": "^1.2.3", + "has-property-descriptors": "^1.0.0" + } + }, + "shebang-command": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", + "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", + "dev": true, + "requires": { + "shebang-regex": "^3.0.0" + } + }, + "shebang-regex": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", + "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", + "dev": true + }, + "side-channel": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.0.4.tgz", + "integrity": "sha512-q5XPytqFEIKHkGdiMIrY10mvLRvnQh42/+GoBlFW3b2LXLE2xxJpZFdm94we0BaoV3RwJyGqg5wS7epxTv0Zvw==", + "dev": true, + "requires": { + "call-bind": "^1.0.0", + "get-intrinsic": "^1.0.2", + "object-inspect": "^1.9.0" + } + }, + "siginfo": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/siginfo/-/siginfo-2.0.0.tgz", + "integrity": "sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g==", + "dev": true + }, + "signal-exit": { + "version": "3.0.7", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.7.tgz", + "integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==", + "dev": true + }, + "slash": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/slash/-/slash-3.0.0.tgz", + "integrity": "sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==", + "dev": true + }, + "source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "dev": true + }, + "source-map-js": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.0.2.tgz", + "integrity": "sha512-R0XvVJ9WusLiqTCEiGCmICCMplcCkIwwR11mOSD9CR5u+IXYdiseeEuXCVAjS54zqwkLcPNnmU4OeJ6tUrWhDw==" + }, + "stackback": { + "version": "0.0.2", + "resolved": "https://registry.npmjs.org/stackback/-/stackback-0.0.2.tgz", + "integrity": "sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw==", + "dev": true + }, + "std-env": { + "version": "3.4.3", + "resolved": "https://registry.npmjs.org/std-env/-/std-env-3.4.3.tgz", + "integrity": "sha512-f9aPhy8fYBuMN+sNfakZV18U39PbalgjXG3lLB9WkaYTxijru61wb57V9wxxNthXM5Sd88ETBWi29qLAsHO52Q==", + "dev": true + }, + "stop-iteration-iterator": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/stop-iteration-iterator/-/stop-iteration-iterator-1.0.0.tgz", + "integrity": "sha512-iCGQj+0l0HOdZ2AEeBADlsRC+vsnDsZsbdSiH1yNSjcfKM7fdpCMfqAL/dwF5BLiw/XhRft/Wax6zQbhq2BcjQ==", + "dev": true, + "requires": { + "internal-slot": "^1.0.4" + } + }, + "string-width": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-5.1.2.tgz", + "integrity": "sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==", + "dev": true, + "requires": { + "eastasianwidth": "^0.2.0", + "emoji-regex": "^9.2.2", + "strip-ansi": "^7.0.1" + }, + "dependencies": { + "ansi-regex": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.0.1.tgz", + "integrity": "sha512-n5M855fKb2SsfMIiFFoVrABHJC8QtHwVx+mHWP3QcEqBHYienj5dHSgjbxtC0WEZXYt4wcD6zrQElDPhFuZgfA==", + "dev": true + }, + "strip-ansi": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.0.tgz", + "integrity": "sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ==", + "dev": true, + "requires": { + "ansi-regex": "^6.0.1" + } + } + } + }, + "string-width-cjs": { + "version": "npm:string-width@4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "dev": true, + "requires": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "dependencies": { + "emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "dev": true + } + } + }, + "strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, + "requires": { + "ansi-regex": "^5.0.1" + } + }, + "strip-ansi-cjs": { + "version": "npm:strip-ansi@6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, + "requires": { + "ansi-regex": "^5.0.1" + } + }, + "strip-json-comments": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz", + "integrity": "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==", + "dev": true + }, + "strip-literal": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/strip-literal/-/strip-literal-1.0.1.tgz", + "integrity": "sha512-QZTsipNpa2Ppr6v1AmJHESqJ3Uz247MUS0OjrnnZjFAvEoWqxuyFuXn2xLgMtRnijJShAa1HL0gtJyUs7u7n3Q==", + "dev": true, + "requires": { + "acorn": "^8.8.2" + } + }, + "supports-color": { + "version": "5.5.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", + "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", + "dev": true, + "requires": { + "has-flag": "^3.0.0" + } + }, + "svg-tags": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/svg-tags/-/svg-tags-1.0.0.tgz", + "integrity": "sha512-ovssysQTa+luh7A5Weu3Rta6FJlFBBbInjOh722LIt6klpU2/HtdUbszju/G4devcvk8PGt7FCLv5wftu3THUA==", + "dev": true + }, + "symbol-tree": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/symbol-tree/-/symbol-tree-3.2.4.tgz", + "integrity": "sha512-9QNk5KwDF+Bvz+PyObkmSYjI5ksVUYtjW7AU22r2NKcfLJcXp96hkDWU3+XndOsUb+AQ9QhfzfCT2O+CNWT5Tw==", + "dev": true, + "optional": true, + "peer": true + }, + "test-exclude": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/test-exclude/-/test-exclude-6.0.0.tgz", + "integrity": "sha512-cAGWPIyOHU6zlmg88jwm7VRyXnMN7iV68OGAbYDk/Mh/xC/pzVPlQtY6ngoIH/5/tciuhGfvESU8GrHrcxD56w==", + "dev": true, + "requires": { + "@istanbuljs/schema": "^0.1.2", + "glob": "^7.1.4", + "minimatch": "^3.0.4" + } + }, + "text-table": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/text-table/-/text-table-0.2.0.tgz", + "integrity": "sha512-N+8UisAXDGk8PFXP4HAzVR9nbfmVJ3zYLAWiTIoqC5v5isinhr+r5uaO8+7r3BMfuNIufIsA7RdpVgacC2cSpw==", + "dev": true + }, + "tiny-case": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/tiny-case/-/tiny-case-1.0.3.tgz", + "integrity": "sha512-Eet/eeMhkO6TX8mnUteS9zgPbUMQa4I6Kkp5ORiBD5476/m+PIRiumP5tmh5ioJpH7k51Kehawy2UDfsnxxY8Q==" + }, + "tinybench": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/tinybench/-/tinybench-2.5.0.tgz", + "integrity": "sha512-kRwSG8Zx4tjF9ZiyH4bhaebu+EDz1BOx9hOigYHlUW4xxI/wKIUQUqo018UlU4ar6ATPBsaMrdbKZ+tmPdohFA==", + "dev": true + }, + "tinypool": { + "version": "0.7.0", + "resolved": "https://registry.npmjs.org/tinypool/-/tinypool-0.7.0.tgz", + "integrity": "sha512-zSYNUlYSMhJ6Zdou4cJwo/p7w5nmAH17GRfU/ui3ctvjXFErXXkruT4MWW6poDeXgCaIBlGLrfU6TbTXxyGMww==", + "dev": true + }, + "tinyspy": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/tinyspy/-/tinyspy-2.2.0.tgz", + "integrity": "sha512-d2eda04AN/cPOR89F7Xv5bK/jrQEhmcLFe6HFldoeO9AJtps+fqEnh486vnT/8y4bw38pSyxDcTCAq+Ks2aJTg==", + "dev": true + }, + "to-fast-properties": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/to-fast-properties/-/to-fast-properties-2.0.0.tgz", + "integrity": "sha512-/OaKK0xYrs3DmxRYqL/yDc+FxFUVYhDlXMhRmv3z915w2HF1tnN1omB354j8VUGO/hbRzyD6Y3sA7v7GS/ceog==", + "dev": true + }, + "to-regex-range": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", + "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", + "dev": true, + "requires": { + "is-number": "^7.0.0" + } + }, + "toposort": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/toposort/-/toposort-2.0.2.tgz", + "integrity": "sha512-0a5EOkAUp8D4moMi2W8ZF8jcga7BgZd91O/yabJCFY8az+XSzeGyTKs0Aoo897iV1Nj6guFq8orWDS96z91oGg==" + }, + "tough-cookie": { + "version": "4.1.3", + "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-4.1.3.tgz", + "integrity": "sha512-aX/y5pVRkfRnfmuX+OdbSdXvPe6ieKX/G2s7e98f4poJHnqH3281gDPm/metm6E/WRamfx7WC4HUqkWHfQHprw==", + "dev": true, + "optional": true, + "peer": true, + "requires": { + "psl": "^1.1.33", + "punycode": "^2.1.1", + "universalify": "^0.2.0", + "url-parse": "^1.5.3" + } + }, + "ts-api-utils": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-1.0.3.tgz", + "integrity": "sha512-wNMeqtMz5NtwpT/UZGY5alT+VoKdSsOOP/kqHFcUW1P/VRhH2wJ48+DN2WwUliNbQ976ETwDL0Ifd2VVvgonvg==", + "dev": true, + "requires": {} + }, + "ts-node": { + "version": "10.9.1", + "resolved": "https://registry.npmjs.org/ts-node/-/ts-node-10.9.1.tgz", + "integrity": "sha512-NtVysVPkxxrwFGUUxGYhfux8k78pQB3JqYBXlLRZgdGUqTO5wU/UyHop5p70iEbGhB7q5KmiZiU0Y3KlJrScEw==", + "dev": true, + "requires": { + "@cspotcode/source-map-support": "^0.8.0", + "@tsconfig/node10": "^1.0.7", + "@tsconfig/node12": "^1.0.7", + "@tsconfig/node14": "^1.0.0", + "@tsconfig/node16": "^1.0.2", + "acorn": "^8.4.1", + "acorn-walk": "^8.1.1", + "arg": "^4.1.0", + "create-require": "^1.1.0", + "diff": "^4.0.1", + "make-error": "^1.1.1", + "v8-compile-cache-lib": "^3.0.1", + "yn": "3.1.1" + } + }, + "type-check": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz", + "integrity": "sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==", + "dev": true, + "requires": { + "prelude-ls": "^1.2.1" + } + }, + "type-detect": { + "version": "4.0.8", + "resolved": "https://registry.npmjs.org/type-detect/-/type-detect-4.0.8.tgz", + "integrity": "sha512-0fr/mIH1dlO+x7TlcMy+bIDqKPsw/70tVyeHW787goQjhmqaZe10uwLujubK9q9Lg6Fiho1KUKDYz0Z7k7g5/g==", + "dev": true + }, + "type-fest": { + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.20.2.tgz", + "integrity": "sha512-Ne+eE4r0/iWnpAxD852z3A+N0Bt5RN//NjJwRd2VFHEmrywxf5vsZlh4R6lixl6B+wz/8d+maTSAkN1FIkI3LQ==", + "dev": true + }, + "typescript": { + "version": "5.2.2", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.2.2.tgz", + "integrity": "sha512-mI4WrpHsbCIcwT9cF4FZvr80QUeKvsUsUvKDoR+X/7XHQH98xYD8YHZg7ANtz2GtZt/CBq2QJ0thkGJMHfqc1w==", + "devOptional": true + }, + "ufo": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/ufo/-/ufo-1.3.1.tgz", + "integrity": "sha512-uY/99gMLIOlJPwATcMVYfqDSxUR9//AUcgZMzwfSTJPDKzA1S8mX4VLqa+fiAtveraQUBCz4FFcwVZBGbwBXIw==", + "dev": true + }, + "undici-types": { + "version": "5.26.5", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-5.26.5.tgz", + "integrity": "sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA==", + "dev": true + }, + "universalify": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/universalify/-/universalify-0.2.0.tgz", + "integrity": "sha512-CJ1QgKmNg3CwvAv/kOFmtnEN05f0D/cn9QntgNOQlQF9dgvVTHj3t+8JPdjqawCHk7V/KA+fbUqzZ9XWhcqPUg==", + "dev": true, + "optional": true, + "peer": true + }, + "update-browserslist-db": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.0.11.tgz", + "integrity": "sha512-dCwEFf0/oT85M1fHBg4F0jtLwJrutGoHSQXCh7u4o2t1drG+c0a9Flnqww6XUKSfQMPpJBRjU8d4RXB09qtvaA==", + "dev": true, + "requires": { + "escalade": "^3.1.1", + "picocolors": "^1.0.0" + } + }, + "uri-js": { + "version": "4.4.1", + "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz", + "integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==", + "dev": true, + "requires": { + "punycode": "^2.1.0" + } + }, + "url-parse": { + "version": "1.5.10", + "resolved": "https://registry.npmjs.org/url-parse/-/url-parse-1.5.10.tgz", + "integrity": "sha512-WypcfiRhfeUP9vvF0j6rw0J3hrWrw6iZv3+22h6iRMJ/8z1Tj6XfLP4DsUix5MhMPnXpiHDoKyoZ/bdCkwBCiQ==", + "dev": true, + "optional": true, + "peer": true, + "requires": { + "querystringify": "^2.1.1", + "requires-port": "^1.0.0" + } + }, + "util-deprecate": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", + "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==", + "dev": true + }, + "v8-compile-cache-lib": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/v8-compile-cache-lib/-/v8-compile-cache-lib-3.0.1.tgz", + "integrity": "sha512-wa7YjyUGfNZngI/vtK0UHAN+lgDCxBPCylVXGp0zu59Fz5aiGtNXaq3DhIov063MorB+VfufLh3JlF2KdTK3xg==", + "dev": true + }, + "v8-to-istanbul": { + "version": "9.1.0", + "resolved": "https://registry.npmjs.org/v8-to-istanbul/-/v8-to-istanbul-9.1.0.tgz", + "integrity": "sha512-6z3GW9x8G1gd+JIIgQQQxXuiJtCXeAjp6RaPEPLv62mH3iPHPxV6W3robxtCzNErRo6ZwTmzWhsbNvjyEBKzKA==", + "dev": true, + "requires": { + "@jridgewell/trace-mapping": "^0.3.12", + "@types/istanbul-lib-coverage": "^2.0.1", + "convert-source-map": "^1.6.0" + } + }, + "vee-validate": { + "version": "4.11.8", + "resolved": "https://registry.npmjs.org/vee-validate/-/vee-validate-4.11.8.tgz", + "integrity": "sha512-ZuVpw0axWYBM3aVTD/bm94hcWHumqeUgNjptOqfBT0gyqyHaGYCrm0tSD/0bygEbWUDwEPJOQaEKaUGM82j8TQ==", + "requires": { + "@vue/devtools-api": "^6.5.0", + "type-fest": "^4.3.1" + }, + "dependencies": { + "type-fest": { + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-4.3.1.tgz", + "integrity": "sha512-pphNW/msgOUSkJbH58x8sqpq8uQj6b0ZKGxEsLKMUnGorRcDjrUaLS+39+/ub41JNTwrrMyJcUB8+YZs3mbwqw==" + } + } + }, + "vite": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/vite/-/vite-4.5.0.tgz", + "integrity": "sha512-ulr8rNLA6rkyFAlVWw2q5YJ91v098AFQ2R0PRFwPzREXOUJQPtFUG0t+/ZikhaOCDqFoDhN6/v8Sq0o4araFAw==", + "dev": true, + "requires": { + "esbuild": "^0.18.10", + "fsevents": "~2.3.2", + "postcss": "^8.4.27", + "rollup": "^3.27.1" + } + }, + "vite-node": { + "version": "0.34.6", + "resolved": "https://registry.npmjs.org/vite-node/-/vite-node-0.34.6.tgz", + "integrity": "sha512-nlBMJ9x6n7/Amaz6F3zJ97EBwR2FkzhBRxF5e+jE6LA3yi6Wtc2lyTij1OnDMIr34v5g/tVQtsVAzhT0jc5ygA==", + "dev": true, + "requires": { + "cac": "^6.7.14", + "debug": "^4.3.4", + "mlly": "^1.4.0", + "pathe": "^1.1.1", + "picocolors": "^1.0.0", + "vite": "^3.0.0 || ^4.0.0 || ^5.0.0-0" + } + }, + "vitest": { + "version": "0.34.6", + "resolved": "https://registry.npmjs.org/vitest/-/vitest-0.34.6.tgz", + "integrity": "sha512-+5CALsOvbNKnS+ZHMXtuUC7nL8/7F1F2DnHGjSsszX8zCjWSSviphCb/NuS9Nzf4Q03KyyDRBAXhF/8lffME4Q==", + "dev": true, + "requires": { + "@types/chai": "^4.3.5", + "@types/chai-subset": "^1.3.3", + "@types/node": "*", + "@vitest/expect": "0.34.6", + "@vitest/runner": "0.34.6", + "@vitest/snapshot": "0.34.6", + "@vitest/spy": "0.34.6", + "@vitest/utils": "0.34.6", + "acorn": "^8.9.0", + "acorn-walk": "^8.2.0", + "cac": "^6.7.14", + "chai": "^4.3.10", + "debug": "^4.3.4", + "local-pkg": "^0.4.3", + "magic-string": "^0.30.1", + "pathe": "^1.1.1", + "picocolors": "^1.0.0", + "std-env": "^3.3.3", + "strip-literal": "^1.0.1", + "tinybench": "^2.5.0", + "tinypool": "^0.7.0", + "vite": "^3.1.0 || ^4.0.0 || ^5.0.0-0", + "vite-node": "0.34.6", + "why-is-node-running": "^2.2.2" + } + }, + "volar-service-eslint": { + "version": "0.0.16", + "resolved": "https://registry.npmjs.org/volar-service-eslint/-/volar-service-eslint-0.0.16.tgz", + "integrity": "sha512-S3zfl6zLKgGaammiwWRAxNlrH3SyyR06MKL94+7tq9VXTp83cTpMPaaMhrpdpccPZjM8RzwcfSg0LzV3b2mvfQ==", + "dev": true, + "requires": {} + }, + "vue": { + "version": "3.3.7", + "resolved": "https://registry.npmjs.org/vue/-/vue-3.3.7.tgz", + "integrity": "sha512-YEMDia1ZTv1TeBbnu6VybatmSteGOS3A3YgfINOfraCbf85wdKHzscD6HSS/vB4GAtI7sa1XPX7HcQaJ1l24zA==", + "requires": { + "@vue/compiler-dom": "3.3.7", + "@vue/compiler-sfc": "3.3.7", + "@vue/runtime-dom": "3.3.7", + "@vue/server-renderer": "3.3.7", + "@vue/shared": "3.3.7" + } + }, + "vue-component-type-helpers": { + "version": "1.8.4", + "resolved": "https://registry.npmjs.org/vue-component-type-helpers/-/vue-component-type-helpers-1.8.4.tgz", + "integrity": "sha512-6bnLkn8O0JJyiFSIF0EfCogzeqNXpnjJ0vW/SZzNHfe6sPx30lTtTXlE5TFs2qhJlAtDFybStVNpL73cPe3OMQ==", + "dev": true + }, + "vue-eslint-parser": { + "version": "9.3.1", + "resolved": "https://registry.npmjs.org/vue-eslint-parser/-/vue-eslint-parser-9.3.1.tgz", + "integrity": "sha512-Clr85iD2XFZ3lJ52/ppmUDG/spxQu6+MAeHXjjyI4I1NUYZ9xmenQp4N0oaHJhrA8OOxltCVxMRfANGa70vU0g==", + "dev": true, + "requires": { + "debug": "^4.3.4", + "eslint-scope": "^7.1.1", + "eslint-visitor-keys": "^3.3.0", + "espree": "^9.3.1", + "esquery": "^1.4.0", + "lodash": "^4.17.21", + "semver": "^7.3.6" + }, + "dependencies": { + "eslint-scope": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-7.1.1.tgz", + "integrity": "sha512-QKQM/UXpIiHcLqJ5AOyIW7XZmzjkzQXYE54n1++wb0u9V/abW3l9uQnxX8Z5Xd18xyKIMTUAyQ0k1e8pz6LUrw==", + "dev": true, + "requires": { + "esrecurse": "^4.3.0", + "estraverse": "^5.2.0" + } + }, + "estraverse": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz", + "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==", + "dev": true + }, + "semver": { + "version": "7.5.4", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.5.4.tgz", + "integrity": "sha512-1bCSESV6Pv+i21Hvpxp3Dx+pSD8lIPt8uVjRrxAUt/nbswYc+tK6Y2btiULjd4+fnq15PX+nqQDC7Oft7WkwcA==", + "dev": true, + "requires": { + "lru-cache": "^6.0.0" + } + } + } + }, + "vue-router": { + "version": "4.2.5", + "resolved": "https://registry.npmjs.org/vue-router/-/vue-router-4.2.5.tgz", + "integrity": "sha512-DIUpKcyg4+PTQKfFPX88UWhlagBEBEfJ5A8XDXRJLUnZOvcpMF8o/dnL90vpVkGaPbjvXazV/rC1qBKrZlFugw==", + "requires": { + "@vue/devtools-api": "^6.5.0" + } + }, + "vue-template-compiler": { + "version": "2.7.15", + "resolved": "https://registry.npmjs.org/vue-template-compiler/-/vue-template-compiler-2.7.15.tgz", + "integrity": "sha512-yQxjxMptBL7UAog00O8sANud99C6wJF+7kgbcwqkvA38vCGF7HWE66w0ZFnS/kX5gSoJr/PQ4/oS3Ne2pW37Og==", + "dev": true, + "requires": { + "de-indent": "^1.0.2", + "he": "^1.2.0" + } + }, + "vue-tsc": { + "version": "1.8.22", + "resolved": "https://registry.npmjs.org/vue-tsc/-/vue-tsc-1.8.22.tgz", + "integrity": "sha512-j9P4kHtW6eEE08aS5McFZE/ivmipXy0JzrnTgbomfABMaVKx37kNBw//irL3+LlE3kOo63XpnRigyPC3w7+z+A==", + "dev": true, + "requires": { + "@volar/typescript": "~1.10.5", + "@vue/language-core": "1.8.22", + "semver": "^7.5.4" + }, + "dependencies": { + "semver": { + "version": "7.5.4", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.5.4.tgz", + "integrity": "sha512-1bCSESV6Pv+i21Hvpxp3Dx+pSD8lIPt8uVjRrxAUt/nbswYc+tK6Y2btiULjd4+fnq15PX+nqQDC7Oft7WkwcA==", + "dev": true, + "requires": { + "lru-cache": "^6.0.0" + } + } + } + }, + "w3c-xmlserializer": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/w3c-xmlserializer/-/w3c-xmlserializer-4.0.0.tgz", + "integrity": "sha512-d+BFHzbiCx6zGfz0HyQ6Rg69w9k19nviJspaj4yNscGjrHu94sVP+aRm75yEbCh+r2/yR+7q6hux9LVtbuTGBw==", + "dev": true, + "optional": true, + "peer": true, + "requires": { + "xml-name-validator": "^4.0.0" + } + }, + "webidl-conversions": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-7.0.0.tgz", + "integrity": "sha512-VwddBukDzu71offAQR975unBIGqfKZpM+8ZX6ySk8nYhVoo5CYaZyzt3YBvYtRtO+aoGlqxPg/B87NGVZ/fu6g==", + "dev": true + }, + "whatwg-encoding": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/whatwg-encoding/-/whatwg-encoding-2.0.0.tgz", + "integrity": "sha512-p41ogyeMUrw3jWclHWTQg1k05DSVXPLcVxRTYsXUk+ZooOCZLcoYgPZ/HL/D/N+uQPOtcp1me1WhBEaX02mhWg==", + "dev": true, + "requires": { + "iconv-lite": "0.6.3" + } + }, + "whatwg-mimetype": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/whatwg-mimetype/-/whatwg-mimetype-3.0.0.tgz", + "integrity": "sha512-nt+N2dzIutVRxARx1nghPKGv1xHikU7HKdfafKkLNLindmPU/ch3U31NOCGGA/dmPcmb1VlofO0vnKAcsm0o/Q==", + "dev": true + }, + "which": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", + "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", + "dev": true, + "requires": { + "isexe": "^2.0.0" + } + }, + "which-boxed-primitive": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/which-boxed-primitive/-/which-boxed-primitive-1.0.2.tgz", + "integrity": "sha512-bwZdv0AKLpplFY2KZRX6TvyuN7ojjr7lwkg6ml0roIy9YeuSr7JS372qlNW18UQYzgYK9ziGcerWqZOmEn9VNg==", + "dev": true, + "requires": { + "is-bigint": "^1.0.1", + "is-boolean-object": "^1.1.0", + "is-number-object": "^1.0.4", + "is-string": "^1.0.5", + "is-symbol": "^1.0.3" + } + }, + "which-collection": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/which-collection/-/which-collection-1.0.1.tgz", + "integrity": "sha512-W8xeTUwaln8i3K/cY1nGXzdnVZlidBcagyNFtBdD5kxnb4TvGKR7FfSIS3mYpwWS1QUCutfKz8IY8RjftB0+1A==", + "dev": true, + "requires": { + "is-map": "^2.0.1", + "is-set": "^2.0.1", + "is-weakmap": "^2.0.1", + "is-weakset": "^2.0.1" + } + }, + "which-typed-array": { + "version": "1.1.13", + "resolved": "https://registry.npmjs.org/which-typed-array/-/which-typed-array-1.1.13.tgz", + "integrity": "sha512-P5Nra0qjSncduVPEAr7xhoF5guty49ArDTwzJ/yNuPIbZppyRxFQsRCWrocxIY+CnMVG+qfbU2FmDKyvSGClow==", + "dev": true, + "requires": { + "available-typed-arrays": "^1.0.5", + "call-bind": "^1.0.4", + "for-each": "^0.3.3", + "gopd": "^1.0.1", + "has-tostringtag": "^1.0.0" + } + }, + "why-is-node-running": { + "version": "2.2.2", + "resolved": "https://registry.npmjs.org/why-is-node-running/-/why-is-node-running-2.2.2.tgz", + "integrity": "sha512-6tSwToZxTOcotxHeA+qGCq1mVzKR3CwcJGmVcY+QE8SHy6TnpFnh8PAvPNHYr7EcuVeG0QSMxtYCuO1ta/G/oA==", + "dev": true, + "requires": { + "siginfo": "^2.0.0", + "stackback": "0.0.2" + } + }, + "wrap-ansi": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", + "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", + "dev": true, + "requires": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "dependencies": { + "ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "requires": { + "color-convert": "^2.0.1" + } + }, + "color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "requires": { + "color-name": "~1.1.4" + } + }, + "color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true + }, + "emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "dev": true + }, + "string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "dev": true, + "requires": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + } + } + } + }, + "wrap-ansi-cjs": { + "version": "npm:wrap-ansi@7.0.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", + "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", + "dev": true, + "requires": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "dependencies": { + "ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "requires": { + "color-convert": "^2.0.1" + } + }, + "color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "requires": { + "color-name": "~1.1.4" + } + }, + "color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true + }, + "emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "dev": true + }, + "string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "dev": true, + "requires": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + } + } + } + }, + "wrappy": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", + "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", + "dev": true + }, + "ws": { + "version": "8.13.0", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.13.0.tgz", + "integrity": "sha512-x9vcZYTrFPC7aSIbj7sRCYo7L/Xb8Iy+pW0ng0wt2vCJv7M9HOMy0UoN3rr+IFC7hb7vXoqS+P9ktyLLLhO+LA==", + "dev": true, + "optional": true, + "peer": true, + "requires": {} + }, + "xml-name-validator": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/xml-name-validator/-/xml-name-validator-4.0.0.tgz", + "integrity": "sha512-ICP2e+jsHvAj2E2lIHxa5tjXRlKDJo4IdvPvCXbXQGdzSfmSpNVyIKMvoZHjDY9DP0zV17iI85o90vRFXNccRw==", + "dev": true + }, + "xmlchars": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/xmlchars/-/xmlchars-2.2.0.tgz", + "integrity": "sha512-JZnDKK8B0RCDw84FNdDAIpZK+JuJw+s7Lz8nksI7SIuU3UXJJslUthsi+uWBUYOwPFwW7W7PRLRfUKpxjtjFCw==", + "dev": true, + "optional": true, + "peer": true + }, + "y18n": { + "version": "5.0.8", + "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz", + "integrity": "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==", + "dev": true + }, + "yallist": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", + "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", + "dev": true + }, + "yargs": { + "version": "16.2.0", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-16.2.0.tgz", + "integrity": "sha512-D1mvvtDG0L5ft/jGWkLpG1+m0eQxOfaBvTNELraWj22wSVUMWxZUvYgJYcKh6jGGIkJFhH4IZPQhR4TKpc8mBw==", + "dev": true, + "requires": { + "cliui": "^7.0.2", + "escalade": "^3.1.1", + "get-caller-file": "^2.0.5", + "require-directory": "^2.1.1", + "string-width": "^4.2.0", + "y18n": "^5.0.5", + "yargs-parser": "^20.2.2" + }, + "dependencies": { + "emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "dev": true + }, + "string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "dev": true, + "requires": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + } + } + } + }, + "yargs-parser": { + "version": "20.2.9", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-20.2.9.tgz", + "integrity": "sha512-y11nGElTIV+CT3Zv9t7VKl+Q3hTQoT9a1Qzezhhl6Rp21gJ/IVTW7Z3y9EWXhuUBC2Shnf+DX0antecpAwSP8w==", + "dev": true + }, + "yn": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/yn/-/yn-3.1.1.tgz", + "integrity": "sha512-Ux4ygGWsu2c7isFWe8Yu1YluJmqVhxqK2cLXNQA5AcC3QfbGNpM7fu0Y8b/z16pXLnFxZYvWhd3fhBY9DLmC6Q==", + "dev": true + }, + "yocto-queue": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", + "integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==", + "dev": true + }, + "yup": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/yup/-/yup-1.3.2.tgz", + "integrity": "sha512-6KCM971iQtJ+/KUaHdrhVr2LDkfhBtFPRnsG1P8F4q3uUVQ2RfEM9xekpha9aA4GXWJevjM10eDcPQ1FfWlmaQ==", + "requires": { + "property-expr": "^2.0.5", + "tiny-case": "^1.0.3", + "toposort": "^2.0.2", + "type-fest": "^2.19.0" + }, + "dependencies": { + "type-fest": { + "version": "2.19.0", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-2.19.0.tgz", + "integrity": "sha512-RAH822pAdBgcNMAfWnCBU3CFZcfZ/i1eZjwFU/dsLKumyuuP3niueg2UAukXYF0E2AAoc82ZSSf9J0WQBinzHA==" + } + } + } + } +} diff --git a/bcbox/frontend/package.json b/bcbox/frontend/package.json new file mode 100644 index 00000000..41137823 --- /dev/null +++ b/bcbox/frontend/package.json @@ -0,0 +1,73 @@ +{ + "name": "bcbox-frontend", + "version": "0.5.0", + "private": true, + "description": "", + "author": "NR Common Service Showcase ", + "license": "Apache-2.0", + "scripts": { + "build": "vite build", + "build:dts": "vue-tsc --declaration --emitDeclarationOnly", + "clean": "rimraf coverage dist", + "debug": "vite --mode debug", + "format": "prettier ./src --write", + "lint": "eslint . --ext .vue,.js,.jsx,.cjs,.mjs,.ts,.tsx,.cts,.mts --no-fix --ignore-path .gitignore", + "preview": "vite preview", + "prebuild": "npm run lint", + "prelint": "npm run typecheck", + "pretest": "npm run lint", + "purge": "rimraf node_modules", + "rebuild": "npm run clean && npm run build", + "reinstall": "npm run purge && npm install", + "serve": "vite", + "test": "vitest run --coverage", + "typecheck": "vue-tsc --noEmit" + }, + "dependencies": { + "@bcgov/bc-sans": "^2.1.0", + "@fortawesome/fontawesome-svg-core": "^6.4.2", + "@fortawesome/free-solid-svg-icons": "^6.4.2", + "@fortawesome/vue-fontawesome": "^3.0.3", + "axios": "^1.6.0", + "date-fns": "^2.30.0", + "filesize": "^10.1.0", + "oidc-client-ts": "^2.4.0", + "pinia": "^2.1.7", + "pinia-plugin-persistedstate": "^3.2.0", + "primeflex": "^3.3.1", + "primeicons": "^6.0.1", + "primevue": "~3.34.1", + "qrcode.vue": "^3.4.1", + "vee-validate": "^4.11.8", + "vue": "^3.3.7", + "vue-router": "^4.2.5", + "yup": "^1.3.2" + }, + "devDependencies": { + "@pinia/testing": "^0.1.3", + "@testing-library/vue": "^8.0.0", + "@tsconfig/node18": "^18.2.2", + "@types/node": "^20.8.10", + "@vitejs/plugin-vue": "^4.4.0", + "@vitejs/plugin-vue-jsx": "^3.0.2", + "@vitest/coverage-c8": "^0.33.0", + "@vitest/coverage-istanbul": "^0.34.6", + "@vue/eslint-config-typescript": "^12.0.0", + "@vue/test-utils": "^2.4.1", + "@vue/tsconfig": "^0.4.0", + "eslint": "^8.52.0", + "eslint-config-prettier": "^9.0.0", + "eslint-plugin-vitest-globals": "^1.4.0", + "eslint-plugin-vue": "^9.18.1", + "happy-dom": "^12.10.3", + "prettier": "3.0.3", + "rimraf": "^5.0.5", + "sass": "^1.69.5", + "ts-node": "^10.9.1", + "typescript": "^5.2.2", + "vite": "^4.5.0", + "vitest": "^0.34.6", + "volar-service-eslint": "^0.0.16", + "vue-tsc": "^1.8.22" + } +} diff --git a/bcbox/frontend/public/favicon.ico b/bcbox/frontend/public/favicon.ico new file mode 100644 index 0000000000000000000000000000000000000000..1841396d21ce974cdec16f7414c88683cd36af16 GIT binary patch literal 22486 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 C+3a-y literal 0 HcmV?d00001 diff --git a/bcbox/frontend/src/App.vue b/bcbox/frontend/src/App.vue new file mode 100644 index 00000000..1400ec50 --- /dev/null +++ b/bcbox/frontend/src/App.vue @@ -0,0 +1,58 @@ + + + diff --git a/bcbox/frontend/src/assets/base.css b/bcbox/frontend/src/assets/base.css new file mode 100644 index 00000000..e910a752 --- /dev/null +++ b/bcbox/frontend/src/assets/base.css @@ -0,0 +1,86 @@ +/* color palette from */ +:root { + --vt-c-white: #ffffff; + --vt-c-white-soft: #f8f8f8; + --vt-c-white-mute: #f2f2f2; + + --vt-c-black: #181818; + --vt-c-black-soft: #222222; + --vt-c-black-mute: #282828; + + --vt-c-indigo: #2c3e50; + + --vt-c-divider-light-1: rgba(60, 60, 60, 0.29); + --vt-c-divider-light-2: rgba(60, 60, 60, 0.12); + --vt-c-divider-dark-1: rgba(84, 84, 84, 0.65); + --vt-c-divider-dark-2: rgba(84, 84, 84, 0.48); + + --vt-c-text-light-1: var(--vt-c-indigo); + --vt-c-text-light-2: rgba(60, 60, 60, 0.66); + --vt-c-text-dark-1: var(--vt-c-white); + --vt-c-text-dark-2: rgba(235, 235, 235, 0.64); +} + +/* semantic color variables for this project */ +:root { + --color-background: var(--vt-c-white); + --color-background-soft: var(--vt-c-white-soft); + --color-background-mute: var(--vt-c-white-mute); + + --color-border: var(--vt-c-divider-light-2); + --color-border-hover: var(--vt-c-divider-light-1); + + --color-heading: var(--vt-c-text-light-1); + --color-text: var(--vt-c-text-light-1); + + --section-gap: 160px; +} + +/* @media (prefers-color-scheme: dark) { + :root { + --color-background: var(--vt-c-black); + --color-background-soft: var(--vt-c-black-soft); + --color-background-mute: var(--vt-c-black-mute); + + --color-border: var(--vt-c-divider-dark-2); + --color-border-hover: var(--vt-c-divider-dark-1); + + --color-heading: var(--vt-c-text-dark-1); + --color-text: var(--vt-c-text-dark-2); + } +} */ + +*, +*::before, +*::after { + box-sizing: border-box; + margin: 0; + font-weight: normal; +} + +body { + min-height: 100vh; + color: var(--color-text); + background: var(--color-background); + transition: + color 0.5s, + background-color 0.5s; + line-height: 1.6; + font-family: + BCSans, + Inter, + -apple-system, + BlinkMacSystemFont, + 'Segoe UI', + Roboto, + Oxygen, + Ubuntu, + Cantarell, + 'Fira Sans', + 'Droid Sans', + 'Helvetica Neue', + sans-serif; + text-rendering: optimizeLegibility; + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; +} diff --git a/bcbox/frontend/src/assets/images/bc_logo.svg b/bcbox/frontend/src/assets/images/bc_logo.svg new file mode 100644 index 00000000..5ba3bb5d --- /dev/null +++ b/bcbox/frontend/src/assets/images/bc_logo.svg @@ -0,0 +1 @@ +bc_logo diff --git a/bcbox/frontend/src/assets/images/bc_logo_print.svg b/bcbox/frontend/src/assets/images/bc_logo_print.svg new file mode 100644 index 00000000..aae06ff7 --- /dev/null +++ b/bcbox/frontend/src/assets/images/bc_logo_print.svg @@ -0,0 +1 @@ +bc_logo_print diff --git a/bcbox/frontend/src/assets/images/bc_logo_square.svg b/bcbox/frontend/src/assets/images/bc_logo_square.svg new file mode 100644 index 00000000..8d228dc8 --- /dev/null +++ b/bcbox/frontend/src/assets/images/bc_logo_square.svg @@ -0,0 +1,22 @@ + + + + + + + + + + + + + + + + + + + + + + diff --git a/bcbox/frontend/src/assets/images/bcboxy.png b/bcbox/frontend/src/assets/images/bcboxy.png new file mode 100644 index 0000000000000000000000000000000000000000..3a450669ec6c426846ba86b0ee61a39266031e51 GIT binary patch literal 29806 zcmeEtRX|+9(&pecxVwb}0)rERTW|=L5EvXn7$8W{!CitwU?2n=+;wnw5AJTk-8D<@ z-G86Kt; zFaQ+aNqvAK9j59Pf9ZFgxxYBPxVUh-UTm<)T}pEqS(+eTpu|6&)!FDDcCnRfZ;v8H zf)UwEGsM~xAd{#t$zsNZWEf{Bkwc{vFbR-#G2sNziaM+=6t}P#0&L9h3Zd8dk0a3G z$$~2FC6|wfh0TBEg$tXnYq^|ttEcVCz#s1yt?`$|Ptu(wDKmiT=&UI5NCRy}cNzY_js73tXub;Qd6J+;N9S?y zdu=Vmj?Q+u&KbJ3rB=S8>3XdB54#Tr*iUJ=41?n20A!u0=rX$BsUog>f6unqQ7QJ0 zmv-%>ae};ErPZ>;633PuJ#^dFk%zj&6S24^HyXz_0?DxNZ3DeVdZNwKaV9^%#at#K zSC?K${cE7M6OCdwg~-GE?@A~ zBs1k{^CnC8NrN1z9EF@K*QHqk@#R%tb+5{aWU5Ji9|jq@;i_}J{o49Z4+1>3PWTkM zsQ4!_!sk_P6r}k8-c@2Jf3L4TR#Ra-AMj~qJ{6cdodv|GkIA&@EXthXTH`jTDZu!X z&;INf=C0@g0|hEVCZy9kLqT8On)R+)&T9S2pfg)0ouu@YK_S_v0Q44G56P|DYy4b1 zR(xZ@%KjX3D%k^gWb$a(qLak$TSUrIRN6s5$C!d=s5%Ik8!-Z~j|YCJiF@!aGH=yr za!|M)FNaiSpmo~JbI0BTAMkwFEC86?%)38Rn*PPOK(c|hMqb}pEZ`jAjlS8Z%0ueq z*@#3CwH81*EODTWZ?MReHo6^-U~VyqH)DlzGFp&48*9{9!|hN9U0)3wm`hPT&d-9r zQLu15ybXZWAl}XsN3W_%_;Q&6y>dAyPnjQVl|3%S1GWTEWFeO_$$|+rl`KD zjWd!CH!asBjlQ}U1G9iEd_D2*X7n(htuhZHY7>M7#+W;Vcg`2r-in7L<5 zv&m6<@oaS?Er$_)Y02Isz4Y?*y!;%>o`dI}LX1MbNol?T=A6`J-kV>NQ7&BXM~WY< z*D3NF@Eu^t1~Slxy45-FFZY+rSc-;mg+LdMNgwLFZZZ$pd|#eSE$}gL30JsCd6f}1 z#%W%&t@^*&F{p)mx3A<2GNLpFld;mXiW}88-e*Gbuzb`}*~}JD$z59JJ4H)}8Wv1v zJ$mx9DTd=6O|@K>9M-q8Li~b$hk}S+2{Y$JSf`f6NML)f14{EUD*I5)m1U7L% z!=)w7`i;VPSHt4(wa~etCu!MGLeGuZDha;)8#$V93`vEl;3wD#5egZC-6_~8c~!{WmM8Ccqo?E4895?MWZ>`g^2MJ;1pvud}mWryo=i#swqkDCI|P!{U% zt2bQ*GIE+BzKO}5<#nI>I+?hHn)h@5oJ&uPtcm$^XsOv~sSQt-HU^RbaB|hd{2jHL zK2AQMH0jt)i!ILaVQn0^Ifr;kqyJ8*owT8dq+HjqqS81t6q=|XJGMeS=n;2dD;~i78Zkox#=giVhVORlJ zIgyNXo*xKgJkRX>3ReCU-22k?VMv%(DKsub%6K7*|55GrM%l)DzVvGj~2JtC=2otnMkS}LE zzA384514VtnPZW&uCdlyL$krCQ!%(8q|)yosM9f*_sXzSca4NM@^`ju znLQgs6-(-8^+Q&zc{3g!7giHlvf#UOU&}L%bBZ&mFmkE7aCI zbS?|}kq~(_871z*z=XTC1DOZ^S>n-+Dj^%?H()&AX$BdL_$F-rYg)*$!FAgxya?_mKR7Kj#(wwmee*_3mj zxC4tPr6Zwu(T4TKw;LE?{T24LQx%WGU!$`Mo)n7@6tn5(wke_EPAOQ8drcivTQa3( z<;Jit^oZLDxo9I$fbDo3f~A!$AZr8~hOdIjvFS)yTj$aH1U{&jbH*Fj#Ypl-ZNHc* z2X9XSlA60Jb(~py$LJkb(HK_^d$XL0CS1{@oBA8Z`jM~Fbi zrXlSIQE5INL1Yv}T>-1ZABzH5dRI$(PVkQKS+H5hBPgT0BS(jvKcHy8!(+^=%pDwZ z9%ny3kwZEou+H7pE%+os>RWI-hvwhT%S}=LW@f(@R@NGBb$Pzn@9JU zoq#f86wKRJ6$|JD!?V+YM6o=5KgbxkT2X?ln@)6eo*cxueaXAxrbY#uFyQ8V{x8D% zR6US!(ze9)YHFAJrS}aLXd!HQgdw<{)3_FcChyw9pqmJQ+b6o+Z)}O3`q1{UNxhj6 z4kMutV-pTX`gULDSP+5BpG|1GWFr`wOQ=m@#|WgaYhvI0WNHi)P8R^*)G8Qpfl^E_ z@oq26wx4$2Ozp`4_|2gdHrJ+~Qcxoyy7Pq~~2&6qFenlC-LAwS^faR=}A=!|B?E&#-Sp{g~Qd6WLgt zrsP z4AotF>L+6dCr42Gl%sY?a_{|<45Y#Z_$T;?X1p9@Y@9lpoto6!m<^7c<9$G^r^gdi znqYUehsw<(X1j5t>iBP*m!HY2B8wA63OsgHcH)|MRAJ|0+zX_L)kuZFW!0HrSaXT0 zudlo++t`Ap+UYix`MxJ`p*_@CM~iyi@m}NiRWcn(Q89c2x>oP7m0T#1y%>1peuhI! zSY$UTvKdzuvCOyQ;rr`ZtuL&n!IS4|Bq_g+q(&cjrP~{w85S5j+gJ7|(ONn6db1-A zWKZ{*5cFqtedV${?oY5+oxFnz6HA$VF+_$}D4dP{WS{!XsZ%qz(?w@gy#iViy=w73 z|56fDE11bIBjNH#sBtV|r|4W#g*(D)%OfAa%eqE%EN|Vpgq@oDa8a~ zFoFZmVXoHU5qNeyPo3e9Zz_i~dW5lCWkmTeQKW33O~zh!)fEm4Y|43H>8wSmWPZ*V3=B6En0Gri21C1)``YbXnsrQ%m z(p1$ZaND>*dST!)KkpS?Yf%r$zj4Eq8xvF+%RRpDz#xD$>M4Zu3v}qR&@zUkAv^#B~$Jij%EnE{02D!MD zZC#z!d`H`BKpHP_|GpiGA@aPA!`#w85l;dB<7tt)9#gKF7X=JxPr%9i~%{m9uM_NOnX%~nVG#_F*$|r)YXP(fwkU%tOfD;n)|C2b~`m zx<@q@e}9?+z0ZT{XD?IcduWqKKV&wsLxkW6$L-$~-(jPf_4m8xV<`%BUn?j1IqOsO}UD8790mN3z~2BZCez*x`JrN-ZHa6ka`x;4iL ztltTz!&D4r-BlO1n?KVmB4s<1%1UU=A$fA6#bw+)5utDMst5bFuTfq$Jz976mVPBL z+^fcL8azFfX*X&3>9l2dE-ZkE_x)Z(pjoSIBB!O0q*4CKL>W{opfWkJfIR0Q#wH{q zQs97g>#MP+%PFlW#;lXYsnv#dwz-!5 zV&0}R;_EE(O_q&uQCcfN3T#u5IM}S|6}D{?Ix3jbKXe}lt&dI zOr_kw3kfuxj&*I+QY^BzS{nxM@Q`NNu9xnGpp>z~6Pi#ROb$l?&euNvG?<33Y#<#v z0Uxrok3;FNKNLsJ>HE{azUI9)MoJ*-6pkaO!0iGtlA(<#4^xjYcLjcCz%bl##u|*P zrmDX%=@<|#XS%Ydg0#=JVuEd7zc@th5s}jU6n^|&v<|zox%R8|ZG4yo)iDS7F(_V) zgk-U;L^4Q5!~})Viuws=Hg zLMW@MRy(@g;(R)6WDM1K^8s?1$7b%AFfSucfsV>C@xx)B0AkZ-cDRSh!^PIQzW!*g zxA<1!Aj@2^%P^w&Z;~Kwey$(cu&?4tJUUPyI;SoCN)$9g^3H&u+I?&fA6u7%C?4VR z{x=UR7v-P!WOjRU$JBTi^d$7^CR;7C`~cBouVns<0J1(VcfS=+<(c+mL&N)7{%0?c zHB(JU^WnB-y74aTuD#hd_BlkSKfDz{dRJsevr+!O9{%EU#3$E+PC1W2@*UzsM}|^c z3RMhV2{!@(Tm+;I5VLzrepV+bb>m|zA8UiXV~Hf&thh9(5*-zVCuqC4PQ|)HF|tDx zxQ9^OqvG=LPfQXwp&7J%6v+}xMh63c$r{Q(-48mu#vecUj-QES#UKXwmcxp9EJ}`) zqNEfL6)lM|_WjFh(rgv}^Xdbae zDBKN>4q8sIX$zaJE69)yx0aGel?rD(YlmNX_lc}Z41Of41d+CGPx^}&WYMvaQ09da zNt*W9*Q`84Gdo%;&s1P%ZeaHhxv&rkbg)_n6I6uu!>W^a{5Gb+ZcJQ~V=3AVOuok14gY@jy56Z=6VpYLLi@^h0tUmVy?QmU(kPpL5Y#A7!AY=C)Ul`q0>xS6n_- z@e=49zCXqG7eO$u*J?QWKYdYngtftreF{LEgZd1&ziNXkWI?PsD*3&$U}D}s`i0yw ze&b+0o!-&uhycdE_<=Qr86-pLZ**XK--i+2uOwhmygO7b0k*sE+d~_XKe53KIxN9h zsp`uNp5<-cuah9OG`;pdhiW1|(W%U6I{L?osyRkyJ-ao(*hK`+yFCcQ-1{X91RXBg z42o+rktjv$bqM12D~3>*gj~J}L!{P>QF&5HC%^_MRXuNnBmbBqbwO_1)#cx#*sLw; z0C9pJ-C6Bf?Ns|M53^~R+X(pG`z-e@Bz=1Bvf2N}+!KmLk2czhSKu0(00-Zg$bMbp zAFZ6EUdM5b^rHMd+QjiTx7$_{=>S(>I>T4kYRh~q-e*@Y?VL*{>*PhgcvfuKL;VvtRdE$Y=ZXW?uD+SoW}h#iL)Wz zWJ^Q*whtC1Stf8abhX@iE964jAG_te`ah@<`$k;Xr&>)0#*EBPIv{$1=zf*_IaCgG z4s~v5qn->;o_JM{I?-CtN!)9@tdvho*UiC^86MSZUnsw%sjBg|-r5UrATo%vDSS@7 zZ;Z!G>4PcCGrJ43H>)pF=Gs_LIr2rmAJEGx_MXM!w1P!s(q`;)dIx$|us-ro_MM6j za9iRWD=S>{-n?148NpWRln$4h;6^W4j!7OxW(FpH#vcj$a$*l0Nn`Py0gE8DRek{~ zs|amB8B!4k)MR4>m&dT%S*LTI+3tS%Fs0?CbFaw{WP z^Jwa;=GKZoGXmgxOWw>2qwK(zVZjIHg&3XXTz|56wVkY2cY|nTP(s+#Gvet|$3nvR zxdx~Gxh0GPul3ZQ0kNrrog(f6pJKFO$*aM5jKpi?>N0xKMehq-s97ER!J!dEq% zO0={Lh>k-|`?gCSW2vVXZ?byMH|d~9k<_&TINNJ;JAGMh1{_>CdzR6`cuHd337if# zId#3@3FYfqTMkFM`2O&3GC9W_Ld{L`Oawkaz zyN1dL8-QZpkI;MF(GfUS~eu>W# zxzRWGF7Pip5Wmq&bn=pbx?ZcUV$2tmD=3zzsr9Vm-kzArIhWM!j* zZ3Uz8@CdCUyZPSJRG_sqrXE%_iRpw4?s1bEGIU-`oTMJ_DSc-?C|S8_I%Tk=g81F* z0WZL8JFOR77H<`V^Z#-T8@zU|klU^d4kA!d*=kX#Z@m0OHKaVE-2BFVAz)!}|D)*l zs?0Pk6R`h31> z$DqAFIzRdI6*u9M?6b8F=3(GU{%j)ql{#(iYcE#sK5Yn0Zb#^$#75+(+Iz+0yU1^M z%Al6sK{GqA-tEYZZ$FX2&u9dG9$wWUC*UtbU)FbQ2C=op;TRtM3jKP0@PYLQ>5onm zc#9mKjmaf`EUx01kv->UXjxC())9L;*bAi&^vGCArG&+~0eGj@$UKt#Z$xEwW27b> zo)<#PFcM=nB(4$}<$#*^GHo3u@Jk@_o&Q$Q#AeV*rJuxC+k7c=JQ=w|r^#b#}F z>BeJkU=~}3Eb)Qja2XW8E5aJ^60Waguu-2TL1XYH9q^q{d(NZ^S90kF9?CSsKk`yIIU@e>d<8p9fxgN zszQ+BA$&>pz{3ibZV?aNQ92>J+8D(l)W-9n@D%3@TxpfkhZz9r_x&FhW)i6Uf!`~P zGe3E#;DAaA?dlEoY$}>Kl>N+AA!AJ(sIUrL{Q{nLJ>F(+v)U4hYr}#*O*~*o21={c zZ+oK>ApAQYG^UWEWE_xfcuWMS(G(*NF#hsX;wiVuGsOT0@`HuIy*KZ~&oR{Ev z`s?Kh>2Q23WSn}o;BVywpCW59pynqb9#*Nq5uLMFZc|5*7LB-i9(w1MlCg)kW}z%{ z#(~yKYiJJg(7Bg&h?7i7LnVOhKJ$Dz|L2ODw2j9e47ep+mT!@n1|>d&&(ckI-5jaB zYuep%YHi1St_J?vE&OHE5En7>YnH>ZT|psjoDcfYir`(nUD0J%edsf`_y=HkykKm* zfw8ezY!HhfS=&HXw76HD{aT#68k6|ZpB`q zqjOQHiQgXdPg?f9G)tR~B}FaBTmt-nnt(IO$5u|`DqZxf%pp1V(3NlP=GYKIBr9C5 zofI~EjkB$#6gVS5nZtYQ;nr2$guz)$1h+Y1P3F6hT=aISS)J942VPShI{G8F-=gp8 zyxaQY@(<8yRD$HdykUI2i<7xhjrG3RvYoz8Y|V2X_F^K=%Ip|Av45I)7c0A~&*nyS ztQ0ox#|Yus;(}pxiDt7>q@Abn%z+K!J8(9WCvx`(y5rdTLrDW)y|manO|Il~#fYM^;TJ&Eb+9j-AQ#`}`H?ietY{@U4vW{&ET+(HoRI$B|M*B^zOmq7kX4mS zsB?Ef1VySFGJTOatv0+rcFcVMJCokH*0b^Hk!7rQSY@dWX519}gOpYPm;Ln-75`m{ zfWB!4{IS%Tqt2$JHOWGzG2Ns1OP;JD|4zsGmk$mCk?6Pr+`zxQ7Hu>;Zqf z4xQAR9&U}k2^nK7>O|dm?|n~k*T0u2qNz!mILD^HKjr1LGTD|mi_5o2ILStmLdGn{ z(AAb&d?Xtq^#4pY>E5;cSM~G)I2!ivGhp2)_rTTdIXT{q+Auze&~go@#LZ%V>y)K6 zkT@tfC?@1XcgH#A+fv{rdP(PO$7sX#Y-bJhV=?&;l2jyvw^|WOwvEy&)|Zm2Nf>Xf z5L1(*{@8sV!;(j%p+Ia*zMrT%F_{x!vw0mKBs)tMX+kQpj&p|uoRL>`b-cdf$&KQe zlAeQxiOD>#q4ks|N|g(LN@@fQ3^LE5!%Z1{VhGH-u3Sv(4)Hx6Rz`XqRKtwElG=*6 z8!|w+xjJeJn2llC)ySTC8MV(Ow62Cn#pIW&stU z0C&t|3?bv&vL?7e4wW<}ov)Z#jS#|#D)}vvW zGB6$Dq4XJBnKRToj^vE-jqmQ&nz`2KI`-H82=ralX3ZucOCCSZo*(khc>h@ z9&N)#tuLPwSg8zU+OYZoOsffz@D-`!4325;(s-8m@hsD8^0p#v*V3cy ztD1K6s?o}tRp|;VN#r0@@`_EheP4BNWL9l3jy}I@5$qLB06C@`4kg3W7k2$=)jC`5 z7hz8({pmFHma_^?tjY{%TSN@M=CUT0TLeXiyk=h?qFn+Rvyg{0@C^Daf&k9_4m)-)XqQ% zkHcWWZD>`64hxo+q6EtfvDYOWR#P3U=V_Kb%3l_;fz(LYRvv%3AIyeJmC@W&9V@2jXsZ=)Wp8aWbU?BB%vzQ=#;U z(2twmJD(^zGW9PKa0+|`hsO*=UFn{S5@Dmf1c<Ekqzy@~Vaph^FBP>lW?8}!fg-nX7Jh|TsMi|q&wOGOPc0*DHNaIm+arSg;N zbKJ!4n@Pu>H|C0-4wwjX+!{G>+3F`T5CS%QGS1_ zON3!Ug}H_`pMPHyL)?T*APfo4g3bvb!u{}IHqEV}`fmigRRWQd$-{fyeSLtjCJO80 z_BWre0TWqXfWhRiJyBytyrl2-+-6RT}~@A&T` zO1~J5*Ul@QNydFxMKQ>}1P`HSL22D9Jpz&?3+QK={khH&Y0tW_@b^p3I~8zlTsry! z;pWB=saIjR=QkPa$05PVR=(k^KtWX=g`AUharqhzJoL*b8z;@rnSX?Ycs|CYCy)GT z-s>|Rs$zOH(vvNUXv)Al9!S@dP37VYL#`S`G8k=7y zn!U!Gh_%q|4xB{cs!%@vo|vEd3g7Pkj*X2miPgNrIGb-Ne+T&q084j@a*vB=L+%;v z+Y1|WXh%NMR2o=ySs?>AOnSKAR`lxB&r8rU@EQ|6(p(>yhaw zMngZkB%TN@+_}DbjOAs4pKUx%sVnv8ydl9YW1rr@pdUtUiG1RVil9%zF&d4_SfC%0 zI8=1Oi^<{~r)-?rkOkK?x6=rsUNPl%Sw2i5IhIR(=&#l9QsRUW;Yn!mx*py;6stkj z9Tx(c1vZQ8P0w`z>IA|*)Qr~dbGMO+N_6A)>z~qIlXtk~OzFz`HkfX$Zoa{zyo87h zvmj)kR&ZEG3Mj{SmXb9g1+RFis5X-mYcD~-gqwd_q}6lTQf9hPoME4A^WFg`+OWv; z;IW1GK<_Nh&&A)cL(JiVXe1Rw+TouIQT}rw&5*E+46hu}y>BKBW8D6t6NtqCnX6B5 z!{|=(omxG>Gh}ly;nUp9HqOL=9AE#M)sqnyKjz*VooyoU%K$R|7s219Ij4WXe_*8Z znd!e@H$s+Srx33|?UCTbUQzX+dUdF)yl#@g>@vI#qk~XGv&U%6f3z>_oVM_h)4! zymx&2@KzX!^ddFhz2^7Y3BE~pE!Vs_+*}$G8w|uH?e=#(eQCvZ#r~-@mBPmDJ?j+7 zCKu>WpJg%V*q9IuA=woT}Tihx8*?T>!*>2-xnMO3*#nqtt}O;f@Mni zcp7DywKBr4$E}3mm%*@6bwU#4)s3zqqLYTU4C{f-=@JjcWwOJ04bpGB)Qsy!fY5wm z|Djm|Gru~nJhlphtbuQ3o+j(FbZJ=2k z-FvX%`JUOPvV4o;khwFU3^gxaj8VyOb_zEqoRQ@r*rM-NPf3IG!lNh(;XVIlKaMGgAy+HH*r#xcyG&`OdxiLl3T9|i2g2b@P5?@xGp!+5MJcXd`1G8< zxxm7kruRC&`0Xjvg6Rm}K^_Wq4oL6~dDkf5rA1Ky>7)mPGh_o9H+k9? z67V@H@lmY1QA5{Z77B%Infm6_XAdh+m-`_jtvPyJ7ZoI`Y)jkk!j^q=$#m_gHO8!`Xy(kG;c)UK zy;_;jQ1B5?dJr9sM$3RskNny@8HMxh%ty)D9!D?p>gi`1-_(9c;*sIoAMz3-%tt7d z;*UK!C{kCaiazpdUPAj)}z7rN5V4bzip2i0Q$9}`=yZ|rZ! z>-lca&qeN@kP{J#k=Dmc_oMv{blSgk<+Md4;^~2;x;+-TbvZ;jQnUltUeeAV z&VEhf2}I0dUP~J2edCB>>9ZYJf$isOix7g}V1o!^M!93(<=&@}&8eN%K~hPu%ih7K z>dPtAEog*d_Bv>6(t zz(cUayl=WCnk5~1oUP5@aj_xxuzK*#{5ckgI+(1Ex?~mX6w0#d|2w~=8DQQTJf7yp zst~Wm(d@K)bE5WlkGW{_n93&1qJx=&w*^4f)E#`5H)Xx!d5ddE`t0J(ph~FeZ*GDM z6#tQ&A&^!Hr#xS1&TWOLBjiU)D`-Z;a_Mg7c}_sk_$*^|I4VV0NVi&v;QJECH67)g zyd{yT#Al3;^_=Iq^Mk!upzv#{0Y|&u1p>ll#G=tC1LuQA;6}R1s(O z2B7;nrE%ut&)G6?r!_{aDlcYyKN(rKWHvIs$Kw+DP>S^9f>Ow$gk<7^CqAgwv*{gV zYcdMHY_Do}Gk(S-Z1so45LEIt=EtJMp*Y`+YIqYd*aZNB_c=KcIdR_p3Bm`>jvytb zVT*P*BJ14lk(bi6TwXepOFHhOp;PvtJKHwHEe8fAf?Yc0pX?>BpZM&;YZ*}4^D$J| zRYA2zcC%_F%!3P+a_XfxY!HtUeYjcm7o?o&RG&1MAi@NvbNW1OdQ{ztCLHGtnD&xm z5IDkwH~@edLV4_^EU-#8;;=beG7Fp2g@Y)i>ayCyK9bvym@NShOANJ4=Xof2Sk)-r z>Uj^D!$L^5nelM$yiku%u9X$#tO)v{L<$~o2Ed{v>hh@w zc2^au;TiGw(f02_ToIGKJKV&-2w!YTzLtutKipiFsc}5=^ERV=slrl9*%fw$(Ut(! z%>HWIP~+NNVln9Prs}l1i38FaCsBL&+2hZhWvr|8U{r=IDl5qbG7Cnxr5{dINU3YW zhv7pK5v^dLjWz-lDaVB@bXF7etj!0qqD zqE`X_V7ayVNpuNmQ5;xX*)G&DnWx&g<}(B{W}}wv%Az1A>9 zDnuR>;zHB;a#fl=*}Zb#Z5<+Cx@R;nMPhJ3u}omXs+DGNPE$9bl^A1$%^qi0!ns+S z@_eA{c+?6BQ{QIw!BxWsV6+9+TD zTeAM$;eub4tSSD))kNHl&P!j6sjz*P(#yl%V!x~$oyhc=noGoaHXt0W!?^zI*zJiP zBuPO_GC33M05pUp)1iY#<|{PFojS4W!QS7Bdlp2F82S2Ph+jUS2p6D^!xz-sYy3m& zOHyHpD{4{dd9iAS9hUM+G=%`RaCs5&mU0c*;Jgv_aIoHJdnW2Nh!mRBvS7Vv3bSDQom$kc&zRfpjN`-omWKHPkJl*#|A zo4$1CyJY0T0)R}X^n6vV%Mdapkl4~Za!RbNO|xeHaWly}!fd4N*tFclbBhj)-P!u0 z?F|dpTX_$cz3OQ9#8bu7@oXKX&u-&Yv%udH50+c`v!g9MT1sUh%y+wrzL;Lo44{R<0QvvD)^! zy8p)qKXwUIH|7+MmiX|3c2E^$p5XCtZntZ08z+ZY^O8vF$Z(fAI?H8R;`5G-+07&|qwVKr0KXXJT)tj=q>d{i2Oe=D2222G`K z?)oDxCwNB!$)pDh-V-`_m<>%`)+kswYiYb!)&kvV=L&Fr#5=k?*#X3?4%-j%tiKuf zyDP|<+7=FL&2csy#L3#i?VaYH*S6mnZ;0F1u$T{#SrXRCT8wKQ)2uc8+?eBM{ml-V z%5rSGx0euSb8T3;#Pqjn<;OE;O$ZGHH({!~Ft7`E;t(v!d9ma%@J1Y>bB^D3Hok;Y zh#Y4zt+GTxT1ykEp{Tq2>CAMPq448%omwfm)S1g0F=0xVuapq#*R`aJ6b&!01&hZV zo*s0Cn2J*#muGZulk86Ui**hXQ4D)>e4fNABt(o6eG>oHGS>{Cf70(>LJW6Q;t06% zDD9~IJ#*Cjf$e5SPey&d%9`#61*wF@Q488jxTlV@u6t9^Hk!G-Z#8|ER&wvGv-YCm zf$)zB{H2t!eXRIe+i7xUq>_V^_z zTpzI=nt@nT`uVDZb~_Y6*ByMVU&mrO_`13>y48DzM-vTc`XfI?t6U1Jk=h!s9HFiJ zePi<|%L_k&`P%57K32QwE-RDn?cA&_0iuMH(`Kd z%C4HGdtveNgL;t-uF`_wgORl7%w!d&0F z+Ym0!tyj=9uLN$XHp}XKZU7i13MA8DQh!76MYV5hRt8_pl&YMjJJ@k6&9>nKPgfI1 zBIgwfS-f?Bx*k_$s$TC~`f^RG1!kF#LL-m!iSz^h%&0H67gr=c?e(ZWI@YbwX)g-9 zEJ)YLjzpwc*;j0O$Yi2q(=C$ljhhP3h_!ZKg#u(CjEEOfIPr?ub#()l{Wa4_IRS~e z@HuIgDrbb~9~yQLhA8vAqr<+)2l}T+B-WFZP4P})iC!FL>j|A}Y})HVzsJ$rhZ3{6T#0;$i-+NhpP@N%IHY$_SOc zOVKjiHi#F*MkeX(L0d*<6a}2x6%_X>N#Wy2TDx+nOLsM3V9L6!&T)tJ40)`yS z%p;%Y(UnXu_XbI>v@%Q%9l^M`@bq*M*BUkJ%Rn|L!0=VO?*J+))FtnTZt_#w3Fqw08*K^((=-n-C*i^Jl!G-uZv>Dq87+yd&5b}+i~O4? zG9kZ5lom(zyw98<Y)jPo3<@B zv_gE}-+kxs+dr=hey0At!8b;i!DR6%Jq3F-zWB@in`&syEWGqvXo+k;f{{x9dL$A7*wOXTa|BK~8^{E5cYuZM@~;?*m~CUla(7H^Oo#dZ)-^mnZ&`@G|l z85D2N00;Gb!3fK8V$uxSHB30>!{CpH9w#w~)RMK$4{W09-#>Inq1X=(tFw{&YILOr z9v4G)Z^@TK#GWh-c_-yZK1O0ao01VO8xgkkO-kM`gmGK#T=Qn2aK`(6%ePVij?ED& zY&Tu?r*RWD_Ez8Tp`_Uuh0bnkaUHnG=bbuYY#mDRsAyF z@F0^R@yslkeesWU+()0ccnXTRXQbw^IKvj`(aBW^7j1C{xHe^W8ZdcTBYJwX#P>`b z*;om=_cD>))BEu~siN?*QGi-uQ0rjUuwAw&5qwD8i2ocic|{SSnfDVC?k)$q`=W2r zWk5V6rG(QFsm{^Ky(a2}#Bui+3Zte94!A&c4|<8ni~;9T{25K76-GD1I|eMT%RGm2502XsC&)`wtfTUn$y!)1k@|T2S5~fh z$3z+#JF4pGs?|0GF#467Ti1_K{+d@7nCBgNl< zbj05^#s^;aA}rPf_v`d~5fb@z&ZU%3Fl(>Cb)8kkzT2wY$5PsrN)l0o_zwr5&G(NU zTE1C5LeFWPa%{O}$HD8Iw1o-*QA88C7l2w&73)A^@Vj1H5_HDF^BVkUWj^3m>T^Uz zN7f3YK>jD`;k+VxrFg@EcEb6&K>Yxkd2Wf(d0yXEe8_#$Rgo0=XFU7K4LgwJ>3xEy zWdnLd_Hw|#KuHJYM|pt*w`?!)?wz=rIcWkA72{KPoDY2fP9sERkayh#VOWK3=xvTo=|Qi^c=OFJ9p`oB-QY23y5C!z6K^aagGc zAq@D?iGz|=F#N8tAO*m2GZD`o)$NP6HC1X5Cy#T(X>fqT?(N237Vd+0MelJC1tw=A zR+$MPtv#x)A?`%*J4fC>^8yM*vwJEaUaW||WEbxeGq?$7iMTb5Ugs|&E1!RDl@p`u zuN7Sz|8OwP5UXdVn6`Igin^TJ+E`;3zL0Q;bZIT>l`ljZ{cZDY$f#G=Blr!8ucTDb zGSrhcbZnmUbFrqkqls)|tB1;_r{dnST2102mrKc;1fw)P_5^%^+{x?zLhQM@X-2)B z+RTmAip_ugo*@|R*74lh#(E@_t$p-({*lv}is+dwZU%Tuu3voquz^W}*o*9~MFIg} z&$qEGQUVM$J?UH)uXyj)e+k%@EO%1D#?RL1JFBfF7Gj-Q-9y+&51uJ7C|Mo;o5FrGMvJPxW*`{=4ACq1-KcMw*%*=Af z_N_UKRv7^%Z-dAhqdkL)d70o*Irh9?^q^zLfS)MrCoaY=qgxp1S~DLCgaWe?WHw!g zKMa1#_qMqGmqsumIDE-IVU;yUny?sYJkF2kgQkByHM<`*{``%UNSP<{3S}@zIV60< zbH{!+=UC*n6?&Z&k`;dEWQdsBS3y27vH}uX#zgQI zpOP0P-GP0E=bDJHQ&Yy-FGK2`cU+h;kX)kkV$^o zP5im^(C2c5K_OF_cBTi^eqrQ$4dWi$U;a-3Wl)y4pAcA{{^Ient=-5cD_06jmAFTJ zthU9QSr$Jng}5Pc;YgsCM~nR`kJ5b;X~xQpQdbV~uzYiZKW|887TPB;I@@ybkT(e> zN17$fmDB5d15Ul^MVK(O;w^gZ>*_?#?4ByDUUA3?I~5S|L`Qipk*+iN<-vXN_E?-7?E#Gutzq-kQ2$5bGW@>8{OI2cUX* zFlpT~!pm1!RzGTVH3E&Fmmpit-Eo$6-n7?nauWTN()k zNrh!;kWNv$7Lbqz2}LEOL>iV9q`Nz%LqTehT;lsI@6Y%5&+qf)91eR9cb|Lj%-or| z^USt>T-1Cxr?WF`aFxajN)A4 z#js45WZ{uC!;`<|72sd?%#QbT=-E<+hnn?b^CFQU3{j3noAz-@FTo8JWN z2vMTo(hFyynWf{bP6&AflbqdKpS2a};y?Y~;9NEZOMmX6)HsSNZMkhs%>30!M+f?s zdfgtoIqTCee?Wfakgrf6i;0tWi*k-hpfbv_G<;92;NWPb)l#IYO(Q3l>S~!+2)8QQ zlYWc7_v8k3FiPVa$*p=iE3U`~}f4bE^m$4*|_$qh1Pc_j1GyWwBQxfn(U1%`!8{zI><)E-6E(1cXS6`rk5 zX+hb$o_4zRCj)nlHO^FNXW=vs4wuO^%zGcHa9O>%C`r_069GBkA+sza-{N)7x?7$t zKo=-q^W3XflZ%j*LGZWXD_RXJ9DjeSsEXig|KWDi=kB(9E5BQJ*6q|bSXCOiN9QrP zXUXVOaavDaQ4A8sN=m_F;n=Z?-dQV=`Q3zg-xSYUz%g0meEtZQBw>SR{*~0~p}5Yy zprbi6`aKU9()53RSLvfNCx{k&1Bz|W+I8@b8x<8 z`I?#I32q$H|20^Se#|ZH6;CWN5=`1W^){^wapARoQ2)93J$auAy*?c2xJ(7p;$?~R zWddxb{=>E!x~R-Z>~l{!4516n8DsS4@bX!TM0~_*x~f5>W@6)MVVd+Cf+~5s${acP zo>RSFEHda%z&`9xcc&`Rn}(i%es7^~^_=<7M;^gAPCH{W`3so%%#gN@t%!=Kdey;` zXn>_$|FM*%Q1#Pz^~*1m&)VTaDHceCN;`zKU;W^pgN36LQksdfA7_H3RD-c`XRz*C z!V~XsrXbed=3zd8QDP!dMey#A$#v7!_@ui5wG}MJ$wvn5^y~8Wf4mdyv0kzHteij9 z&nbt;rzL;{ptt(yR$V9ehieez5UMwus?5{iaPH*jf-acLV%9&Z@B8Gz_cZ%Po-97m zc>Ue>7Q;G&a{4s}XJ6fc1?apT(w5wb<-^W-uVNi}G^;^MDS2OA!C4i%c{|#B`mPYCgg2$G`Krzij>LqZGn? z2hX>sM4bGzTD=o+zOj7sE zcy2ye;x(5(5SW?zs~7r^!{Yx=)Ha5u-Ie1|j3o%-t=bEqF@VLMroAlF0w)#_U>JLS z-5QkfncAS?eQGRCb>w+cdk8*n4x}g3COqLsOEx$sMSC?dcSwWX^sk*+#x(d+)P5k< zvy6Zp9fF3J$$!!7+04fpj_3QJtnLSPLk?3Z&#&C-xLbNaXp|j{(ZE81Jge(`gFu+G zrpoiD@p3}O!Q=A2 zFc)#ce;TV{O%1^}4=9(=jKl|jAZ1 z3qQ!TnoeNFt$kRof-vA47XA#C-h{a6x<0wxGIoP{W{;OE(dT#l&}el375wkWY~`-M zdpS1Ap8d)Mr=i{2jmO}FiLW8Q3IhhUBYVF1tk;v`*oisDl2T^|lBP%TGs*p8QO%?Cz6Kk%U!>vAZf8#cP&wH+i z_Pd`e`-M?pS~YvOveN~VuKNb?hPaPQmSx5#0d9-o!N2|D)gJm{4Ke-``g?bpap90; zF1ww?VedQ3p%DCnP$@}#FnPCtrn_W4gAoQ&xx_JV^lkfIR?m!&!}!BfPiB8{RAuBR zGH`yRF*yA-s}YaEbOBM4a*x90_@^jjHund={5FjTZTu~qy}B5fGynKE!(tZ=YwPQU z_#Zp4mQu9G;3k|si?v_5gT4q~>43?4n^_oj8wB=X@~N%XLDaxpIi60=I6D=IOn4a^!6I* zOc!j{+sas*HJs`g%W-Bj#J|Z= z9V2RKuP3M(eBS)wcDtI0btiJ2Q{kIy+KP77to&nf@h(bx#_?l!!*nFYyKW^*7En^H z|DLg=vqG!AvCU{EmL*(bP+H=LdMf!PCvgiBX&!V%aK-yJ`a@R-0qH1_7qIvaLEE~65+F(iyo?>6?naB?u zS(G1x_h&R67BfFiPbO*J{BRuIao;l5+*&ESTUZQ0@RInBfKYK7eF8A9&|*sc)@<@gyD)G!0v;;2ax-TrZP6GXf%+b?z_(KaOsldChXVg`=%8!Q+fm` zx12$Mk+5vysSVk)eW z&YM^}Lht(!DV9!e{xGu?zDZA*GTDc}xxn#4v%DO#mw%0SA*Zgc3J3`Q0kYdqI?*=j(p=n! z=c;ABSmFRHQVaqgea#QHz&N-{ZWf5P7^nLDe*ST}UX+!(R`21>@?m@*bGvNg7D=Zr zUxbe>rX@OqgDsob+gz*ttMP?6wVw&eD^NL66d%+$Dwl>K5}r4 zWVIK5uJr)ShJg}VgYKhtBY8ivwH?8{j0M(NhhK@2s)rSp&7%xxBHS?pXj z-c5H$w}EKK7m8~C9^DS+XqZT#?{i1S@Z_oj5MThD##frvXYc&Xy5^ReU+rz$I5^XU zL08#qKZ1jHi;IXtZupFdnL|AE#BcSp#O-mrHz8c395%3$uHv{H7r=#>+uWov z^RaX%3cyPLEI!Wd&QjLM}E-^fBeL^)FXny(4^o;rF*T8}v{*ZLJb zY6g43Pj-gc5EG(*bu+1>(3`oo3Cbeo8AFT$R#05 z2@`?QP<2&DBVr2V>=~#3HCwJV>9i)FTlJ{sdrEx{#6%2c%gL|KSNV4zB6!edkA^DC z(p)10-NA{{N$YjARKU znfr2sd9WV&2)3>+AAG61M-FQ2RO}lptz=VMDbtxxU-?jX+F{ub@_nXK#XG?n93%c| z>@a{vD zlShUfRVzh*AZPQhM|pH25!J8~TG43zS%1=qxk)6*W^Mq3eXID(h){$ywFq2hpao}i z6obAQ;&93&qc)1iT7c<`Qi(h{-INDQ#_GDCo(0^?U{V33vl zlp=V%>fz-%>WcD?WgL=;^hNe?vCHbuyN#~IG5p0b7-b?qDx&mpXZ_EgOCFb7bhZ|J zcieuTgqabHRVh-Ga4b1qOos7!aedSypbknnNwj#>g60F>Iu%<%4`|B`vp$yjzkoMA zg!5zQHxpP=tpN2$X>tsFUWv4dpR2VX0Cd>xeXQFt7ppC$S|g2nLbs{zTvha^tf5_C zt+$<7R@qh;hmwbmrY4=td|aX3fu1SmbFB^sc$=p7pe>O-r| zeqA8=Q?CKYzOuz4j5A9`lZQ z9vvdWM;Wj0Qa-AC9^z9AFyV^$nJ3P`zW%aU7<^WHQs-HUx%sQsA12VXS*M$%rXYYkZ}`Yd>Yb zD9M64E-d^mRzre4HaVb<#_^xZ(A@F4D;mm%h(ze)byq6(7k0#83u!hBmWMu;#}(QO zuwIf#-cMS~-j}33Gp5T>CLWN8yI8hF>3E<%EX()0eAi0J1wXX`o4v8u$tl7WVDue2{u#@167RpdPUkkC^#Hom{mau5%bdnv zSDRjn?s|V32eoZ}IHV1vf=TUz=Kz<-Q3D-Sb-5-Y+NAo29=6d8$iL5J>i8V(P!%AT#8Oa|Hz(qsKu*=ES{oX-2-ngh2(Wd%GcV@=sJ}FP>B;~ zjJVVrK(T4F054A2>DOx!cA*E*%Lj7QB3ac3N&nDcghro#)hTg$bT4KAe4r(Rmd-1; zY(SpEux8s>+F{#G$Zo}g{G+a%EL|?vjfSEXv9az<_PlAO1fPgXGRc=>XUJld*|3#z zsItRvzerVnJi3R*Uqvi!?KKYQ*0YBR99X*6J8c+Pf&|N5=d!8!)8x(Od5ZXmu-ZU) z;D9ikgW6{?k(AS8t5zIEMh3WpITg1j_jT5h9-@@OG8E7O&$;OfA7jXRuG4poUGm5h z1-zwcWvXnI0E+eu!fVm_VBQw;4cYc{5R1h!R9whb3aglQU(XIdLXi^-f<{h57tQmc ztJJc<9BpaB%i6Nx93%`Z;U&6jQGa#1I-Iz$wRbbD)qR$x#&vQb9Yu+!dh1{VgI)F! zDo=Fa8J2DOux@>GUz^?}{do7`HjVW%CW|VyEZsxrfQL@97Vp2%oewwSpwXX}Y^gp{ zJA9g1Regn(cvy0aQh>QRs`PXn9x?d1d9J-MENqttr4CPNlR_n5cd2jEXbd|H2m|T4 z3ZiWq#!F&2Qt6*#CDc6N$iHI}Ea}T>LA0i2f+vtH0#wLfMJaY$o7u|&x037=>l6qg z#t}8`Pf8wM@7;CxHqQ31q~dz1!S~u~FnV&TbJbVjU9*6?5CR0uaRj$(W+Ay3K|r(T z9CE6Pl9xPLdwRNh*b##FcF<)#!<=6oEVRjl zT?-Ribq-$fR4}8&#*01H3e)u)mV3$1S7tWMm1Ou0Y2AlvdTNbhVg6VRUCkN`L9Y|r zZN{xHGzO!v*@}Bf_)9{Q&KOc3n78XKWM6Vi#+3f3+DiZ=$k=S$$($+_CeG3w#0lf5 z6Tl;w;bx-JYsj?n8ceH+w|eNC3=a^HHl#O)H|K+5LYQVM#t(LwFvdJM$L6rLuP`R} z;pD)HsOE*E;aN7D+Q9Znt&R0zSdq?MYiOuvw!|XsEF?&@| zn|+ z%-QlKy`pZX?)I|EClNkB(|huFp*0%_N_NSrMo58D+6d{ zeL#6_`~_8OaHB$0sLdnSiZ3XBPg7Kji%oSvA`f_&(v$Q)J%HqV6USiq9=_ZPWOd<7 zWwhZ%#x^Y7p2aaK*&RrrKz9wp)5W7AF`w&40wh&qCPGkFVS^!&t&*SfBU-^JwgKZQMdC_5ZGhhxu;P#wz!&sxo=onJ6Y3*>j zQ^g9moh&83*Q@R>KSQsAN^2GxkQ<)aOo@G5tJ;U7@&>=sMb3O#64$Q&<>?BP-Oqzc z8}{w|O#FRoJhQC2Yw|~?V#?+;M+dwK()m`FmhFF_u1B4UnhCjs($s3ga0-Rx4&i^5 z;vBe%Z_#o;B}hDe*lrFrqDdtdi&UB=#ow)uy946( z>)T}(4s1aKy)iu_f+^*OmK}t(>TlY$sOJfr?-9K%!G7!Bge$Z3!& zI+{ybMDRHc9UxZ$AN-$bb~ggZ|2)el;CI*jUjKc|&#g4xWpdEdQ19@Gxv^ngh{pXR z%dwkhulNCDAE{<%9>Ov8v23Ljwv;ZE;_r@5DOr#ap*EOe%T(loQn+ThSe+u}p&j0_ zr^cm_cP(^}TSuz0@uRCLJw{p3wJm9K&r`?Pfs6`x_pF0P3}`80CD zARFvA=WkLODAt!6>IvWp-`|TUZ6?;AnTt*YB&3ZM%G(Ibb_08_Ng4(3KbPyYp9UGE^e;@T3)wwrYwt&iBS_jb9M_m#&-9Ep3OAg zAQCoRtJaOr{t})n#v;WwQ$uU+w*UIAP9+hrL)Mmx0z z!^w6gGsu;5ek%icM4aafj3YA|aUR6W9+jZeLoJ`mAL{)O1Ne{Fx0kZ`@te_VDSmXC z0zeMA2*lY(ep+{Ya)OYq;AZx$PAJQkpPj|CJ#}9s{NB`=#TD9qqlsR$&@10}wUX7m zlgMtT5l&2vMWHeSR8-U)jo6Y3;4Z%gKorvWW;1m{?PN8ZgY;7+N3G-z?&nVz82d{5 z-4K6}6uLejyXlHP21md?GSv?nHTA7ArO6I591@r2`R(2#Q(E)Gebev9PEaImxhk2| z0;qEvJ*UPK3voSZr*NP9c>xt-7@0hlQaHEf+8)P9$Zb*^zx8VI2b-PnD;5YE8u>Y@ zN5JLe%klw#=s>bNpFw`(RQWVf|M0t}t6YTuo2f3Y#=Aa3dp zouJ6TBK^=m34)e>RFT_C8h!enaE5%(@%ew-Jb^bO5L}rw>dKm#9knc9u_{-y7IZ0Q zc4=Qoi&!{%cKKV&ou=L&z1ZdLarei;NZcC49M7m3iWmVKJB_5r!m0pNx5I`r?oLfP$WK6(!wi>OnaQsB9Om2y5pGs4?+(G zK5y*N-YKWLUI=#Re)}2C|D3@{6^sWog3a${B|fCv+y5h)XAdz9e6lamb^~ChL>65n zKc?$rLX6nEXpW!^liQeeP!2Iu64f%*@FzvAv$J-*jxs_ps~~=;tx4qDo0$E1&Hp*v zpj_+*ht9`F73LKOuB`CiF>oW2@-5r7%A{q1mQ?xd&Z}(W`=RAyVI$UrY2~#dJcT$PPF7OqwF|KpeI9slO z+y;B1;^2TC{JjZv!6z^QV|=IGY2nKN>QMu*p-w>E#^*0xJR>EiwJn2kgDb?m&{>o# zU!BQTYi$Am^uMtE5IqLyV6$f5O9Hs;&A*drMbb9MVePLOTP^``!=q+}y+G%CR+eIe zXSCSb)Z(q!2%%>`Q(FzWh|Pma=-?3ahU-kx_??+JuEClFt$Q1*gfG*wPk8Te-M z(@|gW5Nv(dF$8D zX4IoQX*_&QEH?wk6d9g6y2Mh&#Uc-f7biR&aN9hArUCW~gr{19gJ1eAO5u_(#?sG5FnBuev$NHXY}7(;;OifJ`sSezzq$uOiI&X>d<~UbF<1i7cSx? zC-&C+_>Oi^Bq3!bJgz1VnJkH-xK!uO2+8<3t%~oTiC3g>*}Elm;+;_osn@NqcM^y` zxV3mi6!|Gbi3x?%Z57mB?%hOf(QBsPC&k9sNj!d2=HXvX`Fbq?M_X1pqk>^`QQbt( z5Gw6VKxj4h1cOc1ozJV+GtRbhj>(1iem)Eg8{gygW7<(%#ln}vRQv3qnXKoDR?#-z zFu=S$jhw%B=BL>lBO%0J3lkZ8dQYd^94s{|6>53KK6!!ycUc>xp;qjYXgbK>E`pkz zpGx2tGr($xXzY@-_VpkhllzMckdGP-({ueoMa{eFN(zf^kGW2uq1_WjKWMR86xsLZ z>~6RuDZ`;{b1qTDwV3D6(CEnj_#S$fa%dL04hnaiT4%oADhSg8Y16o`* z=2ZJx--wCuhaJ(> zQcbqsRa5+@L#5$ubLfU}#~~0}u>a}q>3d0^V;E$18+#jiew>SijE)EuK0DbyOq6-K zxkHXDF!(YaX$XWiB0H7E#8_22SGbw~GgppyU?FemV_q`qHI=vG7`P-U8?IFc8wxsS z2I0Q1R-j)z`-5~>a$(yGoyrsDnusB1$1$MkP3V6{DwEcP0H*+iAGZPocSC%Sc1=3) zizY~98-zL2^%7`L42TW9^&?~TADP9TSZ|S6#J*6--Ds#6=kJoDqWeUJ7z)rY=f5|1 z-&OGAOm{tW2@C^+D`h!^21K+r=q)$7s4Vvs)E9|E$+O(Os%Lr)2I?`PrK=`ARb!R# zbl2rCZ{Xb$A@pRT{OSU%z=RE!I zy>;qsYL~Sm9xG%EYrG2S73C+UY~dGsSZ4eAyeQ7`UUyQcXeO+s!iEdl*k+5mK3rfg zu2`gS-I>qL%0NnNW|HF51*Ol%R1zNsK$Y9VkmG~O;S*2m{453x|MQAnY(8P|i7@;d zD9M(&{wsJ>)_ERuDaaJOP91!Dq;PkBM|#VxZ6nNSU3vqb`mt^|)yj3I!dSNL{_QTQ z=z&n-mY`SDYXV3@LV|W-SYork5~bL>*kBI>S@%#rh0BcKn<z($}@EH^fAVfjc{+iui3??=7xgPs<8eSDF;fjK#hAzEF(y1Vo6YN)6Es zz8uyuYansg#G}X^61MEFSwha1<18me#sXd+%5(iX2FkMouMw$4uu`u$+{U*eb+nZR zPG`LwLf1;ApHI<2bgAWWqpRFam zJ^0kL>>_W4wVN|8bwJ(5I(ewHeQgPKt_f$l9vl6z`W0*2T4Fpp-qLcY+^c_~n40 zVqiG&Hrc155m~Gde6e%4t&l(0d$0pRLl0l<7z`V6!CL8wjA$}SK(tXvc}B&({N(&| zEGdmn#Jrw)9u>FOR?;MZEJ;z#nmdNplf6U^huj=K`}XP(s6O271mXJPLU}ZWH{EuL z`{NJZNc%qOBdO^5id><^FtX9ZCXAn(+z zOOzJZTIUDv_}n4;tzQAb;~1-Ko!~#z%ctM_-1CTVa<#t2j_uZnloY0UzOhoR16!s_ z(6FY#T;OGJ(TQI9i<_v8jglv;VF=Kex&QI7k8RBhxs#uZ;%+XBu65x1iL5Fkm8{#? zdPFb<6wXdsJ$?71?xuc3x@@kopv6*lg2H)UF@w{-0%7=$=)`Lr%l0WRYQvZGolzHq z?}DsK2PNXQ2ZK8U+%4=-o-et>1H$ml<~0mcc#f$sl%4+92OlgnR$;laH1@0w5Pltc zTHY->ME=e`SQU#3Hn>QRLJ~F=Q!Z)_2npkrCrhsGHwab#et? z|2<*}4mZumJeb(4X3U$+yE~#?Orr+egsq3cop=(%$JGtyXt3C3zYV1F%GGG=#%=SJ zH&H1XROsQ9Q(;n}rEr``Q$29!aMuK?A>3OpBPuf6*mh!s9CA3@xOwbpTxV$XiCn&)gFQ%cmZ%PG@i^bKWe`IsPJmQARM`_gCXHap3flgmGw0eul}5!7myKBU1-#;|5j+(usPY6VFq_(nu@%u zw2Jwn*h0(-yA5=RXZYkyN&V~wwol=^<}a{EN2=-V_j!K;)n4u`mH6tUXSWe+OpGK? zD0#ih922SG(n!L*IID-T4X;=s0BLssVWMHUv%lM2!NTxav%TC8L0UZjQz@%G5p1fO zfjzM=y0ly_i@IEoU!bq_*{sC#=$2^}gp21q9#lw(7bBoBsAlz`<&dI&=H_R4WLB)p zKmg#GI9J-vD>IC_f%^v7{GD6baC>t z^f2$kxAB8qBYPtUrJlo?H@41+4-WJG`<`M?s^t8~HCsE#aaxu5^DvPFhA0X|$At&k z0{LV-EWv9as1D>8R8549+NzC)^aNa{WSKn*r$Y(S z8c)0TZ)n8pPU{vk0T<$da%>^m4BE#QrFVvhx5CaJG}^F=X^t{0g(g}`Wmbj8>j*Pe zs2f_d&rlOo=SJXJ6JRCNMZwRM-+)3G9&B0XgyKUXu(5sBYBCAps2rr4zyM6(+y zO@yNa%@piIkDo>`rdse>JX7rt7(dd-?nwe-8JpO#jE->$V#<%24@da8I+Zff5B*#S zkH!Zil|Km)|IJPfZMt_<#wM$UX<<98@UU{bRdso6+!RW8@k4wnmMMgejovJzQr}ZUG?UT1B?e_ z4{*k8JArk$+z?5a(8rNc*G#E|vlh(nW?kBb6cBX%&xVQ!o~MP@w&NVj0@)kioIW~e zt@ph3>XO-I1!8D){JcH`ZAl_YO+@i~Y3-R}m{fG5Og-;MA?m>GUJRlF40!8B0owa= zstpAH;`mWf+*9DRSAZ~7sHtV;**gq#!o&I)m2Ebz2 z@BP#NbsYP9g_9mhgZoC1W9s(@QyKq%x<~3_7Xm!R$MV8c+A`6~uBVw1-TMgakmf!b z0qpnxKaTO^^+(B{AN5g9DMRJ&IZXn+Z%~P9NhuToXi4sG2DrQ{)>qBlm08B4sMeKz z|Icx|eVe1r$HS@jWc>#O%TVPz|631As>vO2h}0vK{YUc;D$V zC-Gio10CW1Z?{Y>6zB1FDZ&QoKghcam)g&Wq845K{~RYHaw+N_C|Yn&{-BqucNmXK rin@w|p8$PB(THpzPwbvOL*oTL;{v~^p9zEas6S6swI7!&TZa7~(JqT! literal 0 HcmV?d00001 diff --git a/bcbox/frontend/src/assets/images/home_1.png b/bcbox/frontend/src/assets/images/home_1.png new file mode 100644 index 0000000000000000000000000000000000000000..2a5fdeab274067fbd18076f9396c9b04db2ac9d8 GIT binary patch literal 130458 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

4882pvpE^AxF5Zp+-t(?jlR#N?Y=(yPX6@F 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 \ No newline at end of file diff --git a/bcbox/frontend/src/assets/main.scss b/bcbox/frontend/src/assets/main.scss new file mode 100644 index 00000000..13ba55d4 --- /dev/null +++ b/bcbox/frontend/src/assets/main.scss @@ -0,0 +1,186 @@ +@import './base.css'; +@import './primevue.scss'; +@import './variables.scss'; + +#app { + margin: 0 auto; + font-weight: normal; +} + +// ----- links and buttons: +a, +a:visited { + color: $bcbox-link-text; + text-decoration: underline; + &:hover { + color: $bcbox-link-text-hover; + } +} + +.p-button, +.p-inputswitch-slider { + &:hover { + opacity: 0.8; + } +} +// underline button text +.p-button:hover { + text-decoration: underline; +} +// don't underline button icons +.p-button .p-button-icon:before { + text-decoration: none; + display: inline-block; +} + +.truncate { + max-width: 1px; + white-space: nowrap; + + > div { + overflow: hidden; + text-overflow: ellipsis; + } +} + +.no-indent { + text-indent: 0 !important; +} + +// -------- layout + +.content-center { + text-align: center !important; +} + +.content-right { + text-align: right !important; +} + +.permissions-modal { + width: 800px; +} + +// details page / sidebar +.details-grid { + h2 { + margin-bottom: 1rem; + } + .col-fixed { + width: 150px; // label column + } + // make datatable look like grid + .p-datatable { + thead th { + padding-top: 0; + } + // highlight the row for the currently selected version + tr.selected-row { + background-color: $bcbox-highlight-background !important; + } + td { + padding: 0.5rem; + } + th:first-child, + td:first-child { + padding-left: 0; + } + } +} +.details-value-column { + width: 40rem; +} +.sidebar .details-value-column { + width: 20rem; +} +// eof details page / sidebar + +// wrap text +.wrap-block { + display: inline-block; + overflow-wrap: break-word; + width: 100%; +} +td .wrap-block { + width: inherit; +} + +// --------- Info/settings dialog modals +.bcbox-info-dialog { + &.p-dialog { + .p-dialog-header { + padding-bottom: 0; + + .svg-inline--fa { + color: $bcbox-primary; + font-size: 1.8rem; + padding-right: 0.75rem; + padding-top: 0.15rem; + } + + .p-dialog-title { + flex-grow: 1; + font-size: 1.8rem; + font-weight: bold; + } + } + + .bcbox-info-dialog-subhead { + font-weight: normal; + margin-bottom: 1.5rem; + padding-left: 3.1rem; + @extend .wrap-block; + } + } +} + +.drop-shadow { + box-shadow: 0 6px 6px -1px rgb(145, 145, 145); +} + +.gov-footer { + background-color: #003366 !important; + border-top: 2px solid #fcba19; + flex-shrink: 0; + min-height: 2.5rem; + min-width: 100%; + padding-bottom: 0; + padding-top: 0; + + a { + color: #ffffff; + font-size: 1rem; + text-decoration: none; + &:focus { + outline: none; + } + } + + .button > span { + color: #ffffff; + font-size: 1rem; + font-weight: normal; + text-decoration: none; + text-transform: none; + } +} + +// ---------- datatables + +.p-datatable { + .p-datatable-loading-overlay { + background: white; + opacity: 0.8; + } +} + +.versions-table .p-datatable-table tbody tr:not(.selected-row) { + &:hover { + background-color: $bcbox-highlight-background !important; + cursor: pointer; + } +} +.p-input-icon-clear-right { + position: absolute !important; + right: 0rem; +} diff --git a/bcbox/frontend/src/assets/primevue.scss b/bcbox/frontend/src/assets/primevue.scss new file mode 100644 index 00000000..0bb076b9 --- /dev/null +++ b/bcbox/frontend/src/assets/primevue.scss @@ -0,0 +1,121 @@ +.header-center .p-column-header-content { + justify-content: center; +} + +.header-right .p-column-header-content { + justify-content: right; +} + +.p-dialog-footer { + display: flex; + flex-direction: row-reverse; +} + +.p-datatable { + .p-datatable-thead > tr > th { + background-color: transparent !important; + } + + &.p-datatable-striped .p-datatable-tbody > tr { + &:nth-child(even) { + background-color: $bcbox-table-stripe-background; + } + } + + tbody { + tr { + &.p-highlight { + background: $bcbox-highlight-background !important; + } + + &:focus { + outline: none !important; + } + } + } + + .p-checkbox { + .p-checkbox-box.p-highlight { + background-color: $bcbox-primary; + border-color: $bcbox-primary !important; + + &:hover { + background-color: darken($bcbox-primary, 20%) !important; + } + } + } + + .p-column-title { + font-weight: bold; + } + + .p-paginator { + justify-content: right; + } + + .action-buttons .p-button.p-button-lg { + padding: 0; + margin-left: 1rem; + } +} + +// Primary color overrides for buttons and action items (checkboxes etc) +// Note this could be eventually replaced by a custom themeing (which has JUST been introduced in Primevue) +// once it is more settled implementation-wise +.p-button { + &:not(.p-button-secondary, .p-button-success, .p-button-info, .p-button-warning, .p-button-help, .p-button-danger) { + color: $bcbox-primary !important; + + &:not(.p-button-outlined, .p-button-text) { + background-color: $bcbox-primary; + border-color: $bcbox-primary; + color: $bcbox-outline-on-primary !important; + } + } +} + +.p-checkbox, +.p-radiobutton { + &.p-checkbox-checked, + &.p-radiobutton-checked { + .p-checkbox-box, + .p-radiobutton-box { + background-color: $bcbox-primary; + border-color: $bcbox-primary !important; + + &:hover { + background-color: darken($bcbox-primary, 20%) !important; + } + } + } +} + +.p-inputswitch.p-inputswitch-checked { + .p-inputswitch-slider { + background-color: $bcbox-primary; + } + + &:not(.p-disabled) { + &:hover { + .p-inputswitch-slider { + background-color: darken($bcbox-primary, 10%) !important; + } + } + } +} + +.p-tag { + background-color: $bcbox-primary; + font-size: 1rem; + padding: 0.25rem 0.75rem; + font-weight: normal; +} + +h1, +h2, +h3, +h4, +h5 { + font-weight: 600; + line-height: 1.2em; +} diff --git a/bcbox/frontend/src/assets/variables.scss b/bcbox/frontend/src/assets/variables.scss new file mode 100644 index 00000000..de191499 --- /dev/null +++ b/bcbox/frontend/src/assets/variables.scss @@ -0,0 +1,9 @@ +// General font/etc colors +$bcbox-primary: #036; +$bcbox-link-text: #1a5a96; +$bcbox-link-text-hover: #00f; +$bcbox-outline-on-primary: #fff; + +// Panel/row/etc backgrounds +$bcbox-highlight-background: #d9e1e8; +$bcbox-table-stripe-background: #f2f2f2; diff --git a/bcbox/frontend/src/components/bucket/BucketConfigForm.vue b/bcbox/frontend/src/components/bucket/BucketConfigForm.vue new file mode 100644 index 00000000..3f700007 --- /dev/null +++ b/bcbox/frontend/src/components/bucket/BucketConfigForm.vue @@ -0,0 +1,176 @@ + + + + + diff --git a/bcbox/frontend/src/components/bucket/BucketList.vue b/bcbox/frontend/src/components/bucket/BucketList.vue new file mode 100644 index 00000000..645796de --- /dev/null +++ b/bcbox/frontend/src/components/bucket/BucketList.vue @@ -0,0 +1,148 @@ + + + + + diff --git a/bcbox/frontend/src/components/bucket/BucketPermission.vue b/bcbox/frontend/src/components/bucket/BucketPermission.vue new file mode 100644 index 00000000..4197ad70 --- /dev/null +++ b/bcbox/frontend/src/components/bucket/BucketPermission.vue @@ -0,0 +1,207 @@ + + + + + diff --git a/bcbox/frontend/src/components/bucket/BucketPermissionAddUser.vue b/bcbox/frontend/src/components/bucket/BucketPermissionAddUser.vue new file mode 100644 index 00000000..eb1dc180 --- /dev/null +++ b/bcbox/frontend/src/components/bucket/BucketPermissionAddUser.vue @@ -0,0 +1,36 @@ + + + diff --git a/bcbox/frontend/src/components/bucket/BucketSidebar.vue b/bcbox/frontend/src/components/bucket/BucketSidebar.vue new file mode 100644 index 00000000..513b34fd --- /dev/null +++ b/bcbox/frontend/src/components/bucket/BucketSidebar.vue @@ -0,0 +1,130 @@ + + + + + diff --git a/bcbox/frontend/src/components/bucket/BucketTable.vue b/bcbox/frontend/src/components/bucket/BucketTable.vue new file mode 100644 index 00000000..a17d91fb --- /dev/null +++ b/bcbox/frontend/src/components/bucket/BucketTable.vue @@ -0,0 +1,200 @@ + + + + + diff --git a/bcbox/frontend/src/components/bucket/index.ts b/bcbox/frontend/src/components/bucket/index.ts new file mode 100644 index 00000000..707c6196 --- /dev/null +++ b/bcbox/frontend/src/components/bucket/index.ts @@ -0,0 +1,6 @@ +export { default as BucketConfigForm } from './BucketConfigForm.vue'; +export { default as BucketList } from './BucketList.vue'; +export { default as BucketPermission } from './BucketPermission.vue'; +export { default as BucketPermissionAddUser } from './BucketPermissionAddUser.vue'; +export { default as BucketSidebar } from './BucketSidebar.vue'; +export { default as BucketTable } from './BucketTable.vue'; diff --git a/bcbox/frontend/src/components/common/SyncButton.vue b/bcbox/frontend/src/components/common/SyncButton.vue new file mode 100644 index 00000000..4d488cec --- /dev/null +++ b/bcbox/frontend/src/components/common/SyncButton.vue @@ -0,0 +1,119 @@ + + + diff --git a/bcbox/frontend/src/components/common/index.ts b/bcbox/frontend/src/components/common/index.ts new file mode 100644 index 00000000..d3c5c6b2 --- /dev/null +++ b/bcbox/frontend/src/components/common/index.ts @@ -0,0 +1 @@ +export { default as SyncButton } from './SyncButton.vue'; diff --git a/bcbox/frontend/src/components/form/CopyToClipboard.vue b/bcbox/frontend/src/components/form/CopyToClipboard.vue new file mode 100644 index 00000000..fbf7ff9a --- /dev/null +++ b/bcbox/frontend/src/components/form/CopyToClipboard.vue @@ -0,0 +1,48 @@ + + + diff --git a/bcbox/frontend/src/components/form/GridRow.vue b/bcbox/frontend/src/components/form/GridRow.vue new file mode 100644 index 00000000..c93ab9f7 --- /dev/null +++ b/bcbox/frontend/src/components/form/GridRow.vue @@ -0,0 +1,40 @@ + + + diff --git a/bcbox/frontend/src/components/form/Password.vue b/bcbox/frontend/src/components/form/Password.vue new file mode 100644 index 00000000..1598b416 --- /dev/null +++ b/bcbox/frontend/src/components/form/Password.vue @@ -0,0 +1,47 @@ + + + + + diff --git a/bcbox/frontend/src/components/form/SearchUsers.vue b/bcbox/frontend/src/components/form/SearchUsers.vue new file mode 100644 index 00000000..7301b94f --- /dev/null +++ b/bcbox/frontend/src/components/form/SearchUsers.vue @@ -0,0 +1,182 @@ + + + + + diff --git a/bcbox/frontend/src/components/form/TextInput.vue b/bcbox/frontend/src/components/form/TextInput.vue new file mode 100644 index 00000000..3005374d --- /dev/null +++ b/bcbox/frontend/src/components/form/TextInput.vue @@ -0,0 +1,47 @@ + + + + + diff --git a/bcbox/frontend/src/components/guards/RequireAuth.vue b/bcbox/frontend/src/components/guards/RequireAuth.vue new file mode 100644 index 00000000..ea39b0fb --- /dev/null +++ b/bcbox/frontend/src/components/guards/RequireAuth.vue @@ -0,0 +1,37 @@ + + + + + diff --git a/bcbox/frontend/src/components/guards/RequirePublicOrAuth.vue b/bcbox/frontend/src/components/guards/RequirePublicOrAuth.vue new file mode 100644 index 00000000..4658b1cc --- /dev/null +++ b/bcbox/frontend/src/components/guards/RequirePublicOrAuth.vue @@ -0,0 +1,51 @@ + + + + + diff --git a/bcbox/frontend/src/components/guards/index.ts b/bcbox/frontend/src/components/guards/index.ts new file mode 100644 index 00000000..56ffc7d2 --- /dev/null +++ b/bcbox/frontend/src/components/guards/index.ts @@ -0,0 +1,2 @@ +export { default as RequireAuth } from './RequireAuth.vue'; +export { default as RequirePublicOrAuth } from './RequirePublicOrAuth.vue'; diff --git a/bcbox/frontend/src/components/layout/AppLayout.vue b/bcbox/frontend/src/components/layout/AppLayout.vue new file mode 100644 index 00000000..0b8f2e7c --- /dev/null +++ b/bcbox/frontend/src/components/layout/AppLayout.vue @@ -0,0 +1,47 @@ + + + + + diff --git a/bcbox/frontend/src/components/layout/Footer.vue b/bcbox/frontend/src/components/layout/Footer.vue new file mode 100644 index 00000000..a2c1972c --- /dev/null +++ b/bcbox/frontend/src/components/layout/Footer.vue @@ -0,0 +1,78 @@ + + + diff --git a/bcbox/frontend/src/components/layout/Header.vue b/bcbox/frontend/src/components/layout/Header.vue new file mode 100644 index 00000000..1c4a26c6 --- /dev/null +++ b/bcbox/frontend/src/components/layout/Header.vue @@ -0,0 +1,58 @@ + + + + + diff --git a/bcbox/frontend/src/components/layout/LoginButton.vue b/bcbox/frontend/src/components/layout/LoginButton.vue new file mode 100644 index 00000000..ec2a6f8d --- /dev/null +++ b/bcbox/frontend/src/components/layout/LoginButton.vue @@ -0,0 +1,46 @@ + + + + + diff --git a/bcbox/frontend/src/components/layout/Navbar.vue b/bcbox/frontend/src/components/layout/Navbar.vue new file mode 100644 index 00000000..4bb4289e --- /dev/null +++ b/bcbox/frontend/src/components/layout/Navbar.vue @@ -0,0 +1,105 @@ + + + + + diff --git a/bcbox/frontend/src/components/layout/ProgressLoader.vue b/bcbox/frontend/src/components/layout/ProgressLoader.vue new file mode 100644 index 00000000..6fb748a7 --- /dev/null +++ b/bcbox/frontend/src/components/layout/ProgressLoader.vue @@ -0,0 +1,33 @@ + + + + + diff --git a/bcbox/frontend/src/components/layout/Spinner.vue b/bcbox/frontend/src/components/layout/Spinner.vue new file mode 100644 index 00000000..f820e9d0 --- /dev/null +++ b/bcbox/frontend/src/components/layout/Spinner.vue @@ -0,0 +1,16 @@ + + + + + diff --git a/bcbox/frontend/src/components/layout/index.ts b/bcbox/frontend/src/components/layout/index.ts new file mode 100644 index 00000000..41285fe9 --- /dev/null +++ b/bcbox/frontend/src/components/layout/index.ts @@ -0,0 +1,7 @@ +export { default as AppLayout } from './AppLayout.vue'; +export { default as Footer } from './Footer.vue'; +export { default as Header } from './Header.vue'; +export { default as LoginButton } from './LoginButton.vue'; +export { default as Spinner } from './Spinner.vue'; +export { default as Navbar } from './Navbar.vue'; +export { default as ProgressLoader } from './ProgressLoader.vue'; diff --git a/bcbox/frontend/src/components/object/DeleteObjectButton.vue b/bcbox/frontend/src/components/object/DeleteObjectButton.vue new file mode 100644 index 00000000..de2b4c23 --- /dev/null +++ b/bcbox/frontend/src/components/object/DeleteObjectButton.vue @@ -0,0 +1,97 @@ + + + diff --git a/bcbox/frontend/src/components/object/DownloadObjectButton.vue b/bcbox/frontend/src/components/object/DownloadObjectButton.vue new file mode 100644 index 00000000..98e6cca0 --- /dev/null +++ b/bcbox/frontend/src/components/object/DownloadObjectButton.vue @@ -0,0 +1,80 @@ + + + diff --git a/bcbox/frontend/src/components/object/ObjectAccess.vue b/bcbox/frontend/src/components/object/ObjectAccess.vue new file mode 100644 index 00000000..f3a4ac04 --- /dev/null +++ b/bcbox/frontend/src/components/object/ObjectAccess.vue @@ -0,0 +1,72 @@ + + + diff --git a/bcbox/frontend/src/components/object/ObjectFileDetails.vue b/bcbox/frontend/src/components/object/ObjectFileDetails.vue new file mode 100644 index 00000000..8787e50b --- /dev/null +++ b/bcbox/frontend/src/components/object/ObjectFileDetails.vue @@ -0,0 +1,255 @@ + + + + + diff --git a/bcbox/frontend/src/components/object/ObjectFilters.vue b/bcbox/frontend/src/components/object/ObjectFilters.vue new file mode 100644 index 00000000..6a81a2f9 --- /dev/null +++ b/bcbox/frontend/src/components/object/ObjectFilters.vue @@ -0,0 +1,194 @@ + + + + + diff --git a/bcbox/frontend/src/components/object/ObjectList.vue b/bcbox/frontend/src/components/object/ObjectList.vue new file mode 100644 index 00000000..dcddd191 --- /dev/null +++ b/bcbox/frontend/src/components/object/ObjectList.vue @@ -0,0 +1,147 @@ + + + + + diff --git a/bcbox/frontend/src/components/object/ObjectMetadata.vue b/bcbox/frontend/src/components/object/ObjectMetadata.vue new file mode 100644 index 00000000..1d28d8a5 --- /dev/null +++ b/bcbox/frontend/src/components/object/ObjectMetadata.vue @@ -0,0 +1,154 @@ + + + diff --git a/bcbox/frontend/src/components/object/ObjectMetadataTagForm.vue b/bcbox/frontend/src/components/object/ObjectMetadataTagForm.vue new file mode 100644 index 00000000..b1d55b22 --- /dev/null +++ b/bcbox/frontend/src/components/object/ObjectMetadataTagForm.vue @@ -0,0 +1,216 @@ + + + + + diff --git a/bcbox/frontend/src/components/object/ObjectPermission.vue b/bcbox/frontend/src/components/object/ObjectPermission.vue new file mode 100644 index 00000000..999f9448 --- /dev/null +++ b/bcbox/frontend/src/components/object/ObjectPermission.vue @@ -0,0 +1,222 @@ + + + + + diff --git a/bcbox/frontend/src/components/object/ObjectPermissionAddUser.vue b/bcbox/frontend/src/components/object/ObjectPermissionAddUser.vue new file mode 100644 index 00000000..f5ed70ef --- /dev/null +++ b/bcbox/frontend/src/components/object/ObjectPermissionAddUser.vue @@ -0,0 +1,36 @@ + + + diff --git a/bcbox/frontend/src/components/object/ObjectProperties.vue b/bcbox/frontend/src/components/object/ObjectProperties.vue new file mode 100644 index 00000000..128248d4 --- /dev/null +++ b/bcbox/frontend/src/components/object/ObjectProperties.vue @@ -0,0 +1,106 @@ + + + diff --git a/bcbox/frontend/src/components/object/ObjectPublicToggle.vue b/bcbox/frontend/src/components/object/ObjectPublicToggle.vue new file mode 100644 index 00000000..637451bd --- /dev/null +++ b/bcbox/frontend/src/components/object/ObjectPublicToggle.vue @@ -0,0 +1,53 @@ + + + diff --git a/bcbox/frontend/src/components/object/ObjectSidebar.vue b/bcbox/frontend/src/components/object/ObjectSidebar.vue new file mode 100644 index 00000000..3ede4bcf --- /dev/null +++ b/bcbox/frontend/src/components/object/ObjectSidebar.vue @@ -0,0 +1,119 @@ + + + + + diff --git a/bcbox/frontend/src/components/object/ObjectTable.vue b/bcbox/frontend/src/components/object/ObjectTable.vue new file mode 100644 index 00000000..4ea3dec7 --- /dev/null +++ b/bcbox/frontend/src/components/object/ObjectTable.vue @@ -0,0 +1,301 @@ + + + + diff --git a/bcbox/frontend/src/components/object/ObjectTag.vue b/bcbox/frontend/src/components/object/ObjectTag.vue new file mode 100644 index 00000000..7ec024d4 --- /dev/null +++ b/bcbox/frontend/src/components/object/ObjectTag.vue @@ -0,0 +1,155 @@ + + + diff --git a/bcbox/frontend/src/components/object/ObjectUpload.vue b/bcbox/frontend/src/components/object/ObjectUpload.vue new file mode 100644 index 00000000..d9ac65be --- /dev/null +++ b/bcbox/frontend/src/components/object/ObjectUpload.vue @@ -0,0 +1,191 @@ + + + diff --git a/bcbox/frontend/src/components/object/ObjectUploadBasic.vue b/bcbox/frontend/src/components/object/ObjectUploadBasic.vue new file mode 100644 index 00000000..5698262e --- /dev/null +++ b/bcbox/frontend/src/components/object/ObjectUploadBasic.vue @@ -0,0 +1,159 @@ + + + diff --git a/bcbox/frontend/src/components/object/ObjectUploadFile.vue b/bcbox/frontend/src/components/object/ObjectUploadFile.vue new file mode 100644 index 00000000..bc4ea57a --- /dev/null +++ b/bcbox/frontend/src/components/object/ObjectUploadFile.vue @@ -0,0 +1,135 @@ + + + diff --git a/bcbox/frontend/src/components/object/ObjectVersion.vue b/bcbox/frontend/src/components/object/ObjectVersion.vue new file mode 100644 index 00000000..21fdd6d8 --- /dev/null +++ b/bcbox/frontend/src/components/object/ObjectVersion.vue @@ -0,0 +1,211 @@ + + + diff --git a/bcbox/frontend/src/components/object/index.ts b/bcbox/frontend/src/components/object/index.ts new file mode 100644 index 00000000..f0781066 --- /dev/null +++ b/bcbox/frontend/src/components/object/index.ts @@ -0,0 +1,19 @@ +export { default as DeleteObjectButton } from './DeleteObjectButton.vue'; +export { default as DownloadObjectButton } from './DownloadObjectButton.vue'; +export { default as ObjectAccess } from './ObjectAccess.vue'; +export { default as ObjectFileDetails } from './ObjectFileDetails.vue'; +export { default as ObjectFilters } from './ObjectFilters.vue'; +export { default as ObjectList } from './ObjectList.vue'; +export { default as ObjectMetadata } from './ObjectMetadata.vue'; +export { default as ObjectMetadataTagForm } from './ObjectMetadataTagForm.vue'; +export { default as ObjectPermission } from './ObjectPermission.vue'; +export { default as ObjectPermissionAddUser } from './ObjectPermissionAddUser.vue'; +export { default as ObjectProperties } from './ObjectProperties.vue'; +export { default as ObjectPublicToggle } from './ObjectPublicToggle.vue'; +export { default as ObjectSidebar } from './ObjectSidebar.vue'; +export { default as ObjectTable } from './ObjectTable.vue'; +export { default as ObjectTag } from './ObjectTag.vue'; +export { default as ObjectUpload } from './ObjectUpload.vue'; +export { default as ObjectUploadBasic } from './ObjectUploadBasic.vue'; +export { default as ObjectUploadFile } from './ObjectUploadFile.vue'; +export { default as ObjectVersion } from './ObjectVersion.vue'; diff --git a/bcbox/frontend/src/components/object/share/ShareLinkContent.vue b/bcbox/frontend/src/components/object/share/ShareLinkContent.vue new file mode 100644 index 00000000..229ff2fe --- /dev/null +++ b/bcbox/frontend/src/components/object/share/ShareLinkContent.vue @@ -0,0 +1,56 @@ + + + + + diff --git a/bcbox/frontend/src/components/object/share/ShareObjectButton.vue b/bcbox/frontend/src/components/object/share/ShareObjectButton.vue new file mode 100644 index 00000000..69412085 --- /dev/null +++ b/bcbox/frontend/src/components/object/share/ShareObjectButton.vue @@ -0,0 +1,108 @@ + + + + + diff --git a/bcbox/frontend/src/components/object/share/index.ts b/bcbox/frontend/src/components/object/share/index.ts new file mode 100644 index 00000000..ff46b2d4 --- /dev/null +++ b/bcbox/frontend/src/components/object/share/index.ts @@ -0,0 +1,2 @@ +export { default as ShareLinkContent } from './ShareLinkContent.vue'; +export { default as ShareObjectButton } from './ShareObjectButton.vue'; diff --git a/bcbox/frontend/src/composables/useAlert.ts b/bcbox/frontend/src/composables/useAlert.ts new file mode 100644 index 00000000..9ad86ca0 --- /dev/null +++ b/bcbox/frontend/src/composables/useAlert.ts @@ -0,0 +1,16 @@ +import { useConfirm } from '@/lib/primevue'; + +export function useAlert(header: string, message: string) { + const confirm = useConfirm(); + + const show = () => { + confirm.require({ + header: header, + message: message, + acceptLabel: 'OK', + rejectClass: 'hidden' + }); + }; + + return { show }; +} diff --git a/bcbox/frontend/src/interfaces/IAudit.ts b/bcbox/frontend/src/interfaces/IAudit.ts new file mode 100644 index 00000000..c46da846 --- /dev/null +++ b/bcbox/frontend/src/interfaces/IAudit.ts @@ -0,0 +1,6 @@ +export interface IAudit { + createdBy?: string; + createdAt?: string; + updatedBy?: string; + updatedAt?: string; +} diff --git a/bcbox/frontend/src/interfaces/IChangeEvent.ts b/bcbox/frontend/src/interfaces/IChangeEvent.ts new file mode 100644 index 00000000..59f74085 --- /dev/null +++ b/bcbox/frontend/src/interfaces/IChangeEvent.ts @@ -0,0 +1,3 @@ +export interface IChangeEvent extends Event { + value?: any; +} diff --git a/bcbox/frontend/src/interfaces/IInputEvent.ts b/bcbox/frontend/src/interfaces/IInputEvent.ts new file mode 100644 index 00000000..932fba2e --- /dev/null +++ b/bcbox/frontend/src/interfaces/IInputEvent.ts @@ -0,0 +1,3 @@ +export interface IInputEvent extends Event { + target: HTMLInputElement; +} diff --git a/bcbox/frontend/src/interfaces/index.ts b/bcbox/frontend/src/interfaces/index.ts new file mode 100644 index 00000000..cd333e54 --- /dev/null +++ b/bcbox/frontend/src/interfaces/index.ts @@ -0,0 +1,3 @@ +export type { IAudit } from './IAudit'; +export type { IChangeEvent } from './IChangeEvent'; +export type { IInputEvent } from './IInputEvent'; diff --git a/bcbox/frontend/src/lib/primevue/index.ts b/bcbox/frontend/src/lib/primevue/index.ts new file mode 100644 index 00000000..5bb47349 --- /dev/null +++ b/bcbox/frontend/src/lib/primevue/index.ts @@ -0,0 +1,31 @@ +// Add imports as needed +export { FilterMatchMode } from 'primevue/api'; +export { default as Badge } from 'primevue/badge'; +export { default as Button } from 'primevue/button'; +export { default as Checkbox } from 'primevue/checkbox'; +export { default as Column } from 'primevue/column'; +export { default as ConfirmDialog } from 'primevue/confirmdialog'; +export { default as DataTable } from 'primevue/datatable'; +export { default as Dialog } from 'primevue/dialog'; +export { default as Divider } from 'primevue/divider'; +export { default as Dropdown } from 'primevue/dropdown'; +export { default as FileUpload } from 'primevue/fileupload'; +export { default as InputSwitch } from 'primevue/inputswitch'; +export { default as InputText } from 'primevue/inputtext'; +export { default as Message } from 'primevue/message'; +export { default as MultiSelect } from 'primevue/multiselect'; +export { default as Password } from 'primevue/password'; +export { default as ProgressBar } from 'primevue/progressbar'; +export { default as ProgressSpinner } from 'primevue/progressspinner'; +export { default as RadioButton } from 'primevue/radiobutton'; +export { default as TabView } from 'primevue/tabview'; +export { default as TabPanel } from 'primevue/tabpanel'; +export { default as Tag } from 'primevue/tag'; +export { default as Toast } from 'primevue/toast'; +export { default as Toolbar } from 'primevue/toolbar'; + +export { useConfirm } from 'primevue/useconfirm'; + +export { useToast } from './useToast'; + +export type { DropdownChangeEvent } from 'primevue/dropdown'; diff --git a/bcbox/frontend/src/lib/primevue/useToast.ts b/bcbox/frontend/src/lib/primevue/useToast.ts new file mode 100644 index 00000000..92f0b4e7 --- /dev/null +++ b/bcbox/frontend/src/lib/primevue/useToast.ts @@ -0,0 +1,29 @@ +import { ToastTimeout } from '@/utils/constants'; +import type { ToastMessageOptions } from 'primevue/toast'; +import { useToast as useToastPrimevue } from 'primevue/usetoast'; + +export const useToast = () => { + const toast = useToastPrimevue(); + + const error = (title: string, msg: string = '', options: ToastMessageOptions = {}) => { + const { severity = 'error', summary = `Error: ${title}`, detail = msg, life = ToastTimeout.ERROR } = options; + toast.add({ severity: severity, summary: summary, detail: detail, life: life }); + }; + + const info = (title: string, msg: string = '', options: ToastMessageOptions = {}) => { + const { severity = 'info', summary = `Info: ${title}`, detail = msg, life = ToastTimeout.INFO } = options; + toast.add({ severity: severity, summary: summary, detail: detail, life: life }); + }; + + const success = (title: string, msg: string = '', options: ToastMessageOptions = {}) => { + const { severity = 'success', summary = `Success: ${title}`, detail = msg, life = ToastTimeout.SUCCESS } = options; + toast.add({ severity: severity, summary: summary, detail: detail, life: life }); + }; + + const warn = (title: string, msg: string = '', options: ToastMessageOptions = {}) => { + const { severity = 'warn', summary = `Warning: ${title}`, detail = msg, life = ToastTimeout.WARNING } = options; + toast.add({ severity: severity, summary: summary, detail: detail, life: life }); + }; + + return { error, info, success, warn }; +}; diff --git a/bcbox/frontend/src/main.ts b/bcbox/frontend/src/main.ts new file mode 100644 index 00000000..e3c27b3f --- /dev/null +++ b/bcbox/frontend/src/main.ts @@ -0,0 +1,61 @@ +import { library } from '@fortawesome/fontawesome-svg-core'; +import { fas } from '@fortawesome/free-solid-svg-icons'; +import { FontAwesomeIcon } from '@fortawesome/vue-fontawesome'; +import PrimeVue from 'primevue/config'; +import ConfirmationService from 'primevue/confirmationservice'; +import ToastService from 'primevue/toastservice'; +import Tooltip from 'primevue/tooltip'; +import { createPinia } from 'pinia'; +import { createPersistedState } from 'pinia-plugin-persistedstate'; +import { createApp } from 'vue'; + +import App from '@/App.vue'; +import getRouter from '@/router'; +import { AuthService, ConfigService } from '@/services'; + +import '@bcgov/bc-sans/css/BCSans.css'; +import 'primevue/resources/themes/saga-blue/theme.css'; +import 'primevue/resources/primevue.min.css'; +import 'primeicons/primeicons.css'; +import 'primeflex/primeflex.css'; +import '@/assets/main.scss'; + +/** + * @function initializeApp + * Initializes and mounts the Vue instance + */ +function initializeApp(): void { + library.add(fas); + + const app = createApp(App); + const pinia = createPinia(); + pinia.use( + createPersistedState({ + key: (id) => `bcbox.${id}` + }) + ); + + app.use(pinia); + app.use(getRouter()); + app.use(PrimeVue); + app.use(ToastService); + app.use(ConfirmationService); + app.component('FontAwesomeIcon', FontAwesomeIcon); + app.directive('tooltip', Tooltip); + + app.mount('#app'); +} + +/** + * @function initializeServices + * Initializes and mounts the service singletons + * Services must load in the following order: config, auth, then app. + * @param {Function} [next=undefined] Optional callback function + */ +async function initializeServices(next?: Function): Promise { + await ConfigService.init(); + await AuthService.init(); + if (next) next(); +} + +initializeServices(initializeApp); diff --git a/bcbox/frontend/src/router/index.ts b/bcbox/frontend/src/router/index.ts new file mode 100644 index 00000000..a3f40a5e --- /dev/null +++ b/bcbox/frontend/src/router/index.ts @@ -0,0 +1,157 @@ +import { createRouter, createWebHistory } from 'vue-router'; + +import { AuthService } from '@/services'; +import { useAppStore } from '@/store'; +import { RouteNames, StorageKey } from '@/utils/constants'; + +import type { RouteRecordRaw } from 'vue-router'; + +/** + * @function createProps + * Parses the route query and params to generate vue props + * @param {object} route The route object + * @returns {object} a Vue props object + */ +function createProps(route: { query: any; params: any }): object { + return { ...route.query, ...route.params }; +} + +const routes: Array = [ + { + path: '/', + name: RouteNames.HOME, + component: () => import('../views/HomeView.vue'), + meta: { title: 'Home' } + }, + { + path: '/detail', + component: () => import('@/views/GenericView.vue'), + children: [ + { + path: 'objects', + name: RouteNames.DETAIL_OBJECTS, + component: () => import('@/views/detail/DetailObjectsView.vue'), + props: createProps, + meta: { title: 'Object Details' } + } + ] + }, + { + path: '/developer', + name: RouteNames.DEVELOPER, + component: () => import('@/views/DeveloperView.vue'), + meta: { requiresAuth: true, breadcrumb: 'Developer', title: 'Developer' } + }, + { + path: '/list', + component: () => import('@/views/GenericView.vue'), + children: [ + { + path: 'buckets', + name: RouteNames.LIST_BUCKETS, + component: () => import('@/views/list/ListBucketsView.vue'), + meta: { requiresAuth: true, breadcrumb: 'Buckets', title: 'My Buckets' }, + props: createProps + }, + { + path: 'objects', + name: RouteNames.LIST_OBJECTS, + component: () => import('@/views/list/ListObjectsView.vue'), + meta: { requiresAuth: true, breadcrumb: '__listObjectsDynamic', title: 'My Objects' }, + props: createProps + } + ] + }, + { + path: '/oidc', + component: () => import('@/views/GenericView.vue'), + children: [ + { + path: 'callback', + name: RouteNames.CALLBACK, + component: () => import('@/views/oidc/OidcCallbackView.vue'), + meta: { title: 'Authenticating...' } + }, + { + path: 'login', + name: RouteNames.LOGIN, + component: () => import('@/views/oidc/OidcLoginView.vue'), + meta: { title: 'Logging in...' }, + beforeEnter: () => { + const entrypoint = `${window.location.pathname}${window.location.search}${window.location.hash}`; + window.sessionStorage.setItem(StorageKey.AUTH, entrypoint); + } + }, + { + path: 'logout', + name: RouteNames.LOGOUT, + component: () => import('@/views/oidc/OidcLogoutView.vue'), + meta: { title: 'Logging out...' } + } + ] + }, + { + path: '/forbidden', + name: RouteNames.FORBIDDEN, + component: () => import('@/views/Forbidden.vue'), + meta: { title: 'Forbidden' } + }, + { + path: '/:pathMatch(.*)*', + name: 'NotFound', + component: () => import('@/views/NotFound.vue'), + meta: { title: 'Not Found' } + } +]; + +export default function getRouter() { + const appStore = useAppStore(); + // const navStore = useNavStore(); // Removed for now + const authService = new AuthService(); + const router = createRouter({ + history: createWebHistory(), + routes + }); + + router.beforeEach(async (to) => { + appStore.beginDeterminateLoading(); + // navStore.navigate(to); // Removed for now + + // Uploading navigation guard + if (appStore.getIsUploading) { + if ( + !confirm( + 'Navigation may cancel upload(s) in progress. ' + 'Please confirm you want to navigate from current page.' + ) + ) { + return false; + } + } + + // Backend Redirection Handler + if (to.query?.r) { + router.replace({ + path: to.query.r ? to.query.r.toString() : to.path, + // eslint-disable-next-line @typescript-eslint/no-unused-vars, no-unused-vars + query: (({ r, ...q }) => q)(to.query) + }); + } + + // Authentication Guard + if (to.meta.requiresAuth) { + const user = await authService.getUser(); + if (!user || user.expired) { + router.replace({ name: RouteNames.LOGIN }); + } + } + }); + + router.afterEach((to) => { + // Update document title + document.title = to.meta.title ? `BCBox - ${to.meta.title}` : 'BCBox'; + + appStore.endDeterminateLoading(); + }); + + return router; +} diff --git a/bcbox/frontend/src/services/authService.ts b/bcbox/frontend/src/services/authService.ts new file mode 100644 index 00000000..f4f784a7 --- /dev/null +++ b/bcbox/frontend/src/services/authService.ts @@ -0,0 +1,118 @@ +import { Log, UserManager, WebStorageStateStore } from 'oidc-client-ts'; + +import ConfigService from './configService'; + +import type { User, UserManagerSettings } from 'oidc-client-ts'; + +const isDebugMode: boolean = import.meta.env.MODE.toUpperCase() === 'DEBUG'; + +/** + * @class AuthService + * A singleton wrapper for managing user authentication + */ +export default class AuthService { + private static _instance: AuthService; + private static _userManager: UserManager; + + /** + * @constructor + */ + constructor() { + if (!AuthService._instance) { + AuthService._instance = this; + AuthService._userManager = new UserManager(this.getOidcSettings()); + + Log.setLogger(console); + Log.setLevel(isDebugMode ? Log.DEBUG : Log.INFO); + } + + return AuthService._instance; + } + + /** + * @function getOidcSettings + * Acquires OIDC settings from config + * @returns {UserManagerSettings} Yields OIDC settings + * @throws If OIDC configuration is missing or incomplete + */ + public getOidcSettings(): UserManagerSettings { + const config = new ConfigService().getConfig(); + + if (!config?.oidc?.authority || !config?.oidc?.clientId) { + throw new Error('OIDC is misconfigured'); + } + + return { + automaticSilentRenew: true, + authority: config.oidc.authority, + client_id: config.oidc.clientId, + redirect_uri: `${window.location.protocol}//${window.location.host}/oidc/callback`, + loadUserInfo: true, + post_logout_redirect_uri: `${window.location.protocol}//${window.location.host}/oidc/logout`, + userStore: new WebStorageStateStore({ store: window.localStorage }) + }; + } + + /** + * @function init + * Initializes the AuthService singleton + * @returns {Promise} An instance of AuthService + */ + public static async init(): Promise { + return new Promise((resolve) => { + const authService = new AuthService(); + resolve(authService); + }); + } + + /** + * @function getUser + * Returns the raw OIDC current user information + * @returns {Promise} Returns a user object if logged in, null otherwise + */ + public async getUser(): Promise { + return AuthService._userManager.getUser(); + } + + /** + * @function getUserManager + * Returns the OIDC user manager + * @returns {Promise} Returns a user object if logged in, null otherwise + */ + public getUserManager(): UserManager { + return AuthService._userManager; + } + + /** + * @function login + * Performs the OIDC user login flow + * @returns {Promise} + */ + public async login(): Promise { + return AuthService._userManager.signinRedirect({ + redirectMethod: 'replace', + extraQueryParams: { kc_idp_hint: 'bceid' } + }); + } + + /** + * @function loginCallback + * Handles the OIDC callback user login flow + * @returns {Promise} Resolves upon completion + */ + public async loginCallback(): Promise { + // Register and store user to local storage + await AuthService._userManager.signinRedirectCallback(); + // signinRedirectCallback appears to do this already? + // await AuthService._userManager.storeUser(user); + } + + /** + * @function logout + * Performs the OIDC user logout flow + * @returns {Promise} + */ + public async logout(): Promise { + return AuthService._userManager.signoutRedirect({ redirectMethod: 'replace' }); + } +} diff --git a/bcbox/frontend/src/services/bucketService.ts b/bcbox/frontend/src/services/bucketService.ts new file mode 100644 index 00000000..22f85a15 --- /dev/null +++ b/bcbox/frontend/src/services/bucketService.ts @@ -0,0 +1,58 @@ +import { comsAxios } from './interceptors'; + +import type { Bucket, SearchBucketsOptions } from '@/types'; + +const BUCKET_PATH = 'bucket'; + +export default { + /** + * @function searchForBuckets + * Returns a list of buckets + * @param {SearchBucketsOptions} params Optional object containing the data to filter against + * @returns {Promise} An axios response + */ + searchBuckets(params?: SearchBucketsOptions) { + return comsAxios().get(`${BUCKET_PATH}`, { params: params }); + }, + + /** + * @function createBucket + * Creates a bucket + * @param {Bucket} data Bucket object containing the data to create bucket + * @returns {Promise} An axios response + */ + createBucket(data: Bucket) { + return comsAxios().put(`${BUCKET_PATH}`, data); + }, + + /** + * @function deleteBucket + * Deletes a bucket + * This is a COMS DB delete only. The S3 bucket remains intact + * @param {string} bucketId Bucket ID for the bucket to delete + * @returns {Promise} An axios response + */ + deleteBucket(bucketId: string) { + return comsAxios().delete(`${BUCKET_PATH}/${bucketId}`); + }, + + /** + * @function updateBucket + * Updates a bucket + * @param {Bucket} data Bucket object containing the data to update bucket + * @returns {Promise} An axios response + */ + updateBucket(bucketId: string, data: Bucket) { + return comsAxios().patch(`${BUCKET_PATH}/${bucketId}`, data); + }, + + /** + * @function syncBucket + * Synchronizes a bucket + * @param {string} bucketId Bucket ID for the bucket to synchronize + * @returns {Promise} An axios response + */ + syncBucket(bucketId: string) { + return comsAxios().get(`${BUCKET_PATH}/${bucketId}/sync`); + } +}; diff --git a/bcbox/frontend/src/services/configService.ts b/bcbox/frontend/src/services/configService.ts new file mode 100644 index 00000000..5ec94423 --- /dev/null +++ b/bcbox/frontend/src/services/configService.ts @@ -0,0 +1,65 @@ +import axios from 'axios'; + +import { StorageKey } from '@/utils/constants'; + +const storageType = window.sessionStorage; + +/** + * @class ConfigService + * A singleton wrapper for acquiring and storing configuration data + * in session storage + */ +export default class ConfigService { + private static _instance: ConfigService; + + /** + * @constructor + */ + constructor() { + if (!ConfigService._instance) { + ConfigService._instance = this; + } + + return ConfigService._instance; + } + + /** + * @function init + * Initializes the ConfigService singleton + * @returns {Promise} An instance of ConfigService + */ + public static async init(): Promise { + return new Promise((resolve, reject) => { + if (storageType.getItem(StorageKey.CONFIG) === null) { + axios + .get('/config') + .then(({ data }) => { + storageType.setItem(StorageKey.CONFIG, JSON.stringify(data)); + resolve(new ConfigService()); + }) + .catch((err) => { + storageType.removeItem(StorageKey.CONFIG); + reject(`Failed to initialize configuration: ${err}`); + }); + } else { + resolve(new ConfigService()); + } + }); + } + + /** + * @function getConfig + * Fetches and returns the config object if available + * @returns {any | undefined} The config object if available + */ + public getConfig(): any | undefined { + try { + const cfgString = storageType.getItem(StorageKey.CONFIG); + return cfgString ? JSON.parse(cfgString) : undefined; + } catch (err: unknown) { + // eslint-disable-next-line no-console + console.error(`Missing configuration: ${err}`); + return undefined; + } + } +} diff --git a/bcbox/frontend/src/services/index.ts b/bcbox/frontend/src/services/index.ts new file mode 100644 index 00000000..11810ae5 --- /dev/null +++ b/bcbox/frontend/src/services/index.ts @@ -0,0 +1,7 @@ +export { default as AuthService } from './authService'; +export { default as bucketService } from './bucketService'; +export { default as ConfigService } from './configService'; +export { default as objectService } from './objectService'; +export { default as permissionService } from './permissionService'; +export { default as userService } from './userService'; +export { default as versionService } from './versionService'; diff --git a/bcbox/frontend/src/services/interceptors.ts b/bcbox/frontend/src/services/interceptors.ts new file mode 100644 index 00000000..0b1ef69b --- /dev/null +++ b/bcbox/frontend/src/services/interceptors.ts @@ -0,0 +1,35 @@ +import axios from 'axios'; + +import { AuthService, ConfigService } from './index'; + +import type { AxiosInstance, AxiosRequestConfig, InternalAxiosRequestConfig } from 'axios'; + +/** + * @function comsAxios + * Returns an Axios instance for the COMS API + * @param {AxiosRequestConfig} options Axios request config options + * @returns {AxiosInstance} An axios instance + */ +export function comsAxios(options: AxiosRequestConfig = {}): AxiosInstance { + const instance = axios.create({ + baseURL: new ConfigService().getConfig().coms.apiPath, + timeout: 10000, + ...options + }); + + instance.interceptors.request.use( + async (cfg: InternalAxiosRequestConfig) => { + const authService = new AuthService(); + const user = await authService.getUser(); + if (!!user && !user.expired) { + cfg.headers.Authorization = `Bearer ${user.access_token}`; + } + return Promise.resolve(cfg); + }, + (error: Error) => { + return Promise.reject(error); + } + ); + + return instance; +} diff --git a/bcbox/frontend/src/services/objectService.ts b/bcbox/frontend/src/services/objectService.ts new file mode 100644 index 00000000..472025ba --- /dev/null +++ b/bcbox/frontend/src/services/objectService.ts @@ -0,0 +1,370 @@ +import ConfigService from './configService'; +import { comsAxios } from './interceptors'; +import { excludeMetaTag, setDispositionHeader } from '@/utils/utils'; + +import type { AxiosRequestConfig } from 'axios'; +import type { GetMetadataOptions, GetObjectTaggingOptions, MetadataPair, SearchObjectsOptions, Tag } from '@/types'; +import { ExcludeTypes } from '@/utils/enums'; + +const PATH = '/object'; + +export default { + /** + * @function createObject + * Post an object + * @param {any} object Object to be created + * @param {string} bucketId Bucket id containing the object + * @param {AxiosRequestConfig} axiosOptions Axios request config options + * @returns {Promise} An axios response + */ + async createObject( + object: any, + headers: { + metadata?: Array<{ key: string; value: string }>; + }, + params: { + bucketId?: string; + tagset?: Array<{ key: string; value: string }>; + }, + axiosOptions?: AxiosRequestConfig + ) { + // setDispositionHeader constructs header based on file name + // Content-Type defaults octet-stream if MIME type unavailable + const config = { + headers: { + 'Content-Disposition': setDispositionHeader(object.name), + 'Content-Type': object?.type ?? 'application/octet-stream' + }, + params: { + bucketId: params.bucketId, + tagset: {} + } + }; + + // Map the metadata if required + if (headers.metadata) { + config.headers = { + ...config.headers, + ...Object.fromEntries(headers.metadata.map((x: { key: string; value: string }) => [x.key, x.value])) + }; + } + + // Map the tagset if required + if (params.tagset) { + config.params.tagset = Object.fromEntries( + params.tagset.map((x: { key: string; value: string }) => [x.key, x.value]) + ); + } + + return comsAxios(axiosOptions).put(PATH, object, config); + }, + + /** + * @function deleteObject + * Delete an object + * @param {string} objectId The id for the object to delete + * @param {string} versionId An optional versionId + * @returns {Promise} An axios response + */ + deleteObject(objectId: string, versionId?: string) { + return comsAxios().delete(`${PATH}/${objectId}`, { + params: { + versionId: versionId + } + }); + }, + + /** + * @function deleteTagging + * Removes the specified set of tags from the object + * All tags in the tag-set will be removed from the object if no tags are specified + * @returns {Promise} An axios response + */ + deleteTagging(objectId: string, tagging: Array, versionId?: string) { + return comsAxios().delete(`${PATH}/${objectId}/tagging`, { + params: { + tagset: Object.fromEntries(tagging.map((x: { key: string; value: string }) => [x.key, x.value])), + versionId: versionId + } + }); + }, + + /** + * @function getMetadata + * Get an object's metadata + * @param {any} headers Optional request headers + * @param {GetMetadataOptions} params Optional query parameters + * @returns {Promise} An axios response + */ + getMetadata(headers: any = {}, params: GetMetadataOptions = {}) { + // remove objectId array if its first element is undefined + if (params.objectId && params.objectId[0] === undefined) delete params.objectId; + return ( + comsAxios() + .get(`${PATH}/metadata`, { headers: headers, params: params }) + // filter out a configured list of select metadata + .then((response) => ({ data: excludeMetaTag(ExcludeTypes.METADATA, response.data) })) + ); + }, + + /** + * @function getObjectTagging + * Get an objects tags + * @param {GetObjectTaggingOptions} params Optional query parameters + * @returns {Promise} An axios response + */ + getObjectTagging(params: GetObjectTaggingOptions = {}) { + return ( + comsAxios() + .get(`${PATH}/tagging`, { params: params }) + // filter out a configured list of select tags + .then((response) => ({ data: excludeMetaTag(ExcludeTypes.TAGSET, response.data) })) + ); + }, + + /** + * @function getObject + * Get an object + * @param {string} objectId The id for the object to get + * @param {string} versionId An optional versionId + */ + getObject(objectId: string, versionId?: string) { + // Running in 'url' download mode only, could add options for other modes if needed + return comsAxios() + .get(`${PATH}/${objectId}`, { + params: { + versionId: versionId, + download: 'url' + } + }) + .then((response) => { + const url = response.data; + window.open(url, '_blank'); + }); + }, + + /** + * @function headObject + * Get an object details (head call) + * @param {string} objectId The id for the object to get + * @returns {Promise} An axios response + */ + headObject(objectId: string) { + return comsAxios().head(`${PATH}/${objectId}`); + }, + + /** + * @function listObjectVersion + * Returns the object version history + * @param {string} objectId The id for the object to get + * @returns {Promise} An axios response + */ + listObjectVersion(objectId: string) { + return comsAxios().get(`${PATH}/${objectId}/version`); + }, + + /** + * @function replaceMetadata + * Creates a copy and new version of the object with the given metadata replacing the existing + * @returns {Promise} An axios response + */ + replaceMetadata(objectId: string, metadata: Array<{ key: string; value: string }>, versionId?: string) { + return comsAxios().put(`${PATH}/${objectId}/metadata`, undefined, { + headers: { + ...Object.fromEntries(metadata.map((x: { key: string; value: string }) => [x.key, x.value])) + }, + params: { + versionId: versionId + } + }); + }, + + /** + * @function replaceTagging + * Replace the existing tag-set of an object with the set of given tags + * @returns {Promise} An axios response + */ + replaceTagging(objectId: string, tagging: Array, versionId?: string) { + return comsAxios().put(`${PATH}/${objectId}/tagging`, undefined, { + params: { + tagset: Object.fromEntries(tagging.map((x: { key: string; value: string }) => [x.key, x.value])), + versionId: versionId + } + }); + }, + + /** + * @function searchMetadata + * Gets a list of tags matching the given parameters + * @param {Object} headers Optional request headers + * @returns {Promise} An axios response + */ + searchMetadata(headers: { metadata?: Array }) { + const config = { + headers: {} + }; + + // Map the metadata if required + if (headers.metadata) { + config.headers = { + ...Object.fromEntries(headers.metadata.map((x: { key: string; value: string }) => [x.key, x.value])) + }; + } + return ( + comsAxios() + .get(`${PATH}/metadata`, config) + // filter out a configured list of select metadata + .then((response) => ({ data: excludeMetaTag(ExcludeTypes.METADATA, response.data) })) + ); + }, + + /** + * @function searchObjects + * List and search for all objects + * @param {SearchObjectsOptions} params Optional query parameters + * @returns {Promise} An axios response + */ + async searchObjects(params: SearchObjectsOptions = {}, headers: any = {}) { + // remove objectId array if its first element is undefined + if (params.objectId && params.objectId[0] === undefined) delete params.objectId; + + if (params.objectId) { + /** + * split calls to COMS if query params (eg objectId's) + * will cause url length to excede 2000 characters + * see: https://stackoverflow.com/questions/417142/what-is-the-maximum-length-of-a-url-in-different-browsers + * + * TODO: consider creating a utils function + * eg: `divideParam(params, attr)` + * ... + * return Promise.all(divideParam(params, objectId) + * .map(zparam => comsAxios().get(PATH, {params: zparam, headers: headers}); + */ + let urlLimit = 2000; + + const baseUrl = new URL(`${new ConfigService().getConfig().coms.apiPath}${PATH}`).toString(); + urlLimit -= baseUrl.length; // subtract baseUrl length + if (params.deleteMarker) urlLimit -= 19; // subtract `deleteMarker=false` + if (params.latest) urlLimit -= 13; // subtract `latest=false` + if (params.bucketId) urlLimit -= 48; // subtract a single bucketId `bucketId[]=` + // if tagset parameters passed + if (params.tagset) { + type TagsetObjectEntries = { + [K in keyof Tag]: [K, Tag[K]]; + }[keyof Tag][]; + for (const [key, value] of Object.entries(params.tagset) as TagsetObjectEntries) { + urlLimit -= 10 + key.length + value.length; + } + } + + // number of allowed objectId's in each group - 48 chars for each objectId (&objectId[]=) + const space = urlLimit; + const groupSize = Math.floor(space / 48); + + // loop through each group and push COMS result to `groups` array + const iterations = Math.ceil(params.objectId.length / groupSize); + const groups = []; + for (let i = 0; i < iterations; i++) { + const ids = params.objectId.slice(i * groupSize, i * groupSize + groupSize); + groups.push(await comsAxios().get(PATH, { params: { ...params, objectId: ids }, headers: headers })); + } + return Promise.resolve({ data: groups.flatMap((result) => result.data) }); + } + // else just call COMS once + else { + return comsAxios().get(PATH, { params: params, headers: headers }); + } + }, + + /** + * @function searchTagging + * Gets a list of tags matching the given parameters + * @param {Array} tagset Query parameters to search on + * @returns {Promise} An axios response + */ + searchTagging(tagset: Array) { + return ( + comsAxios() + .get(`${PATH}/tagging`, { + params: { + tagset: Object.fromEntries(tagset.map((x: { key: string; value: string }) => [x.key, x.value])) + } + }) + // filter out a configured list of select tags + .then((response) => ({ data: excludeMetaTag(ExcludeTypes.TAGSET, response.data) })) + ); + }, + + /** + * @function togglePublic + * Toggles the public property for an object + * @param {string} objectId The id for the object + * @param {boolean} isPublic Boolean on public status + * @returns {Promise} An axios response + */ + togglePublic(objectId: string, isPublic: boolean) { + return comsAxios().patch(`${PATH}/${objectId}/public`, null, { + params: { + public: isPublic + } + }); + }, + + /** + * @function updateObject + * Update the object record (will add new version) + * @param {string} objectId The id for the object + * @param {any} object Object to be created + * @param {AxiosRequestConfig} axiosOptions Axios request config options + * @returns {Promise} An axios response + */ + async updateObject( + objectId: string, + object: any, + headers: { + metadata?: Array<{ key: string; value: string }>; + }, + params: { + tagset?: Array<{ key: string; value: string }>; + }, + axiosOptions?: AxiosRequestConfig + ) { + // setDispositionHeader constructs header based on file name + // Content-Type defaults octet-stream if MIME type unavailable + const config = { + headers: { + 'Content-Type': object?.type ?? 'application/octet-stream' + }, + params: { + tagset: {} + } + }; + + // Map the metadata if required + if (headers.metadata) { + config.headers = { + ...config.headers, + ...Object.fromEntries(headers.metadata.map((x: { key: string; value: string }) => [x.key, x.value])) + }; + } + + // Map the tagset if required + if (params.tagset) { + config.params.tagset = Object.fromEntries( + params.tagset.map((x: { key: string; value: string }) => [x.key, x.value]) + ); + } + + return comsAxios(axiosOptions).put(`${PATH}/${objectId}`, object, config); + }, + + /** + * @function syncObject + * Synchronize and object's data + * @param {string} objectId The id for the object + * @returns {Promise} An axios response + */ + syncObject(objectId: string) { + return comsAxios().get(`${PATH}/${objectId}/sync`); + } +}; diff --git a/bcbox/frontend/src/services/permissionService.ts b/bcbox/frontend/src/services/permissionService.ts new file mode 100644 index 00000000..4406ab53 --- /dev/null +++ b/bcbox/frontend/src/services/permissionService.ts @@ -0,0 +1,106 @@ +import { comsAxios } from './interceptors'; + +import type { + BucketAddPermissionsOptions, + BucketDeletePermissionsOptions, + BucketGetPermissionsOptions, + BucketSearchPermissionsOptions, + ObjectAddPermissionsOptions, + ObjectDeletePermissionsOptions, + ObjectGetPermissionsOptions, + ObjectSearchPermissionsOptions +} from '@/types'; + +const PATH = 'permission'; +const BUCKET = 'bucket'; +const OBJECT = 'object'; + +export default { + /** + * @function bucketAddPermissions + * Adds the given permissions to the bucket + * @param {string} bucketId ID of the bucket to add permissions to + * @param {Array} data Array containing permissions to add + * @returns {Promise} An axios response + */ + bucketAddPermissions(bucketId: string, data: Array) { + return comsAxios().put(`${PATH}/${BUCKET}/${bucketId}`, data); + }, + + /** + * @function bucketDeletePermission + * Deletes the given permission from the bucket + * @param {string} bucketId ID of the bucket to remove permissions from + * @param {BucketDeletePermissionsOptions} params Object containing the permission to remove + * @returns {Promise} An axios response + */ + bucketDeletePermission(bucketId: string, params: BucketDeletePermissionsOptions) { + return comsAxios().delete(`${PATH}/${BUCKET}/${bucketId}`, { + params: params + }); + }, + + /** + * @function bucketSearchPermissions + * Returns a list of bucket permissions + * @param {BucketSearchPermissionsOptions} params Optional object containing the data to filter against + * @returns {Promise} An axios response + */ + bucketSearchPermissions(params?: BucketSearchPermissionsOptions) { + return comsAxios().get(`${PATH}/${BUCKET}`, { params: params }); + }, + + /** + * @function bucketGetPermissions + * Returns a list of permissions for the bucket + * @param {string} objectId ID of the bucket + * @param {BucketGetPermissionsOptions} params Optional object containing the data to filter against + * @returns {Promise} An axios response + */ + bucketGetPermissions(bucketId: string, params?: BucketGetPermissionsOptions) { + return comsAxios().get(`${PATH}/${OBJECT}/${bucketId}`, { params: params }); + }, + + /** + * @function objectAddPermissions + * Adds the given permissions to the object + * @param {string} objectId ID of the object to add permissions to + * @param {Array} data Array containing permissions to add + * @returns {Promise} An axios response + */ + objectAddPermissions(objectId: string, data: Array) { + return comsAxios().put(`${PATH}/${OBJECT}/${objectId}`, data); + }, + + /** + * @function objectDeletePermission + * Deletes the given permission from the object + * @param {string} objectId ID of the object to remove permissions from + * @param {ObjectDeletePermissionsOptions} params Object containing the permission to remove + * @returns {Promise} An axios response + */ + objectDeletePermission(objectId: string, params: ObjectDeletePermissionsOptions) { + return comsAxios().delete(`${PATH}/${OBJECT}/${objectId}`, { params: params }); + }, + + /** + * @function objectSearchPermissions + * Returns a list of object permissions + * @param {ObjectSearchPermissionsOptions} params Optional object containing the data to filter against + * @returns {Promise} An axios response + */ + objectSearchPermissions(params?: ObjectSearchPermissionsOptions) { + return comsAxios().get(`${PATH}/${OBJECT}`, { params: params }); + }, + + /** + * @function objectGetPermissions + * Returns a list of permissions for the object + * @param {string} objectId ID of the object + * @param {ObjectGetPermissionsOptions} params Optional object containing the data to filter against + * @returns {Promise} An axios response + */ + objectGetPermissions(objectId: string, params?: ObjectGetPermissionsOptions) { + return comsAxios().get(`${PATH}/${OBJECT}/${objectId}`, { params: params }); + } +}; diff --git a/bcbox/frontend/src/services/userService.ts b/bcbox/frontend/src/services/userService.ts new file mode 100644 index 00000000..05ac47a6 --- /dev/null +++ b/bcbox/frontend/src/services/userService.ts @@ -0,0 +1,44 @@ +import { comsAxios } from './interceptors'; + +import type { AxiosResponse } from 'axios'; +import type { SearchUsersOptions } from '@/types'; + +import { SYSTEM_USER } from '@/utils/constants'; + +const PATH = 'user'; + +export default { + /** + * @function searchForUsers + * Returns a list of users based on the provided filtering parameters + * @param {SearchUsersOptions} params SearchUsersOptions object containing the data to filter against + * @returns {Promise} An axios response or empty array + */ + searchForUsers(params: SearchUsersOptions): Promise { + // Filter out SYSTEM_USER from userId param + const userIds = params.userId?.filter((id) => id !== null && id !== SYSTEM_USER); + // Drop userId param if it only contains the system user or assign back to params object, without duplicates + if (userIds) { + if (userIds.length === 0) { + delete params.userId; + } else { + params.userId = [...new Set(userIds)]; + } + } + + if (Object.keys(params).length) { + return comsAxios().get(`${PATH}`, { params: params }); + } else { + return Promise.resolve({ data: [] } as AxiosResponse); + } + }, + + /** + * @function listIdps + * Fetch identity providers + * @returns {Promise} An axios response + */ + listIdps(): Promise { + return comsAxios().get(`${PATH}/idpList`); + } +}; diff --git a/bcbox/frontend/src/services/versionService.ts b/bcbox/frontend/src/services/versionService.ts new file mode 100644 index 00000000..bb782f6a --- /dev/null +++ b/bcbox/frontend/src/services/versionService.ts @@ -0,0 +1,37 @@ +import { comsAxios } from './interceptors'; +import { excludeMetaTag } from '@/utils/utils'; + +import type { GetVersionMetadataOptions, GetVersionTaggingOptions } from '@/types'; +import { ExcludeTypes } from '@/utils/enums'; + +const PATH = '/version'; + +export default { + /** + * @function getMetadata + * Get an object's metadata + * @returns {Promise} An axios response + */ + getMetadata(headers: any = {}, params: GetVersionMetadataOptions) { + return ( + comsAxios() + .get(`${PATH}/metadata`, { headers: headers, params: params }) + // filter out a configured list of select metadata + .then((response) => ({ data: excludeMetaTag(ExcludeTypes.METADATA, response.data) })) + ); + }, + + /** + * @function getObjectTagging + * Get an objects tags + * @returns {Promise} An axios response + */ + getObjectTagging(params: GetVersionTaggingOptions) { + return ( + comsAxios() + .get(`${PATH}/tagging`, { params: params }) + // filter out a configured list of select tags + .then((response) => ({ data: excludeMetaTag(ExcludeTypes.TAGSET, response.data) })) + ); + } +}; diff --git a/bcbox/frontend/src/shims.d.ts b/bcbox/frontend/src/shims.d.ts new file mode 100644 index 00000000..506bf2e5 --- /dev/null +++ b/bcbox/frontend/src/shims.d.ts @@ -0,0 +1,5 @@ +declare module '*.vue' { + import { DefineComponent } from 'vue'; + const component: DefineComponent<{}, {}, any>; + export default component; +} diff --git a/bcbox/frontend/src/store/appStore.ts b/bcbox/frontend/src/store/appStore.ts new file mode 100644 index 00000000..3fd0af98 --- /dev/null +++ b/bcbox/frontend/src/store/appStore.ts @@ -0,0 +1,97 @@ +import { defineStore } from 'pinia'; +import { computed, ref } from 'vue'; + +import type { Ref } from 'vue'; + +export type AppStoreState = { + loadingCalls: Ref; + loadingInterval: Ref | undefined>; + loadingMode: Ref<'determinate' | 'indeterminate'>; + loadingValue: Ref; + uploadingCalls: Ref; +}; + +export const useAppStore = defineStore('app', () => { + // State + const state: AppStoreState = { + loadingCalls: ref(0), + loadingInterval: ref(undefined), + loadingMode: ref('indeterminate'), + loadingValue: ref(0), + uploadingCalls: ref(0) + }; + + // Getters + const getters = { + getIsLoading: computed(() => state.loadingCalls.value > 0), + getIsUploading: computed(() => state.uploadingCalls.value > 0), + getLoadingCalls: computed(() => state.loadingCalls.value), + getLoadingMode: computed(() => state.loadingMode.value), + getLoadingValue: computed(() => state.loadingValue.value) + }; + + // Actions + function beginDeterminateLoading() { + state.loadingValue.value = 0; + ++state.loadingCalls.value; + state.loadingMode.value = 'determinate'; + state.loadingInterval.value = setInterval(() => { + let newValue = state.loadingValue.value + Math.floor(Math.random() * 10) + 1; + if (newValue >= 100) newValue = 100; + state.loadingValue.value = newValue; + }, 1000); + } + + function beginIndeterminateLoading() { + ++state.loadingCalls.value; + state.loadingMode.value = 'indeterminate'; + } + + function endDeterminateLoading() { + state.loadingValue.value = 100; + setTimeout(() => { + clearInterval(state.loadingInterval.value); + state.loadingInterval.value = undefined; + --state.loadingCalls.value; + }, 300); + } + + function endIndeterminateLoading() { + setTimeout(() => { + --state.loadingCalls.value; + }, 300); + } + + function beginUploading() { + ++state.uploadingCalls.value; + } + + function clearUploads() { + state.uploadingCalls.value = 0; + } + + function endUploading() { + if (--state.uploadingCalls.value < 0) { + state.loadingCalls.value = 0; // Safeguard negatives + } + } + + return { + // State + ...state, + + // Getters + ...getters, + + // Actions + beginDeterminateLoading, + beginIndeterminateLoading, + endDeterminateLoading, + endIndeterminateLoading, + beginUploading, + clearUploads, + endUploading + }; +}); + +export default useAppStore; diff --git a/bcbox/frontend/src/store/authStore.ts b/bcbox/frontend/src/store/authStore.ts new file mode 100644 index 00000000..81151421 --- /dev/null +++ b/bcbox/frontend/src/store/authStore.ts @@ -0,0 +1,128 @@ +import { defineStore } from 'pinia'; +import { computed, ref } from 'vue'; + +import { AuthService, ConfigService, userService } from '@/services'; + +import type { IdTokenClaims, User } from 'oidc-client-ts'; +import type { Ref } from 'vue'; + +import type { IdentityProvider } from '@/types'; + +export type AuthStoreState = { + accessToken: Ref; + expiresAt: Ref; + identityId: Ref; + idToken: Ref; + isAuthenticated: Ref; + profile: Ref; + refreshToken: Ref; + scope: Ref; + user: Ref; + userId: Ref; +}; + +export const useAuthStore = defineStore('auth', () => { + const configService = new ConfigService(); + const authService = new AuthService(); + const userManager = authService.getUserManager(); + + // State + const state: AuthStoreState = { + accessToken: ref(undefined), + expiresAt: ref(0), + identityId: ref(undefined), + idToken: ref(undefined), + isAuthenticated: ref(false), + profile: ref(undefined), + refreshToken: ref(undefined), + scope: ref(undefined), + user: ref(null), + userId: ref(undefined) + }; + + // Getters + const getters = { + getAccessToken: computed(() => state.accessToken.value), + getExpiresAt: computed(() => state.expiresAt.value), + getIdentityId: computed(() => state.identityId.value), + getIdToken: computed(() => state.idToken.value), + getIsAuthenticated: computed(() => state.isAuthenticated.value), + getProfile: computed(() => state.profile.value), + getRefreshToken: computed(() => state.refreshToken.value), + getScope: computed(() => state.scope.value), + getUser: computed(() => state.user.value), + getUserId: computed(() => state.userId.value) + }; + + // Actions + function _registerEvents() { + userManager.events.addAccessTokenExpired(_updateState); + userManager.events.addAccessTokenExpiring(_updateState); + userManager.events.addSilentRenewError(_updateState); + userManager.events.addUserLoaded(_updateState); + userManager.events.addUserSessionChanged(_updateState); + userManager.events.addUserSignedIn(_updateState); + userManager.events.addUserSignedOut(_updateState); + userManager.events.addUserUnloaded(_updateState); + } + + async function _updateState() { + const user = await authService.getUser(); + const profile = user?.profile; + const isAuthenticated = !!user && !user.expired; + const identityId = configService + .getConfig() + .idpList.map((provider: IdentityProvider) => (profile ? profile[provider.identityKey] : undefined)) + .filter((item?: string) => item)[0]; + + state.accessToken.value = user?.access_token; + state.expiresAt.value = user?.expires_at; + state.identityId.value = identityId; + state.idToken.value = user?.id_token; + state.isAuthenticated.value = isAuthenticated; + state.profile.value = profile; + state.refreshToken.value = user?.refresh_token; + state.scope.value = user?.scope; + state.user.value = user; + state.userId.value = + isAuthenticated && identityId + ? (await userService.searchForUsers({ identityId: identityId })).data[0].userId + : undefined; + } + + async function init() { + await AuthService.init(); + _registerEvents(); + await _updateState(); + } + + async function login() { + return authService.login(); + } + + async function loginCallback() { + return authService.loginCallback(); + } + + async function logout() { + return authService.logout(); + } + + return { + // State + ...state, + + // Getters + ...getters, + + // Actions + _registerEvents, + _updateState, + init, + login, + loginCallback, + logout + }; +}); + +export default useAuthStore; diff --git a/bcbox/frontend/src/store/bucketStore.ts b/bcbox/frontend/src/store/bucketStore.ts new file mode 100644 index 00000000..d7b0d549 --- /dev/null +++ b/bcbox/frontend/src/store/bucketStore.ts @@ -0,0 +1,131 @@ +import { defineStore } from 'pinia'; +import { computed, ref } from 'vue'; + +import { useToast } from '@/lib/primevue'; +import { bucketService } from '@/services'; +import { useAppStore, usePermissionStore } from '@/store'; +import { partition } from '@/utils/utils'; + +import type { Ref } from 'vue'; +import type { Bucket, BucketSearchPermissionsOptions } from '@/types'; + +export type BucketStoreState = { + buckets: Ref>; +}; + +export const useBucketStore = defineStore('bucket', () => { + const toast = useToast(); + + // Store + const appStore = useAppStore(); + const permissionStore = usePermissionStore(); + + // State + const state: BucketStoreState = { + buckets: ref([]) + }; + + // Getters + const getters = { + getBuckets: computed(() => state.buckets.value) + }; + + // Actions + async function createBucket(bucket: Bucket) { + try { + appStore.beginIndeterminateLoading(); + + return (await bucketService.createBucket(bucket)).data; + } finally { + appStore.endIndeterminateLoading(); + } + } + + async function deleteBucket(bucketId: string) { + try { + appStore.beginIndeterminateLoading(); + + await bucketService.deleteBucket(bucketId); + } finally { + appStore.endIndeterminateLoading(); + } + } + + async function fetchBuckets(params?: BucketSearchPermissionsOptions) { + try { + appStore.beginIndeterminateLoading(); + + // Get a unique list of bucket IDs the user has access to + const permResponse = await permissionStore.fetchBucketPermissions(params); + if (permResponse) { + const uniqueIds: Array = [ + ...new Set(permResponse.map((x: { bucketId: string }) => x.bucketId)) + ]; + + let response = Array(); + if (uniqueIds.length) { + response = (await bucketService.searchBuckets({ bucketId: uniqueIds })).data; + + // Remove old values matching search parameters + const matches = (x: Bucket) => !params?.bucketId || x.bucketId === params.bucketId; + + const [, difference] = partition(state.buckets.value, matches); + + // Merge and assign + state.buckets.value = difference.concat(response); + } else { + state.buckets.value = response; + } + } + } catch (error: any) { + toast.error('Fetching buckets', error); + } finally { + appStore.endIndeterminateLoading(); + } + } + + function findBucketById(bucketId: string) { + return state.buckets.value.find((x) => x.bucketId === bucketId); + } + + async function updateBucket(bucketId: string, bucket: Bucket) { + try { + appStore.beginIndeterminateLoading(); + + return (await bucketService.updateBucket(bucketId, bucket)).data; + } finally { + appStore.endIndeterminateLoading(); + } + } + + async function syncBucket(bucketId: string) { + try { + appStore.beginIndeterminateLoading(); + + await bucketService.syncBucket(bucketId); + toast.success('', 'Sync is in queue and will begin soon'); + } catch (error: any) { + toast.error('Unable to sync', error); + } finally { + appStore.endIndeterminateLoading(); + } + } + + return { + // State + ...state, + + // Getters + ...getters, + + // Actions + createBucket, + deleteBucket, + fetchBuckets, + findBucketById, + syncBucket, + updateBucket + }; +}); + +export default useBucketStore; diff --git a/bcbox/frontend/src/store/configStore.ts b/bcbox/frontend/src/store/configStore.ts new file mode 100644 index 00000000..cecee7bf --- /dev/null +++ b/bcbox/frontend/src/store/configStore.ts @@ -0,0 +1,44 @@ +import { defineStore } from 'pinia'; +import { computed, ref } from 'vue'; + +import { ConfigService } from '@/services'; + +import type { Ref } from 'vue'; + +export type ConfigStoreState = { + config: Ref; +}; + +export const useConfigStore = defineStore('config', () => { + const configService = new ConfigService(); + + // State + const state: ConfigStoreState = { + config: ref(null) + }; + + // Getters + const getters = { + getConfig: computed(() => state.config.value) + }; + + // Actions + async function init(): Promise { + await ConfigService.init(); + + state.config.value = configService.getConfig(); + } + + return { + // State + ...state, + + // Getters + ...getters, + + // Actions + init + }; +}); + +export default useConfigStore; diff --git a/bcbox/frontend/src/store/index.ts b/bcbox/frontend/src/store/index.ts new file mode 100644 index 00000000..2c3e9185 --- /dev/null +++ b/bcbox/frontend/src/store/index.ts @@ -0,0 +1,11 @@ +export { default as useAppStore } from './appStore'; +export { default as useAuthStore } from './authStore'; +export { default as useBucketStore } from './bucketStore'; +export { default as useConfigStore } from './configStore'; +export { default as useMetadataStore } from './metadataStore'; +export { default as useNavStore } from './navStore'; +export { default as useObjectStore } from './objectStore'; +export { default as usePermissionStore } from './permissionStore'; +export { default as useTagStore } from './tagStore'; +export { default as useUserStore } from './userStore'; +export { default as useVersionStore } from './versionStore'; diff --git a/bcbox/frontend/src/store/metadataStore.ts b/bcbox/frontend/src/store/metadataStore.ts new file mode 100644 index 00000000..e89d422a --- /dev/null +++ b/bcbox/frontend/src/store/metadataStore.ts @@ -0,0 +1,116 @@ +import { defineStore } from 'pinia'; +import { computed, ref } from 'vue'; + +import { useToast } from '@/lib/primevue'; +import { objectService } from '@/services'; +import { useAppStore } from '@/store'; +import { partition } from '@/utils/utils'; + +import type { Ref } from 'vue'; +import type { GetMetadataOptions, Metadata, MetadataPair } from '@/types'; + +export type MetadataStoreState = { + metadata: Ref>; + metadataSearchResults: Ref>; +}; + +export const useMetadataStore = defineStore('metadata', () => { + const appStore = useAppStore(); + const toast = useToast(); + + // State + const state: MetadataStoreState = { + metadata: ref([]), + metadataSearchResults: ref([]) + }; + + // Getters + const getters = { + getMetadata: computed(() => state.metadata.value), + getMetadataSearchResults: computed(() => state.metadataSearchResults.value) + }; + + // Actions + async function fetchMetadata(params: GetMetadataOptions = {}) { + try { + appStore.beginIndeterminateLoading(); + + const response = (await objectService.getMetadata(null, params)).data; + + // Remove old values matching search parameters + const matches = (x: Metadata) => + !params.objectId || + (Array.isArray(params.objectId) && params.objectId.some((y: string) => x.objectId === y)) || + (!Array.isArray(params.objectId) && params.objectId === x.objectId); + + const [, difference] = partition(state.metadata.value, matches); + + // Merge and assign + state.metadata.value = difference.concat(response); + } catch (error: any) { + toast.error('Fetching metadata', error); + } finally { + appStore.endIndeterminateLoading(); + } + } + + function findMetadataByObjectId(objectId: string) { + return state.metadata.value.find((x: Metadata) => x.objectId === objectId); + } + + function findValue(objectId: string, key: string) { + return findMetadataByObjectId(objectId)?.metadata.find((x) => x.key === key)?.value; + } + + async function replaceMetadata( + objectId: string, + metadata: Array<{ key: string; value: string }>, + versionId?: string + ) { + try { + appStore.beginIndeterminateLoading(); + // Ensure x-amz-meta- prefix exists + if (metadata) { + for (const meta of metadata) { + if (!meta.key.startsWith('x-amz-meta-')) { + meta.key = `x-amz-meta-${meta.key}`; + } + } + } + + await objectService.replaceMetadata(objectId, metadata, versionId); + await fetchMetadata({ objectId: objectId }); + } catch (error: any) { + toast.error('Updating metadata', error); + } finally { + appStore.endIndeterminateLoading(); + } + } + + async function searchMetadata(metadataSet: Array = []) { + try { + state.metadataSearchResults.value = []; + const response = (await objectService.searchMetadata({ metadata: metadataSet })).data; + state.metadataSearchResults.value = response; + } catch (error: any) { + toast.error('Searching metadata', error); + } + } + + return { + // State + ...state, + + // Getters + ...getters, + + // Actions + fetchMetadata, + findMetadataByObjectId, + findValue, + searchMetadata, + replaceMetadata + }; +}); + +export default useMetadataStore; diff --git a/bcbox/frontend/src/store/navStore.ts b/bcbox/frontend/src/store/navStore.ts new file mode 100644 index 00000000..98a606c9 --- /dev/null +++ b/bcbox/frontend/src/store/navStore.ts @@ -0,0 +1,63 @@ +import { defineStore } from 'pinia'; +import { ref } from 'vue'; + +import type { Ref } from 'vue'; +import type { RouteLocationNormalized } from 'vue-router'; + +export type NavStoreState = { + home: Ref; + items: Ref>; +}; + +export const useNavStore = defineStore('nav', () => { + // State + const state: NavStoreState = { + home: ref({ + label: 'Home', + to: '/' + }), + items: ref([]) + }; + + // Getters + const getters = {}; + + // Actions + function navigate(navLink: RouteLocationNormalized) { + // Get the path without any hash info + const fullPath = navLink.fullPath.split('#')[0]; + + // Check for existing path + const item = state.items.value.findIndex((x: any) => x.to === fullPath); + + if (navLink.path === '/') { + // Clear if going to Home + state.items.value = []; + } else if (item >= 0) { + // Navigating back to existing item, clear any items after + state.items.value.splice(item + 1); + } else if (navLink.meta?.breadcrumb) { + // Add new nav item + state.items.value.push({ label: navLink.meta.breadcrumb, to: fullPath }); + } + } + + function replace(oldLabel: string, newLabel: string) { + const item = state.items.value.find((x: any) => x.label === oldLabel); + if (item) item.label = newLabel; + } + + return { + // State + ...state, + + // Getters + ...getters, + + // Actions + navigate, + replace + }; +}); + +export default useNavStore; diff --git a/bcbox/frontend/src/store/objectStore.ts b/bcbox/frontend/src/store/objectStore.ts new file mode 100644 index 00000000..2664e74e --- /dev/null +++ b/bcbox/frontend/src/store/objectStore.ts @@ -0,0 +1,285 @@ +import { defineStore, storeToRefs } from 'pinia'; +import { computed, ref } from 'vue'; + +import { useToast } from '@/lib/primevue'; +import { objectService } from '@/services'; +import { useAppStore, useAuthStore, usePermissionStore } from '@/store'; +import { partition } from '@/utils/utils'; + +import type { AxiosRequestConfig } from 'axios'; +import type { Ref } from 'vue'; +import type { COMSObject, MetadataPair, ObjectSearchPermissionsOptions, Tag } from '@/types'; + +export type ObjectStoreState = { + objects: Ref>; + selectedObjects: Ref>; // All selected table row items + unfilteredObjectIds: Ref>; +}; + +export const useObjectStore = defineStore('object', () => { + const toast = useToast(); + + // Store + const appStore = useAppStore(); + const permissionStore = usePermissionStore(); + const { getUserId } = storeToRefs(useAuthStore()); + + // State + const state: ObjectStoreState = { + objects: ref([]), + selectedObjects: ref([]), + unfilteredObjectIds: ref([]) + }; + + // Getters + const getters = { + getObjects: computed(() => state.objects.value), + getSelectedObjects: computed(() => state.selectedObjects.value), + getUnfilteredObjectIds: computed(() => state.unfilteredObjectIds.value) + }; + + // Actions + async function createObject( + object: any, + headers: { + metadata?: Array<{ key: string; value: string }>; + }, + params: { + bucketId?: string; + tagset?: Array<{ key: string; value: string }>; + }, + axiosOptions?: AxiosRequestConfig + ) { + try { + appStore.beginIndeterminateLoading(); + + // Ensure x-amz-meta- prefix exists + if (headers.metadata) { + for (const meta of headers.metadata) { + if (!meta.key.startsWith('x-amz-meta-')) { + meta.key = `x-amz-meta-${meta.key}`; + } + } + } + + await objectService.createObject(object, headers, params, axiosOptions); + } catch (error: any) { + if (error?.response?.status === 409) { + toast.error('Creating object', 'File already exists'); + } else { + toast.error('Creating object', error); + } + } finally { + appStore.endIndeterminateLoading(); + } + } + + async function deleteObject(objectId: string, versionId?: string) { + const bucketId = findObjectById(objectId)?.bucketId; + + try { + appStore.beginIndeterminateLoading(); + await objectService.deleteObject(objectId, versionId); + removeSelectedObject(objectId); + toast.success('Object deleted'); + } catch (error: any) { + toast.error('deleting object.'); + throw error; + } finally { + fetchObjects({ bucketId: bucketId, userId: getUserId.value, bucketPerms: true }); + appStore.endIndeterminateLoading(); + } + } + + async function downloadObject(objectId: string, versionId?: string) { + try { + appStore.beginIndeterminateLoading(); + await objectService.getObject(objectId, versionId); + } catch (error: any) { + toast.error('Downloading object', error); + } finally { + appStore.endIndeterminateLoading(); + } + } + + async function fetchObjects( + params: ObjectSearchPermissionsOptions = {}, + tagset?: Array, + metadata?: Array + ) { + try { + appStore.beginIndeterminateLoading(); + + // Get a unique list of object IDs the user has access to + const permResponse = await permissionStore.fetchObjectPermissions(params); + + if (permResponse) { + const uniqueIds: Array = [ + ...new Set( + permResponse + .map((x: { objectId: string }) => x.objectId) + // Resolve API returning all objects with bucketPerms=true even when requesting single objectId + .filter((objectId: string) => !params.objectId || objectId === params.objectId) + ) + ]; + + let response = Array(); + if (uniqueIds.length) { + // If metadata specified, search for objects with matching metadata + const headers: Record = {}; + if (metadata?.length) { + for (const meta of metadata) { + headers[`x-amz-meta-${meta.key}`] = meta.value; + } + } + + response = await objectService + .searchObjects( + { + bucketId: params.bucketId ? [params.bucketId] : undefined, + objectId: uniqueIds, + tagset: tagset ? tagset.reduce((acc, cur) => ({ ...acc, [cur.key]: cur.value }), {}) : undefined, + + // Added to allow deletion of objects before versioning implementation + // TODO: Verify if needed after versioning implemented + deleteMarker: false, + latest: true + }, + headers + ) + .then((r) => r.data); + + // Remove old values matching search parameters + const matches = (x: COMSObject) => + (!params.objectId || x.id === params.objectId) && (!params.bucketId || x.bucketId === params.bucketId); + + const [, difference] = partition(state.objects.value, matches); + + // Merge and assign + state.objects.value = difference.concat(response); + + // Track all the object IDs in this bucket that the user would have access to in the table + // (even if filters are applied) + if (!tagset?.length && !metadata?.length) { + state.unfilteredObjectIds.value = state.objects.value + .filter((x) => !params.bucketId || x.bucketId === params.bucketId) + .map((o) => o.id); + } + } else { + state.objects.value = response; + state.unfilteredObjectIds.value = []; + } + } + } catch (error: any) { + toast.error('Fetching objects', error); + } finally { + appStore.endIndeterminateLoading(); + } + } + + function findObjectById(objectId: string) { + return state.objects.value.find((x) => x.id === objectId); + } + + async function headObject(objectId: string) { + try { + appStore.beginIndeterminateLoading(); + + // Return full response as data will always be No Content + return await objectService.headObject(objectId); + } catch (error: any) { + toast.error('Fetching head', error); + } finally { + appStore.endIndeterminateLoading(); + } + } + + function setSelectedObjects(selected: Array) { + state.selectedObjects.value = selected; + } + + function removeSelectedObject(removeSelected: string) { + state.selectedObjects.value = state.selectedObjects.value.filter((object) => { + return object.id != removeSelected; + }); + } + + async function togglePublic(objectId: string, isPublic: boolean) { + try { + appStore.beginIndeterminateLoading(); + await objectService.togglePublic(objectId, isPublic); + const obj = findObjectById(objectId); + if (obj) obj.public = isPublic; + } catch (error: any) { + toast.error('Changing public state', error); + } finally { + appStore.endIndeterminateLoading(); + } + } + + async function updateObject( + objectId: string, + object: any, + headers: { + metadata?: Array; + }, + params: { + tagset?: Array; + }, + axiosOptions?: AxiosRequestConfig + ) { + try { + appStore.beginIndeterminateLoading(); + + // Ensure x-amz-meta- prefix exists + if (headers.metadata) { + for (const meta of headers.metadata) { + if (!meta.key.startsWith('x-amz-meta-')) { + meta.key = `x-amz-meta-${meta.key}`; + } + } + } + + await objectService.updateObject(objectId, object, headers, params, axiosOptions); + } catch (error: any) { + toast.error('Updating object', error); + } finally { + appStore.endIndeterminateLoading(); + } + } + + async function syncObject(objectId: string) { + try { + appStore.beginIndeterminateLoading(); + await objectService.syncObject(objectId); + toast.success('', 'Sync is in queue and will begin soon'); + } catch (error: any) { + toast.error('Unable to sync', error); + } finally { + appStore.endIndeterminateLoading(); + } + } + + return { + // State + ...state, + + // Getters + ...getters, + + // Actions + createObject, + deleteObject, + downloadObject, + fetchObjects, + findObjectById, + headObject, + removeSelectedObject, + setSelectedObjects, + syncObject, + togglePublic, + updateObject + }; +}); + +export default useObjectStore; diff --git a/bcbox/frontend/src/store/permissionStore.ts b/bcbox/frontend/src/store/permissionStore.ts new file mode 100644 index 00000000..a3c71f41 --- /dev/null +++ b/bcbox/frontend/src/store/permissionStore.ts @@ -0,0 +1,347 @@ +import { defineStore, storeToRefs } from 'pinia'; +import { computed, ref } from 'vue'; + +import { useToast } from '@/lib/primevue'; +import { permissionService, userService } from '@/services'; +import { useAppStore, useAuthStore, useConfigStore } from '@/store'; +import { Permissions } from '@/utils/constants'; +import { partition } from '@/utils/utils'; + +import type { Ref } from 'vue'; +import type { + BucketPermission, + BucketSearchPermissionsOptions, + COMSObjectPermission, + ObjectSearchPermissionsOptions, + IdentityProvider, + Permission, + User, + UserPermissions +} from '@/types'; + +export type PermissionStoreState = { + bucketPermissions: Ref>; + mappedBucketToUserPermissions: Ref>; + mappedObjectToUserPermissions: Ref>; + objectPermissions: Ref>; + permissions: Ref>; +}; + +export const usePermissionStore = defineStore('permission', () => { + // Store + const appStore = useAppStore(); + const { getProfile } = storeToRefs(useAuthStore()); + const { getConfig } = storeToRefs(useConfigStore()); + const toast = useToast(); + + // State + const state: PermissionStoreState = { + bucketPermissions: ref([]), + mappedBucketToUserPermissions: ref([]), + mappedObjectToUserPermissions: ref([]), + objectPermissions: ref([]), + permissions: ref([]) + }; + + // Getters + const getters = { + getBucketPermissions: computed(() => state.bucketPermissions.value), + getMappedBucketToUserPermissions: computed(() => state.mappedBucketToUserPermissions.value), + getMappedObjectToUserPermissions: computed(() => state.mappedObjectToUserPermissions.value), + getObjectPermissions: computed(() => state.objectPermissions.value), + getPermissions: computed(() => state.permissions.value) + }; + + // Actions + async function addBucketPermission(bucketId: string, userId: string, permCode: string) { + try { + appStore.beginIndeterminateLoading(); + await permissionService.bucketAddPermissions(bucketId, [{ userId, permCode }]); + } catch (error: any) { + toast.error('Adding bucket permission', error); + } finally { + await fetchBucketPermissions(); + await mapBucketToUserPermissions(bucketId); + appStore.endIndeterminateLoading(); + } + } + + function addBucketUser(user: UserPermissions) { + try { + getters.getMappedBucketToUserPermissions.value.push(user); + } catch (error: any) { + toast.error('Adding bucket user', error); + } + } + + async function addObjectPermission(objectId: string, userId: string, permCode: string) { + try { + appStore.beginIndeterminateLoading(); + await permissionService.objectAddPermissions(objectId, [{ userId, permCode }]); + + const forceToggleReadOn: Array = [Permissions.UPDATE, Permissions.DELETE, Permissions.MANAGE]; + if (forceToggleReadOn.some((x: string) => x === permCode)) { + await permissionService.objectAddPermissions(objectId, [{ userId, permCode: Permissions.READ }]); + } + } catch (error: any) { + toast.error('Adding object permission', error); + } finally { + await fetchObjectPermissions(); + await mapObjectToUserPermissions(objectId); + appStore.endIndeterminateLoading(); + } + } + + function addObjectUser(user: UserPermissions) { + try { + getters.getMappedObjectToUserPermissions.value.push(user); + } catch (error: any) { + toast.error('Adding object user', error); + } + } + + async function deleteBucketPermission(bucketId: string, userId: string, permCode: string) { + try { + appStore.beginIndeterminateLoading(); + await permissionService.bucketDeletePermission(bucketId, { userId, permCode }); + } catch (error: any) { + toast.error('Deleting bucket permission', error); + } finally { + await fetchBucketPermissions(); + await mapBucketToUserPermissions(bucketId); + appStore.endIndeterminateLoading(); + } + } + + async function deleteObjectPermission(objectId: string, userId: string, permCode: string) { + try { + appStore.beginIndeterminateLoading(); + await permissionService.objectDeletePermission(objectId, { userId, permCode }); + + if (permCode === Permissions.READ) { + const forceToggleOnRead: Array = [Permissions.UPDATE, Permissions.DELETE, Permissions.MANAGE]; + await permissionService.objectDeletePermission(objectId, { userId, permCode: forceToggleOnRead }); + } + } catch (error: any) { + toast.error('Deleting object permission', error); + } finally { + await fetchObjectPermissions(); + await mapObjectToUserPermissions(objectId); + appStore.endIndeterminateLoading(); + } + } + + async function fetchBucketPermissions(params: BucketSearchPermissionsOptions = {}) { + try { + appStore.beginIndeterminateLoading(); + + const response = (await permissionService.bucketSearchPermissions(params)).data; + const newPerms: Array = response.flatMap((x: any) => x.permissions); + + // Remove old values matching search parameters + const matches = (x: BucketPermission) => + (!params.bucketId || x.bucketId === params.bucketId) && + (!params.userId || x.userId === params.userId) && + (!params.permCode || x.permCode === params.permCode); + + const [, difference] = partition(state.bucketPermissions.value, matches); + + // Merge and assign + state.bucketPermissions.value = difference.concat(newPerms); + + // Pass response back so bucketStore can handle bucketPerms=true correctly + return response; + } catch (error: any) { + toast.error('Fetching bucket permissions', error); + } finally { + appStore.endIndeterminateLoading(); + } + } + + async function fetchObjectPermissions(params: ObjectSearchPermissionsOptions = {}) { + try { + appStore.beginIndeterminateLoading(); + + const response = (await permissionService.objectSearchPermissions(params)).data; + + const newPerms: Array = response.flatMap((x: any) => x.permissions); + + // Remove old values matching search parameters + const matches = (x: COMSObjectPermission) => + (!params.objectId || x.id === params.objectId) && + (!params.userId || x.userId === params.userId) && + (!params.permCode || x.permCode === params.permCode); + + const [, difference] = partition(state.objectPermissions.value, matches); + + // Merge and assign + state.objectPermissions.value = difference.concat(newPerms); + + // Pass response back so objectStore can handle bucketPerms=true correctly + return response; + } catch (error: any) { + toast.error('Fetching object permissions', error); + } finally { + appStore.endIndeterminateLoading(); + } + } + + function isBucketActionAllowed(bucketId: string, userId?: string, permCode?: string) { + return state.bucketPermissions.value.some( + (x: BucketPermission) => x.bucketId === bucketId && x.userId === userId && x.permCode === permCode + ); + } + + function isObjectActionAllowed(objectId: string, userId?: string, permCode?: string, bucketId?: string) { + const bucketPerm = state.bucketPermissions.value.some( + (x: BucketPermission) => x.bucketId === bucketId && x.userId === userId && x.permCode === permCode + ); + + const objectPerm = state.objectPermissions.value.some( + (x: COMSObjectPermission) => x.objectId === objectId && x.userId === userId && x.permCode === permCode + ); + + return bucketPerm || objectPerm; + } + + function isUserElevatedRights() { + const idp = getConfig.value.idpList.find( + (provider: IdentityProvider) => provider.idp === getProfile.value?.identity_provider + ); + + return !!idp?.elevatedRights; + } + + async function mapBucketToUserPermissions(bucketId: string) { + try { + appStore.beginIndeterminateLoading(); + const bucketPerms = state.bucketPermissions.value.filter((x: BucketPermission) => x.bucketId === bucketId); + const uniqueIds = [...new Set(bucketPerms.map((x: BucketPermission) => x.userId))]; + const uniqueUsers: Array = (await userService.searchForUsers({ userId: uniqueIds })).data; + + const hasPermission = (userId: string, permission: string) => { + return bucketPerms.some((perm: BucketPermission) => perm.userId === userId && perm.permCode === permission); + }; + + const userPermissions: UserPermissions[] = []; + uniqueUsers.forEach((user: User) => { + const idp = getConfig.value.idpList.find((idp: IdentityProvider) => idp.idp === user.idp); + + userPermissions.push({ + userId: user.userId, + idpName: idp?.name, + elevatedRights: idp?.elevatedRights, + fullName: user.fullName, + create: hasPermission(user.userId, Permissions.CREATE), + read: hasPermission(user.userId, Permissions.READ), + update: hasPermission(user.userId, Permissions.UPDATE), + delete: hasPermission(user.userId, Permissions.DELETE), + manage: hasPermission(user.userId, Permissions.MANAGE) + }); + }); + + state.mappedBucketToUserPermissions.value = userPermissions; + } catch (error: any) { + toast.error('Mapping bucket permissions to user permissions', error); + } finally { + appStore.endIndeterminateLoading(); + } + } + + async function mapObjectToUserPermissions(objectId: string) { + try { + appStore.beginIndeterminateLoading(); + const objectPerms = state.objectPermissions.value.filter((x: COMSObjectPermission) => x.objectId === objectId); + const uniqueIds = [...new Set(objectPerms.map((x: COMSObjectPermission) => x.userId))]; + const uniqueUsers: Array = (await userService.searchForUsers({ userId: uniqueIds })).data; + + const hasPermission = (userId: string, permission: string) => { + return objectPerms.some((perm: COMSObjectPermission) => perm.userId === userId && perm.permCode === permission); + }; + + const userPermissions: UserPermissions[] = []; + uniqueUsers.forEach((user: User) => { + const idp = getConfig.value.idpList.find((idp: IdentityProvider) => idp.idp === user.idp); + + userPermissions.push({ + userId: user.userId, + idpName: idp?.name, + elevatedRights: idp?.elevatedRights, + fullName: user.fullName, + create: hasPermission(user.userId, Permissions.CREATE), + read: hasPermission(user.userId, Permissions.READ), + update: hasPermission(user.userId, Permissions.UPDATE), + delete: hasPermission(user.userId, Permissions.DELETE), + manage: hasPermission(user.userId, Permissions.MANAGE) + }); + }); + + state.mappedObjectToUserPermissions.value = userPermissions; + } catch (error: any) { + toast.error('Mapping bucket permissions to user permissions', error); + } finally { + appStore.endIndeterminateLoading(); + } + } + + async function removeBucketUser(bucketId: string, userId: string) { + try { + appStore.beginIndeterminateLoading(); + for (const value of Object.values(Permissions)) { + await permissionService.bucketDeletePermission(bucketId, { userId, permCode: value }); + } + } catch (error: any) { + toast.error('Removing bucket user', error); + } finally { + await fetchBucketPermissions(); + await mapBucketToUserPermissions(bucketId); + appStore.endIndeterminateLoading(); + } + } + + async function removeObjectUser(objectId: string, userId: string) { + try { + appStore.beginIndeterminateLoading(); + + for (const value of Object.values(Permissions)) { + await permissionService.objectDeletePermission(objectId, { + userId, + permCode: value + }); + } + } catch (error: any) { + toast.error('Removing object user', error); + } finally { + await fetchObjectPermissions(); + await mapObjectToUserPermissions(objectId); + appStore.endIndeterminateLoading(); + } + } + + return { + // State + ...state, + + // Getters + ...getters, + + // Actions + addBucketPermission, + addBucketUser, + addObjectPermission, + addObjectUser, + deleteBucketPermission, + deleteObjectPermission, + fetchBucketPermissions, + fetchObjectPermissions, + isBucketActionAllowed, + isObjectActionAllowed, + isUserElevatedRights, + mapBucketToUserPermissions, + mapObjectToUserPermissions, + removeBucketUser, + removeObjectUser + }; +}); + +export default usePermissionStore; diff --git a/bcbox/frontend/src/store/tagStore.ts b/bcbox/frontend/src/store/tagStore.ts new file mode 100644 index 00000000..d9d50793 --- /dev/null +++ b/bcbox/frontend/src/store/tagStore.ts @@ -0,0 +1,112 @@ +import { defineStore } from 'pinia'; +import { computed, ref } from 'vue'; + +import { useToast } from '@/lib/primevue'; +import { objectService } from '@/services'; +import { useAppStore } from '@/store'; +import { partition } from '@/utils/utils'; + +import type { Ref } from 'vue'; +import type { GetObjectTaggingOptions, Tag, Tagging } from '@/types'; + +export type TagStoreState = { + tagging: Ref>; + tagSearchResults: Ref>; +}; + +export const useTagStore = defineStore('tag', () => { + const appStore = useAppStore(); + const toast = useToast(); + + // State + const state: TagStoreState = { + tagging: ref([]), + tagSearchResults: ref([]) + }; + + // Getters + const getters = { + getTagging: computed(() => state.tagging.value), + getTagSearchResults: computed(() => state.tagSearchResults.value) + }; + + // Actions + async function deleteTagging(objectId: string, tagging: Array, versionId?: string) { + try { + appStore.beginIndeterminateLoading(); + + await objectService.deleteTagging(objectId, tagging, versionId); + await fetchTagging({ objectId: objectId }); + } catch (error: any) { + toast.error('Deleting tags', error); + } finally { + appStore.endIndeterminateLoading(); + } + } + + async function fetchTagging(params: GetObjectTaggingOptions = {}) { + try { + appStore.beginIndeterminateLoading(); + + const response = (await objectService.getObjectTagging(params)).data; + + // Remove old values matching search parameters + const matches = (x: Tagging) => + !params.objectId || + (Array.isArray(params.objectId) && params.objectId.some((y: string) => x.objectId === y)) || + (!Array.isArray(params.objectId) && params.objectId === x.objectId); + + const [, difference] = partition(state.tagging.value, matches); + + // Merge and assign + state.tagging.value = difference.concat(response); + } catch (error: any) { + toast.error('Fetching tags', error); + } finally { + appStore.endIndeterminateLoading(); + } + } + + const findTaggingByObjectId = (objectId: string) => state.tagging.value.find((x: Tagging) => x.objectId === objectId); + + async function replaceTagging(objectId: string, tagging: Array, versionId?: string) { + try { + appStore.beginIndeterminateLoading(); + + await objectService.replaceTagging(objectId, tagging, versionId); + await fetchTagging({ objectId: objectId }); + } catch (error: any) { + toast.error('Updating tags', error); + } finally { + appStore.endIndeterminateLoading(); + } + } + + async function searchTagging(tagset: Array = []) { + try { + state.tagSearchResults.value = []; + // await new Promise((resolve) => setTimeout(resolve, 4000)); + const response = (await objectService.searchTagging(tagset)).data; + state.tagSearchResults.value = response; + } catch (error: any) { + toast.error('Searching tags', error); + } + } + + return { + // State + ...state, + + // Getters + ...getters, + + // Actions + deleteTagging, + fetchTagging, + findTaggingByObjectId, + replaceTagging, + searchTagging + }; +}); + +export default useTagStore; diff --git a/bcbox/frontend/src/store/userStore.ts b/bcbox/frontend/src/store/userStore.ts new file mode 100644 index 00000000..a99eae1c --- /dev/null +++ b/bcbox/frontend/src/store/userStore.ts @@ -0,0 +1,87 @@ +import { defineStore } from 'pinia'; +import { computed, ref } from 'vue'; + +import { useToast } from '@/lib/primevue'; +import { userService } from '@/services'; +import { useAppStore } from '@/store'; +import { partition } from '@/utils/utils'; + +import type { Ref } from 'vue'; +import type { IdentityProvider, SearchUsersOptions, User } from '@/types'; + +export type UserStoreState = { + currentUser: Ref; + idps: Ref>; + userSearch: Ref>; +}; + +export const useUserStore = defineStore('user', () => { + const toast = useToast(); + + // Store + const appStore = useAppStore(); + + // State + const state: UserStoreState = { + currentUser: ref(null), + idps: ref([]), + userSearch: ref([]) + }; + + // Getters + const getters = { + getIdps: computed(() => state.idps.value), + getUserSearch: computed(() => state.userSearch.value) + }; + + // Actions + async function fetchUsers(params: SearchUsersOptions) { + try { + appStore.beginIndeterminateLoading(); + + // Search & filter out any user without an IDP + const response = (await userService.searchForUsers(params)).data.filter((x: User) => !!x.identityId); + + // Remove old values matching search parameters + const matches = (x: User) => + (!params.userId || + (Array.isArray(params.userId) && params.userId.some((y: string | undefined) => x.userId === y)) || + (!Array.isArray(params.userId) && params.userId === x.userId)) && + (!params.email || x.email === params.email) && + (!params.idp || x.idp === params.idp) && + (!params.lastName || x.lastName === params.lastName); + + const [, difference] = partition(state.userSearch.value, matches); + + // Merge and assign + state.userSearch.value = difference.concat(response); + } catch (error: any) { + toast.error('Searching users', error); + } finally { + appStore.endIndeterminateLoading(); + } + } + + function clearSearch() { + state.userSearch.value = []; + } + + function findUsersById(userId: Array) { + return state.userSearch.value.filter((x: User) => userId.includes(x.userId)); + } + + return { + // State + ...state, + + // Getters + ...getters, + + // Actions + clearSearch, + fetchUsers, + findUsersById + }; +}); + +export default useUserStore; diff --git a/bcbox/frontend/src/store/versionStore.ts b/bcbox/frontend/src/store/versionStore.ts new file mode 100644 index 00000000..f1fcde4e --- /dev/null +++ b/bcbox/frontend/src/store/versionStore.ts @@ -0,0 +1,150 @@ +import { defineStore } from 'pinia'; +import { computed, ref } from 'vue'; + +import { useToast } from '@/lib/primevue'; +import { objectService, versionService } from '@/services'; +import { useAppStore } from '@/store'; +import { partition } from '@/utils/utils'; + +import type { Ref } from 'vue'; +import type { + GetVersionOptions, + GetVersionMetadataOptions, + GetVersionTaggingOptions, + Metadata, + Tagging, + Version +} from '@/types'; + +export type VersionStoreState = { + metadata: Ref>; + tagging: Ref>; + versions: Ref>; +}; + +export const useVersionStore = defineStore('version', () => { + const appStore = useAppStore(); + const toast = useToast(); + + // State + const state: VersionStoreState = { + metadata: ref([]), + tagging: ref([]), + versions: ref([]) + }; + + // Getters + const getters = { + getMetadata: computed(() => state.metadata.value), + getTagging: computed(() => state.tagging.value), + getVersions: computed(() => state.versions.value) + }; + + // Actions + async function fetchMetadata(params: GetVersionMetadataOptions) { + try { + appStore.beginIndeterminateLoading(); + + const response = (await versionService.getMetadata(null, params)).data; + + // Remove old values matching search parameters + const matches = (x: Metadata) => !params.versionId || params.versionId === x.versionId; + + const [, difference] = partition(state.metadata.value, matches); + + // Merge and assign + state.metadata.value = difference.concat(response); + } catch (error: any) { + toast.error('Fetching metadata', error); + } finally { + appStore.endIndeterminateLoading(); + } + } + + async function fetchTagging(params: GetVersionTaggingOptions) { + try { + appStore.beginIndeterminateLoading(); + + const response = (await versionService.getObjectTagging(params)).data; + + // Remove old values matching search parameters + const matches = (x: Tagging) => !params.versionId || params.versionId === x.versionId; + + const [, difference] = partition(state.tagging.value, matches); + + // Merge and assign + state.tagging.value = difference.concat(response); + } catch (error: any) { + toast.error('Fetching tags', error); + } finally { + appStore.endIndeterminateLoading(); + } + } + + async function fetchVersions(params: GetVersionOptions) { + try { + appStore.beginIndeterminateLoading(); + + const response = (await objectService.listObjectVersion(params.objectId)).data; + + // Remove old values matching search parameters + const matches = (x: Version) => !params.objectId || x.objectId === params.objectId; + + const [, difference] = partition(state.versions.value, matches); + + // Merge and assign + state.versions.value = difference.concat(response); + } catch (error: any) { + toast.error('Fetching versions', error); + } finally { + appStore.endIndeterminateLoading(); + } + } + + function findLatestVersionIdByObjectId(objectId: string) { + return state.versions.value + .filter((x: Version) => x.objectId === objectId) + .sort((a: Version, b: Version) => Date.parse(b.createdAt as string) - Date.parse(a.createdAt as string))[0]?.id; + } + + function findMetadataByVersionId(versionId: string) { + return state.metadata.value.find((x: Metadata) => x.versionId === versionId); + } + + function findMetadataValue(versionId: string, key: string) { + return findMetadataByVersionId(versionId)?.metadata.find((x) => x.key === key)?.value; + } + + function findTaggingByVersionId(versionId: string) { + return state.tagging.value.find((x: Tagging) => x.versionId === versionId); + } + + function findVersionById(versionId: string) { + return state.versions.value.find((x: Version) => x.id === versionId); + } + + function findVersionsByObjectId(objectId: string) { + return state.versions.value.filter((x: Version) => x.objectId === objectId); + } + + return { + // State + ...state, + + // Getters + ...getters, + + // Actions + fetchMetadata, + fetchTagging, + fetchVersions, + findLatestVersionIdByObjectId, + findMetadataByVersionId, + findMetadataValue, + findTaggingByVersionId, + findVersionById, + findVersionsByObjectId + }; +}); + +export default useVersionStore; diff --git a/bcbox/frontend/src/types/Bucket.ts b/bcbox/frontend/src/types/Bucket.ts new file mode 100644 index 00000000..9057a75e --- /dev/null +++ b/bcbox/frontend/src/types/Bucket.ts @@ -0,0 +1,13 @@ +import type { IAudit } from '@/interfaces'; + +export type Bucket = { + active: boolean; + accessKeyId: string; + bucket: string; + bucketId: string; + bucketName: string; + endpoint: string; + key: string; + region: string; + secretAccessKey: string; +} & IAudit; diff --git a/bcbox/frontend/src/types/BucketPermission.ts b/bcbox/frontend/src/types/BucketPermission.ts new file mode 100644 index 00000000..f59a30c9 --- /dev/null +++ b/bcbox/frontend/src/types/BucketPermission.ts @@ -0,0 +1,8 @@ +import type { IAudit } from '@/interfaces'; + +export type BucketPermission = { + bucketId: string; + id: string; + permCode: string; + userId: string; +} & IAudit; diff --git a/bcbox/frontend/src/types/COMSObject.ts b/bcbox/frontend/src/types/COMSObject.ts new file mode 100644 index 00000000..01e8e8fa --- /dev/null +++ b/bcbox/frontend/src/types/COMSObject.ts @@ -0,0 +1,10 @@ +import type { IAudit } from '@/interfaces'; + +export type COMSObject = { + active: boolean; + bucketId: string; + id: string; + name: string; + path: string; + public: boolean; +} & IAudit; diff --git a/bcbox/frontend/src/types/COMSObjectPermission.ts b/bcbox/frontend/src/types/COMSObjectPermission.ts new file mode 100644 index 00000000..36e399df --- /dev/null +++ b/bcbox/frontend/src/types/COMSObjectPermission.ts @@ -0,0 +1,8 @@ +import type { IAudit } from '@/interfaces'; + +export type COMSObjectPermission = { + id: string; + objectId: string; + permCode: string; + userId: string; +} & IAudit; diff --git a/bcbox/frontend/src/types/IdentityProvider.ts b/bcbox/frontend/src/types/IdentityProvider.ts new file mode 100644 index 00000000..490456f1 --- /dev/null +++ b/bcbox/frontend/src/types/IdentityProvider.ts @@ -0,0 +1,7 @@ +export type IdentityProvider = { + name: string; + elevatedRights: boolean; + identityKey: string; + idp: string; + searchable: boolean; +}; diff --git a/bcbox/frontend/src/types/Metadata.ts b/bcbox/frontend/src/types/Metadata.ts new file mode 100644 index 00000000..ff5697b2 --- /dev/null +++ b/bcbox/frontend/src/types/Metadata.ts @@ -0,0 +1,7 @@ +import type { MetadataPair } from '@/types'; + +export type Metadata = { + metadata: Array; + objectId?: string; + versionId?: string; +}; diff --git a/bcbox/frontend/src/types/MetadataPair.ts b/bcbox/frontend/src/types/MetadataPair.ts new file mode 100644 index 00000000..438aec43 --- /dev/null +++ b/bcbox/frontend/src/types/MetadataPair.ts @@ -0,0 +1,4 @@ +export type MetadataPair = { + key: string; + value: string; +}; diff --git a/bcbox/frontend/src/types/Permission.ts b/bcbox/frontend/src/types/Permission.ts new file mode 100644 index 00000000..f386a140 --- /dev/null +++ b/bcbox/frontend/src/types/Permission.ts @@ -0,0 +1,6 @@ +import type { IAudit } from '@/interfaces'; + +export type Permission = { + active: boolean; + permCode: string; +} & IAudit; diff --git a/bcbox/frontend/src/types/Tag.ts b/bcbox/frontend/src/types/Tag.ts new file mode 100644 index 00000000..48194460 --- /dev/null +++ b/bcbox/frontend/src/types/Tag.ts @@ -0,0 +1,4 @@ +export type Tag = { + key: string; + value: string; +}; diff --git a/bcbox/frontend/src/types/Tagging.ts b/bcbox/frontend/src/types/Tagging.ts new file mode 100644 index 00000000..d581dcdf --- /dev/null +++ b/bcbox/frontend/src/types/Tagging.ts @@ -0,0 +1,7 @@ +import type { Tag } from '@/types'; + +export type Tagging = { + objectId?: string; + tagset: Array; + versionId?: string; +}; diff --git a/bcbox/frontend/src/types/User.ts b/bcbox/frontend/src/types/User.ts new file mode 100644 index 00000000..e43034e7 --- /dev/null +++ b/bcbox/frontend/src/types/User.ts @@ -0,0 +1,14 @@ +import type { IAudit } from '@/interfaces'; + +export type User = { + active: boolean; + email: string; + firstName: string; + fullName: string; + identityId: string | null; + idp: string; + lastName: string; + userId: string; + username: string; + elevatedRights: boolean; +} & IAudit; diff --git a/bcbox/frontend/src/types/UserPermissions.ts b/bcbox/frontend/src/types/UserPermissions.ts new file mode 100644 index 00000000..2c983114 --- /dev/null +++ b/bcbox/frontend/src/types/UserPermissions.ts @@ -0,0 +1,11 @@ +export type UserPermissions = { + userId: string; + idpName?: string; + elevatedRights?: boolean; + fullName: string; + create?: boolean; + read: boolean; + update: boolean; + delete: boolean; + manage: boolean; +}; diff --git a/bcbox/frontend/src/types/Version.ts b/bcbox/frontend/src/types/Version.ts new file mode 100644 index 00000000..7601eb08 --- /dev/null +++ b/bcbox/frontend/src/types/Version.ts @@ -0,0 +1,9 @@ +import type { IAudit } from '@/interfaces'; + +export type Version = { + deleteMarker: boolean; + id: string; + mimeType: string; + objectId: string; + s3VersionId: string; +} & IAudit; diff --git a/bcbox/frontend/src/types/index.ts b/bcbox/frontend/src/types/index.ts new file mode 100644 index 00000000..2e54e040 --- /dev/null +++ b/bcbox/frontend/src/types/index.ts @@ -0,0 +1,30 @@ +export type { Bucket } from './Bucket'; +export type { BucketPermission } from './BucketPermission'; +export type { COMSObject } from './COMSObject'; +export type { COMSObjectPermission } from './COMSObjectPermission'; +export type { IdentityProvider } from './IdentityProvider'; +export type { Metadata } from './Metadata'; +export type { MetadataPair } from './MetadataPair'; +export type { Permission } from './Permission'; +export type { Tagging } from './Tagging'; +export type { Tag } from './Tag'; +export type { User } from './User'; +export type { UserPermissions } from './UserPermissions'; +export type { Version } from './Version'; + +export type { BucketAddPermissionsOptions } from './options/BucketAddPermissionsOptions'; +export type { BucketDeletePermissionsOptions } from './options/BucketDeletePermissionsOptions'; +export type { BucketGetPermissionsOptions } from './options/BucketGetPermissionsOptions'; +export type { BucketSearchPermissionsOptions } from './options/BucketSearchPermissionsOptions'; +export type { GetMetadataOptions } from './options/GetMetadataOptions'; +export type { GetObjectTaggingOptions } from './options/GetObjectTaggingOptions'; +export type { GetVersionOptions } from './options/GetVersionOptions'; +export type { GetVersionMetadataOptions } from './options/GetVersionMetadataOptions'; +export type { GetVersionTaggingOptions } from './options/GetVersionTaggingOptions'; +export type { ObjectAddPermissionsOptions } from './options/ObjectAddPermissionsOptions'; +export type { ObjectDeletePermissionsOptions } from './options/ObjectDeletePermissionsOptions'; +export type { ObjectGetPermissionsOptions } from './options/ObjectGetPermissionsOptions'; +export type { ObjectSearchPermissionsOptions } from './options/ObjectSearchPermissionsOptions'; +export type { SearchBucketsOptions } from './options/SearchBucketsOptions'; +export type { SearchObjectsOptions } from './options/SearchObjectsOptions'; +export type { SearchUsersOptions } from './options/SearchUsersOptions'; diff --git a/bcbox/frontend/src/types/options/BucketAddPermissionsOptions.ts b/bcbox/frontend/src/types/options/BucketAddPermissionsOptions.ts new file mode 100644 index 00000000..72e1cb5d --- /dev/null +++ b/bcbox/frontend/src/types/options/BucketAddPermissionsOptions.ts @@ -0,0 +1,4 @@ +export type BucketAddPermissionsOptions = { + userId: string; + permCode: string; +}; diff --git a/bcbox/frontend/src/types/options/BucketDeletePermissionsOptions.ts b/bcbox/frontend/src/types/options/BucketDeletePermissionsOptions.ts new file mode 100644 index 00000000..e247eea3 --- /dev/null +++ b/bcbox/frontend/src/types/options/BucketDeletePermissionsOptions.ts @@ -0,0 +1,4 @@ +export type BucketDeletePermissionsOptions = { + userId?: string; + permCode?: string; +}; diff --git a/bcbox/frontend/src/types/options/BucketGetPermissionsOptions.ts b/bcbox/frontend/src/types/options/BucketGetPermissionsOptions.ts new file mode 100644 index 00000000..330adae0 --- /dev/null +++ b/bcbox/frontend/src/types/options/BucketGetPermissionsOptions.ts @@ -0,0 +1,4 @@ +export type BucketGetPermissionsOptions = { + userId?: string; + permCode?: string; +}; diff --git a/bcbox/frontend/src/types/options/BucketSearchPermissionsOptions.ts b/bcbox/frontend/src/types/options/BucketSearchPermissionsOptions.ts new file mode 100644 index 00000000..da96a983 --- /dev/null +++ b/bcbox/frontend/src/types/options/BucketSearchPermissionsOptions.ts @@ -0,0 +1,6 @@ +export type BucketSearchPermissionsOptions = { + bucketId?: string; + objectPerms?: boolean; + permCode?: string; + userId?: string; +}; diff --git a/bcbox/frontend/src/types/options/GetMetadataOptions.ts b/bcbox/frontend/src/types/options/GetMetadataOptions.ts new file mode 100644 index 00000000..d0ba8bd2 --- /dev/null +++ b/bcbox/frontend/src/types/options/GetMetadataOptions.ts @@ -0,0 +1,3 @@ +export type GetMetadataOptions = { + objectId?: string | Array; +}; diff --git a/bcbox/frontend/src/types/options/GetObjectTaggingOptions.ts b/bcbox/frontend/src/types/options/GetObjectTaggingOptions.ts new file mode 100644 index 00000000..006ad64f --- /dev/null +++ b/bcbox/frontend/src/types/options/GetObjectTaggingOptions.ts @@ -0,0 +1,3 @@ +export type GetObjectTaggingOptions = { + objectId?: string | Array; +}; diff --git a/bcbox/frontend/src/types/options/GetVersionMetadataOptions.ts b/bcbox/frontend/src/types/options/GetVersionMetadataOptions.ts new file mode 100644 index 00000000..e1cfd91f --- /dev/null +++ b/bcbox/frontend/src/types/options/GetVersionMetadataOptions.ts @@ -0,0 +1,3 @@ +export type GetVersionMetadataOptions = { + versionId: string; +}; diff --git a/bcbox/frontend/src/types/options/GetVersionOptions.ts b/bcbox/frontend/src/types/options/GetVersionOptions.ts new file mode 100644 index 00000000..7322a656 --- /dev/null +++ b/bcbox/frontend/src/types/options/GetVersionOptions.ts @@ -0,0 +1,3 @@ +export type GetVersionOptions = { + objectId: string; +}; diff --git a/bcbox/frontend/src/types/options/GetVersionTaggingOptions.ts b/bcbox/frontend/src/types/options/GetVersionTaggingOptions.ts new file mode 100644 index 00000000..17d4be68 --- /dev/null +++ b/bcbox/frontend/src/types/options/GetVersionTaggingOptions.ts @@ -0,0 +1,3 @@ +export type GetVersionTaggingOptions = { + versionId: string; +}; diff --git a/bcbox/frontend/src/types/options/ObjectAddPermissionsOptions.ts b/bcbox/frontend/src/types/options/ObjectAddPermissionsOptions.ts new file mode 100644 index 00000000..aa34cab6 --- /dev/null +++ b/bcbox/frontend/src/types/options/ObjectAddPermissionsOptions.ts @@ -0,0 +1,4 @@ +export type ObjectAddPermissionsOptions = { + userId: string; + permCode: string | Array; +}; diff --git a/bcbox/frontend/src/types/options/ObjectDeletePermissionsOptions.ts b/bcbox/frontend/src/types/options/ObjectDeletePermissionsOptions.ts new file mode 100644 index 00000000..62be57e9 --- /dev/null +++ b/bcbox/frontend/src/types/options/ObjectDeletePermissionsOptions.ts @@ -0,0 +1,4 @@ +export type ObjectDeletePermissionsOptions = { + userId?: string; + permCode?: string | Array; +}; diff --git a/bcbox/frontend/src/types/options/ObjectGetPermissionsOptions.ts b/bcbox/frontend/src/types/options/ObjectGetPermissionsOptions.ts new file mode 100644 index 00000000..09bad0ab --- /dev/null +++ b/bcbox/frontend/src/types/options/ObjectGetPermissionsOptions.ts @@ -0,0 +1,4 @@ +export type ObjectGetPermissionsOptions = { + userId?: string; + permCode?: string; +}; diff --git a/bcbox/frontend/src/types/options/ObjectSearchPermissionsOptions.ts b/bcbox/frontend/src/types/options/ObjectSearchPermissionsOptions.ts new file mode 100644 index 00000000..3228d040 --- /dev/null +++ b/bcbox/frontend/src/types/options/ObjectSearchPermissionsOptions.ts @@ -0,0 +1,7 @@ +export type ObjectSearchPermissionsOptions = { + bucketId?: string; + bucketPerms?: boolean; + objectId?: string; + permCode?: string; + userId?: string; +}; diff --git a/bcbox/frontend/src/types/options/SearchBucketsOptions.ts b/bcbox/frontend/src/types/options/SearchBucketsOptions.ts new file mode 100644 index 00000000..af6f7fe9 --- /dev/null +++ b/bcbox/frontend/src/types/options/SearchBucketsOptions.ts @@ -0,0 +1,6 @@ +export type SearchBucketsOptions = { + bucketId?: Array; + bucketName?: string; + key?: string; + active?: boolean; +}; diff --git a/bcbox/frontend/src/types/options/SearchObjectsOptions.ts b/bcbox/frontend/src/types/options/SearchObjectsOptions.ts new file mode 100644 index 00000000..de4146d3 --- /dev/null +++ b/bcbox/frontend/src/types/options/SearchObjectsOptions.ts @@ -0,0 +1,12 @@ +export type SearchObjectsOptions = { + bucketId?: Array; + objectId?: Array; + name?: string; + path?: string; + mimeType?: string; + tagset?: any; + public?: boolean; + active?: boolean; + deleteMarker?: boolean; + latest?: boolean; +}; diff --git a/bcbox/frontend/src/types/options/SearchUsersOptions.ts b/bcbox/frontend/src/types/options/SearchUsersOptions.ts new file mode 100644 index 00000000..f2153b9f --- /dev/null +++ b/bcbox/frontend/src/types/options/SearchUsersOptions.ts @@ -0,0 +1,8 @@ +export type SearchUsersOptions = { + email?: string; + identityId?: string; + idp?: string; + lastName?: string; + search?: string; + userId?: Array; +}; diff --git a/bcbox/frontend/src/utils/constants.ts b/bcbox/frontend/src/utils/constants.ts new file mode 100644 index 00000000..94fa4e6a --- /dev/null +++ b/bcbox/frontend/src/utils/constants.ts @@ -0,0 +1,63 @@ +export const BucketConfig = Object.freeze({ + HEADER_NEW_BUCKET: 'Configure bucket', + TITLE_NEW_BUCKET: 'Use this form to configure a bucket to be used in BCBox for the first time.' +}); + +/** + * Default string delimiter + */ +export const DELIMITER = '/'; + +/** + * Max allowable tags to be added to an object + * S3 max is 10, but one is reserved for the COMS `coms-id` + */ +export const MAX_TAGS = 9; + +export const Permissions = Object.freeze({ + CREATE: 'CREATE', + READ: 'READ', + UPDATE: 'UPDATE', + DELETE: 'DELETE', + MANAGE: 'MANAGE' +}); + +export const Regex = Object.freeze({ + // https://emailregex.com/ + // HTML5 - Modified to require domain of at least 2 characters + EMAIL: '^[a-zA-Z0-9.!#$%&’*+/=?^_`{|}~-]+@[a-zA-Z0-9-]+(?:\\.[a-zA-Z0-9-]{2,})+$' +}); + +export const RouteNames = Object.freeze({ + CALLBACK: 'callback', + CREATE_BUCKET: 'createBucket', + DETAIL_OBJECTS: 'detailObjects', + DEVELOPER: 'developer', + FORBIDDEN: 'forbidden', + HOME: 'home', + LIST_BUCKETS: 'listBuckets', + LIST_OBJECTS: 'listObjects', + LOGIN: 'login', + LOGOUT: 'logout' +}); + +export const StorageKey = Object.freeze({ + AUTH: 'entrypoint', + CONFIG: 'config' +}); + +/** + * Default COMS System User ID + */ +export const SYSTEM_USER = '00000000-0000-0000-0000-000000000000'; + +export const ToastTimeout = Object.freeze({ + ERROR: 5000, + INFO: 3000, + SUCCESS: 3000, + WARNING: 5000 +}); + +export const ValidationMessages = Object.freeze({ + REQUIRED: 'This field is required.' +}); diff --git a/bcbox/frontend/src/utils/enums.ts b/bcbox/frontend/src/utils/enums.ts new file mode 100644 index 00000000..533c38b9 --- /dev/null +++ b/bcbox/frontend/src/utils/enums.ts @@ -0,0 +1,13 @@ +/* eslint-disable no-unused-vars */ // TODO: Remove this blanket disable +// Enums that represent general app-wide values (not buisness-specific like objects, buckets, etc) + +// Which way to display a button that can appear in multiple modes +export enum ButtonMode { + BUTTON = 'BUTTON', + ICON = 'ICON' +} + +export enum ExcludeTypes { + METADATA = 'metadata', + TAGSET = 'tagset' +} diff --git a/bcbox/frontend/src/utils/formatters.ts b/bcbox/frontend/src/utils/formatters.ts new file mode 100644 index 00000000..ad06587a --- /dev/null +++ b/bcbox/frontend/src/utils/formatters.ts @@ -0,0 +1,39 @@ +import { format, parseJSON } from 'date-fns'; + +function _dateFnsFormat(value: string, formatter: string) { + const formatted = ''; + try { + if (value) { + return format(parseJSON(value), formatter); + } + } catch (error) { + // eslint-disable-next-line no-console + console.error(`_dateFnsFormat: Error parsing ${value} to ${error}`); + } + return formatted; +} + +/** + * @function formatDate + * Converts a date to an 'MMMM D YYYY' formatted string + * @param {String} value A string representation of a date + * @returns {String} A string representation of `value` + */ +export function formatDate(value: string) { + return _dateFnsFormat(value, 'MMMM d yyyy'); +} + +/** + * @function formatDateLong + * Converts a date to an 'MMMM D yyyy, h:mm:ss a' formatted string + * @param {String} value A string representation of a date + * @returns {String} A string representation of `value` + */ +export function formatDateLong(value: string) { + return _dateFnsFormat(value, 'MMMM d yyyy, h:mm:ss a'); +} + +export function toKebabCase(str: string | null) { + const strs = str && str.match(/[A-Z]{2,}(?=[A-Z][a-z]+[0-9]*|\b)|[A-Z]?[a-z]+[0-9]*|[A-Z]|[0-9]+/g); + return strs ? strs.join('-').toLocaleLowerCase() : ''; +} diff --git a/bcbox/frontend/src/utils/utils.ts b/bcbox/frontend/src/utils/utils.ts new file mode 100644 index 00000000..ff6aea9f --- /dev/null +++ b/bcbox/frontend/src/utils/utils.ts @@ -0,0 +1,118 @@ +import { DELIMITER } from '@/utils/constants'; +import ConfigService from '@/services/configService'; +import { ExcludeTypes } from '@/utils/enums'; + +/** + * @function differential + * Create a key/value differential from source against comparer + * @param {object} source Source object + * @param {object} comparer The object to be compared against + * @returns {object} Object containing the non-matching key/value pairs in the source object + */ +export function differential(source: any, comparer: any): any { + return Object.fromEntries(Object.entries(source).filter(([key, value]) => comparer[key] !== value)); +} + +/** + * @function isDebugMode + * Checks if the app is currently running in debug mode + * @returns {boolean} True if in debug, false otherwise + */ +export function isDebugMode(): boolean { + return import.meta.env.MODE.toUpperCase() === 'DEBUG'; +} + +/** + * @function joinPath + * Joins a set of string arguments to yield a string path + * @param {...string} items The strings to join on + * @returns {string} A path string with the specified delimiter + */ +export function joinPath(...items: Array): string { + if (items && items.length) { + const parts: Array = []; + items.forEach((p) => { + if (p) + p.split(DELIMITER).forEach((x) => { + if (x && x.trim().length) parts.push(x); + }); + }); + return parts.join(DELIMITER) ?? ''; + } else return ''; +} + +/** + * @function partition + * Partitions an array into two array sets depending on conditional + * @see {@link https://stackoverflow.com/a/71247432} + * @param {Array} arr The array to partition + * @param {Function} predicate The predicate function + * @returns + */ +export function partition( + arr: Array, + // eslint-disable-next-line no-unused-vars, @typescript-eslint/no-unused-vars + predicate: (v: T, i: number, ar: Array) => boolean +): [Array, Array] { + return arr.reduce( + (acc, item, index, array) => { + acc[+!predicate(item, index, array)].push(item); + return acc; + }, + [[], []] as [Array, Array] + ); +} + +/** + * @function excludeMetaTag + * Filter out a configured list of select metadata or tags from a COMS response + * @param {string} type either 'metadata' or 'tagset' + * @param {object} data An object with metadata/tags from COMS + * @returns {object} The response data with select metadata/tags from a configured list removed + */ +export function excludeMetaTag( + type: ExcludeTypes, + data: [ + { + objectId: string; + metadata?: Array<{ key: string; value: string }>; + tagset?: Array<{ key: string; value: string }>; + } + ] +) { + // TODO: consider unit testing this function + // array of selected tags/metadata (keys) to hide from UI + const excludeArray = + new ConfigService() + .getConfig() + .exclude?.[type]?.split(',') + .map((s: string) => s.trim()) ?? []; + // filter COMS data + return data.map((obj: any) => { + return { + ...obj, + [type]: obj[type]?.filter((el: any) => !excludeArray.includes(el.key)) + }; + }); +} + +/** + * @function setDispositionHeader + * Constructs a valid RFC 6266 'Content-Disposition' header + * and optionally handles RFC 8187 UTF-8 encoding when necessary + * @param {string} filename The file name to check if encoding is needed + * @returns {string} The value for the key 'Content-Disposition' + */ +export function setDispositionHeader(filename: string) { + const dispositionHeader = `attachment; filename="${filename}"`; + const encodedFilename = encodeURIComponent(filename).replace( + /[!'()*]/g, + (c) => `%${c.charCodeAt(0).toString(16).toUpperCase()}` + ); + + if (filename === encodedFilename) { + return dispositionHeader; + } else { + return dispositionHeader.concat(`; filename*=UTF-8''${encodedFilename}`); + } +} diff --git a/bcbox/frontend/src/views/DeveloperView.vue b/bcbox/frontend/src/views/DeveloperView.vue new file mode 100644 index 00000000..608e2f2c --- /dev/null +++ b/bcbox/frontend/src/views/DeveloperView.vue @@ -0,0 +1,46 @@ + + + diff --git a/bcbox/frontend/src/views/Forbidden.vue b/bcbox/frontend/src/views/Forbidden.vue new file mode 100644 index 00000000..122e5930 --- /dev/null +++ b/bcbox/frontend/src/views/Forbidden.vue @@ -0,0 +1,5 @@ + + + + + diff --git a/bcbox/frontend/src/views/NotFound.vue b/bcbox/frontend/src/views/NotFound.vue new file mode 100644 index 00000000..be123037 --- /dev/null +++ b/bcbox/frontend/src/views/NotFound.vue @@ -0,0 +1,5 @@ + + + + + diff --git a/bcbox/frontend/src/views/list/ListBucketsView.vue b/bcbox/frontend/src/views/list/ListBucketsView.vue new file mode 100644 index 00000000..aabc1b6f --- /dev/null +++ b/bcbox/frontend/src/views/list/ListBucketsView.vue @@ -0,0 +1,7 @@ + + + diff --git a/bcbox/frontend/src/views/list/ListObjectsView.vue b/bcbox/frontend/src/views/list/ListObjectsView.vue new file mode 100644 index 00000000..80b71d3b --- /dev/null +++ b/bcbox/frontend/src/views/list/ListObjectsView.vue @@ -0,0 +1,92 @@ + + + + + diff --git a/bcbox/frontend/src/views/oidc/OidcCallbackView.vue b/bcbox/frontend/src/views/oidc/OidcCallbackView.vue new file mode 100644 index 00000000..b01c8ffa --- /dev/null +++ b/bcbox/frontend/src/views/oidc/OidcCallbackView.vue @@ -0,0 +1,37 @@ + + + + + diff --git a/bcbox/frontend/src/views/oidc/OidcLoginView.vue b/bcbox/frontend/src/views/oidc/OidcLoginView.vue new file mode 100644 index 00000000..96f6296d --- /dev/null +++ b/bcbox/frontend/src/views/oidc/OidcLoginView.vue @@ -0,0 +1,29 @@ + + + + + diff --git a/bcbox/frontend/src/views/oidc/OidcLogoutView.vue b/bcbox/frontend/src/views/oidc/OidcLogoutView.vue new file mode 100644 index 00000000..d0092caa --- /dev/null +++ b/bcbox/frontend/src/views/oidc/OidcLogoutView.vue @@ -0,0 +1,34 @@ + + + + + diff --git a/bcbox/frontend/tests/unit/App.spec.ts b/bcbox/frontend/tests/unit/App.spec.ts new file mode 100644 index 00000000..3bed12bd --- /dev/null +++ b/bcbox/frontend/tests/unit/App.spec.ts @@ -0,0 +1,7 @@ +import { ValidationMessages } from '@/utils/constants'; + +describe('App', () => { + it('working', () => { + expect(ValidationMessages).toBeTruthy(); + }); +}); diff --git a/bcbox/frontend/tests/unit/components/bucket/BucketConfigForm.spec.ts b/bcbox/frontend/tests/unit/components/bucket/BucketConfigForm.spec.ts new file mode 100644 index 00000000..5dd5b710 --- /dev/null +++ b/bcbox/frontend/tests/unit/components/bucket/BucketConfigForm.spec.ts @@ -0,0 +1,80 @@ +import { createTestingPinia } from '@pinia/testing'; +import { mount, shallowMount, RouterLinkStub } from '@vue/test-utils'; + +import BucketConfigForm from '@/components/bucket/BucketConfigForm.vue'; +import * as primevue from '@/lib/primevue'; +import { StorageKey } from '@/utils/constants'; +import PrimeVue from 'primevue/config'; +import ToastService from 'primevue/toastservice'; + +const mockToast = vi.fn(); +const useToastSpy = vi.spyOn(primevue, 'useToast'); + +beforeEach(() => { + sessionStorage.setItem( + StorageKey.CONFIG, + JSON.stringify({ + oidc: { + authority: 'abc', + clientId: '123' + } + }) + ); + + vi.clearAllMocks(); + useToastSpy.mockImplementation(() => ({ error: mockToast, info: mockToast, success: mockToast, warn: mockToast })); +}); + +afterEach(() => { + sessionStorage.clear(); +}); + +describe('BucketConfigForm.vue', () => { + it('renders', () => { + const wrapper = shallowMount(BucketConfigForm, { + global: { + plugins: [PrimeVue, createTestingPinia(), ToastService], + stubs: { + RouterLink: RouterLinkStub + } + }, + props: {} + }); + expect(wrapper).toBeTruthy(); + }); + + it('emits cancel config', async () => { + const wrapper = mount(BucketConfigForm, { + global: { + plugins: [PrimeVue, createTestingPinia(), ToastService], + stubs: { + RouterLink: RouterLinkStub + } + }, + props: {} + }); + + const cancelButton = wrapper.get('[aria-label="Cancel"]'); + await cancelButton.trigger('click'); + + expect(wrapper.emitted()).toHaveProperty('cancel-bucket-config'); + }); + + it('emits triggers onSubmit error', async () => { + const wrapper = mount(BucketConfigForm, { + global: { + plugins: [PrimeVue, createTestingPinia(), ToastService], + stubs: { + RouterLink: RouterLinkStub + } + }, + props: {} + }); + + const submitBtn = wrapper.get('[aria-label="Apply"]'); + await submitBtn.trigger('click'); + + expect(useToastSpy).toHaveBeenCalled(); + expect(wrapper).toBeTruthy(); + }); +}); diff --git a/bcbox/frontend/tests/unit/components/bucket/BucketList.spec.ts b/bcbox/frontend/tests/unit/components/bucket/BucketList.spec.ts new file mode 100644 index 00000000..bd993d53 --- /dev/null +++ b/bcbox/frontend/tests/unit/components/bucket/BucketList.spec.ts @@ -0,0 +1,70 @@ +import { createTestingPinia } from '@pinia/testing'; +import { mount, shallowMount } from '@vue/test-utils'; + +import BucketList from '@/components/bucket/BucketList.vue'; +import * as primevue from '@/lib/primevue'; +import { usePermissionStore } from '@/store'; +import { StorageKey } from '@/utils/constants'; +import PrimeVue from 'primevue/config'; +import ToastService from 'primevue/toastservice'; + +const mockToast = vi.fn(); +const useToastSpy = vi.spyOn(primevue, 'useToast'); + +beforeEach(() => { + sessionStorage.setItem( + StorageKey.CONFIG, + JSON.stringify({ + oidc: { + authority: 'abc', + clientId: '123' + } + }) + ); + + vi.clearAllMocks(); + useToastSpy.mockImplementation(() => ({ error: mockToast, info: mockToast, success: mockToast, warn: mockToast })); +}); + +afterEach(() => { + sessionStorage.clear(); +}); + +describe('BucketList.vue', async () => { + it('renders', () => { + const testingPinia = createTestingPinia({ + initialState: { + auth: { + user: {} + } + } + }); + + const wrapper = shallowMount(BucketList, { + global: { + plugins: [testingPinia, PrimeVue, ToastService], + stubs: ['font-awesome-icon'] + } + }); + + expect(wrapper).toBeTruthy(); + }); + + it('shows connect bucket button', async () => { + // Mock isUserElevatedRights to return true + const pinia = createTestingPinia(); + const permStore = usePermissionStore(pinia); + vi.mocked(permStore.isUserElevatedRights).mockReturnValue(true); + + const wrapper = mount(BucketList, { + global: { + plugins: [PrimeVue, ToastService, pinia], + stubs: ['font-awesome-icon', 'BucketTable'] + } + }); + + const button = wrapper.find('[data-test="connect-bucket"]'); + + expect(button.isVisible()).toBeTruthy(); + }); +}); diff --git a/bcbox/frontend/tests/unit/components/bucket/BucketPermission.spec.ts b/bcbox/frontend/tests/unit/components/bucket/BucketPermission.spec.ts new file mode 100644 index 00000000..0b1da1d8 --- /dev/null +++ b/bcbox/frontend/tests/unit/components/bucket/BucketPermission.spec.ts @@ -0,0 +1,55 @@ +import { createTestingPinia } from '@pinia/testing'; +import { mount } from '@vue/test-utils'; + +import BucketPermission from '@/components/bucket/BucketPermission.vue'; +import * as primevue from '@/lib/primevue'; +import { StorageKey } from '@/utils/constants'; +import PrimeVue from 'primevue/config'; +import ConfirmationService from 'primevue/confirmationservice'; +import ToastService from 'primevue/toastservice'; + +const mockToast = vi.fn(); +const useToastSpy = vi.spyOn(primevue, 'useToast'); + +beforeEach(() => { + sessionStorage.setItem( + StorageKey.CONFIG, + JSON.stringify({ + oidc: { + authority: 'abc', + clientId: '123' + } + }) + ); + + vi.clearAllMocks(); + useToastSpy.mockImplementation(() => ({ error: mockToast, info: mockToast, success: mockToast, warn: mockToast })); +}); + +afterEach(() => { + sessionStorage.clear(); +}); + +describe('BucketPermission.vue', async () => { + it('renders', () => { + const testingPinia = createTestingPinia({ + initialState: { + auth: { + user: {} + } + } + }); + + const wrapper = mount(BucketPermission, { + props: { + bucketId: 'testBucketId' + }, + global: { + plugins: [ConfirmationService, PrimeVue, testingPinia, ToastService], + stubs: ['font-awesome-icon', 'DataTable'] + } + }); + + expect(wrapper).toBeTruthy(); + }); +}); diff --git a/bcbox/frontend/tests/unit/components/bucket/BucketPermissionAddUser.spec.ts b/bcbox/frontend/tests/unit/components/bucket/BucketPermissionAddUser.spec.ts new file mode 100644 index 00000000..77ea6214 --- /dev/null +++ b/bcbox/frontend/tests/unit/components/bucket/BucketPermissionAddUser.spec.ts @@ -0,0 +1,51 @@ +import { createTestingPinia } from '@pinia/testing'; +import { shallowMount } from '@vue/test-utils'; + +import BucketPermissionAddUser from '@/components/bucket/BucketPermissionAddUser.vue'; +import * as primevue from '@/lib/primevue'; +import { StorageKey } from '@/utils/constants'; +import PrimeVue from 'primevue/config'; +import ToastService from 'primevue/toastservice'; + +const mockToast = vi.fn(); +const useToastSpy = vi.spyOn(primevue, 'useToast'); + +beforeEach(() => { + sessionStorage.setItem( + StorageKey.CONFIG, + JSON.stringify({ + oidc: { + authority: 'abc', + clientId: '123' + } + }) + ); + + vi.clearAllMocks(); + useToastSpy.mockImplementation(() => ({ error: mockToast, info: mockToast, success: mockToast, warn: mockToast })); +}); + +afterEach(() => { + sessionStorage.clear(); +}); + +describe('BucketPermissionAddUser.vue', async () => { + it('renders', () => { + const testingPinia = createTestingPinia({ + initialState: { + auth: { + user: {} + } + } + }); + + const wrapper = shallowMount(BucketPermissionAddUser, { + global: { + plugins: [testingPinia, PrimeVue, ToastService], + stubs: ['font-awesome-icon'] + } + }); + + expect(wrapper).toBeTruthy(); + }); +}); diff --git a/bcbox/frontend/tests/unit/components/bucket/BucketSidebar.spec.ts b/bcbox/frontend/tests/unit/components/bucket/BucketSidebar.spec.ts new file mode 100644 index 00000000..c77f07b9 --- /dev/null +++ b/bcbox/frontend/tests/unit/components/bucket/BucketSidebar.spec.ts @@ -0,0 +1,137 @@ +import { createTestingPinia } from '@pinia/testing'; +import { shallowMount } from '@vue/test-utils'; + +import BucketSidebar from '@/components/bucket/BucketSidebar.vue'; +import * as primevue from '@/lib/primevue'; +import { StorageKey } from '@/utils/constants'; +import PrimeVue from 'primevue/config'; +import ToastService from 'primevue/toastservice'; + +const mockToast = vi.fn(); +const useToastSpy = vi.spyOn(primevue, 'useToast'); +const testSidebarInfo = { + bucketId: '11111111-2222-3333-4444-555555555555', + bucketName: 'Minio', + accessKeyId: 'REDACTED', + bucket: 'testbucket', + endpoint: 'http://127.0.0.1:9000', + key: '/', + secretAccessKey: 'REDACTED', + region: 'null', + active: true, + createdBy: '11111111-2222-3333-4444-555555555555', + createdAt: '2023-09-28T21:25:38.927Z', + updatedBy: '2023-09-28T21:25:38.927Z', + updatedAt: '2023-09-28T21:25:38.927Z' +}; +const testUserSearch = [ + { + userId: 'ABCDEF123456789', + identityId: '123456789ABCDEF', + idp: 'idir', + username: '1111111111111@idir', + email: 'test@gov.bc.ca', + firstName: 'wil', + fullName: 'Wong, wil WLRS:EX', + lastName: 'Wong', + active: true, + createdBy: '00000000-0000-0000-0000-000000000000', + createdAt: '2023-08-02T22:09:21.042Z', + updatedBy: null, + updatedAt: null + } +]; + +beforeEach(() => { + sessionStorage.setItem( + StorageKey.CONFIG, + JSON.stringify({ + oidc: { + authority: 'abc', + clientId: '123' + } + }) + ); + + vi.clearAllMocks(); + useToastSpy.mockImplementation(() => ({ error: mockToast, info: mockToast, success: mockToast, warn: mockToast })); +}); + +afterEach(() => { + sessionStorage.clear(); +}); + +describe('BucketSidebar.vue', async () => { + it('renders', () => { + const testingPinia = createTestingPinia({ + initialState: { + user: { + userSearch: testUserSearch + } + } + }); + + const wrapper = shallowMount(BucketSidebar, { + props: { + sidebarInfo: testSidebarInfo + }, + global: { + plugins: [testingPinia, PrimeVue, ToastService], + stubs: ['font-awesome-icon'] + } + }); + + expect(wrapper).toBeTruthy(); + }); + + it('expect to match test data', () => { + const testingPinia = createTestingPinia({ + initialState: { + user: { + userSearch: testUserSearch + } + } + }); + + const wrapper = shallowMount(BucketSidebar, { + props: { + sidebarInfo: testSidebarInfo + }, + global: { + plugins: [testingPinia, PrimeVue, ToastService], + stubs: ['font-awesome-icon'] + } + }); + const textArray: Array = []; + wrapper.findAll('div').forEach((ele) => { + textArray.push(ele.text()); + }); + expect(textArray).toContain(`Bucket ID:${testSidebarInfo.bucketId}`); + expect(textArray).toContain(`Bucket Name:${testSidebarInfo.bucketName}`); + }); + + it('emits close modal', async () => { + const testingPinia = createTestingPinia({ + initialState: { + user: { + userSearch: testUserSearch + } + } + }); + + const wrapper = shallowMount(BucketSidebar, { + props: { + sidebarInfo: testSidebarInfo + }, + global: { + plugins: [testingPinia, PrimeVue, ToastService], + stubs: ['font-awesome-icon'] + } + }); + + const btn = wrapper.get('button-stub'); + await btn.trigger('click'); + + expect(wrapper.emitted()).toHaveProperty('close-sidebar-info'); + }); +}); diff --git a/bcbox/frontend/tests/unit/components/bucket/BucketTable.spec.ts b/bcbox/frontend/tests/unit/components/bucket/BucketTable.spec.ts new file mode 100644 index 00000000..ef85b496 --- /dev/null +++ b/bcbox/frontend/tests/unit/components/bucket/BucketTable.spec.ts @@ -0,0 +1,59 @@ +import { createTestingPinia } from '@pinia/testing'; +import { mount, RouterLinkStub } from '@vue/test-utils'; + +import BucketTable from '@/components/bucket/BucketTable.vue'; +import * as primevue from '@/lib/primevue'; +import { StorageKey } from '@/utils/constants'; +import PrimeVue from 'primevue/config'; +import ConfirmationService from 'primevue/confirmationservice'; +import ToastService from 'primevue/toastservice'; +import Tooltip from 'primevue/tooltip'; + +const mockToast = vi.fn(); +const useToastSpy = vi.spyOn(primevue, 'useToast'); + +beforeEach(() => { + sessionStorage.setItem( + StorageKey.CONFIG, + JSON.stringify({ + oidc: { + authority: 'abc', + clientId: '123' + } + }) + ); + + vi.clearAllMocks(); + useToastSpy.mockImplementation(() => ({ error: mockToast, info: mockToast, success: mockToast, warn: mockToast })); +}); + +afterEach(() => { + sessionStorage.clear(); +}); + +describe('BucketList.vue', async () => { + it('renders', () => { + const testingPinia = createTestingPinia({ + initialState: { + auth: { + user: {} + } + } + }); + + const wrapper = mount(BucketTable, { + global: { + plugins: [ConfirmationService, testingPinia, PrimeVue, ToastService], + stubs: { + RouterLink: RouterLinkStub, + 'font-awesome-icon': true + }, + directives: { + Tooltip: Tooltip + } + } + }); + + expect(wrapper).toBeTruthy(); + }); +}); diff --git a/bcbox/frontend/tests/unit/components/form/TextInput.spec.ts b/bcbox/frontend/tests/unit/components/form/TextInput.spec.ts new file mode 100644 index 00000000..5504f468 --- /dev/null +++ b/bcbox/frontend/tests/unit/components/form/TextInput.spec.ts @@ -0,0 +1,79 @@ +import { shallowMount } from '@vue/test-utils'; + +import PrimeVue from 'primevue/config'; +import TextInput from '@/components/form/TextInput.vue'; + +describe('TextInput.vue', () => { + it('renders', () => { + const wrapper = shallowMount(TextInput, { + props: { + name: 'test' + }, + global: { + plugins: [PrimeVue] + } + }); + + expect(wrapper).toBeTruthy(); + }); + + it('displays the label', () => { + const wrapper = shallowMount(TextInput, { + props: { + label: 'testlabel', + name: 'test' + }, + global: { + plugins: [PrimeVue] + } + }); + + const label = wrapper.find('label'); + expect(label.text()).toBe('testlabel'); + }); + + it('displays the help text', () => { + const wrapper = shallowMount(TextInput, { + props: { + helpText: 'some help text', + name: 'test' + }, + global: { + plugins: [PrimeVue] + } + }); + + const help = wrapper.find('small'); + expect(help.text()).toBe('some help text'); + }); + + it('sets the placeholder text', () => { + const wrapper = shallowMount(TextInput, { + props: { + name: 'test', + placeholder: 'some placeholder text' + }, + global: { + plugins: [PrimeVue] + } + }); + + const input = wrapper.getComponent({ name: 'InputText' }); + expect(input.attributes('placeholder')).toBe('some placeholder text'); + }); + + it('sets the disabled attribute', () => { + const wrapper = shallowMount(TextInput, { + props: { + disabled: true, + name: 'test' + }, + global: { + plugins: [PrimeVue] + } + }); + + const input = wrapper.getComponent({ name: 'InputText' }); + expect(input.attributes('disabled')).toBeDefined(); + }); +}); diff --git a/bcbox/frontend/tests/unit/components/layout/Footer.spec.ts b/bcbox/frontend/tests/unit/components/layout/Footer.spec.ts new file mode 100644 index 00000000..1a8548b5 --- /dev/null +++ b/bcbox/frontend/tests/unit/components/layout/Footer.spec.ts @@ -0,0 +1,53 @@ +import { createTestingPinia } from '@pinia/testing'; +import { mount, shallowMount } from '@vue/test-utils'; + +import PrimeVue from 'primevue/config'; +import Footer from '@/components/layout/Footer.vue'; +import { StorageKey } from '@/utils/constants'; + +// Mock router calls +vi.mock('vue-router', () => ({ + useRouter: () => ({ + push: vi.fn() + }) +})); + +beforeEach(() => { + sessionStorage.setItem( + StorageKey.CONFIG, + JSON.stringify({ + oidc: { + authority: 'abc', + clientId: '123' + } + }) + ); + + vi.clearAllMocks(); +}); + +afterEach(() => { + sessionStorage.clear(); +}); + +describe('Footer.vue', () => { + it('renders', () => { + const wrapper = shallowMount(Footer, { + global: { + plugins: [createTestingPinia(), PrimeVue] + } + }); + expect(wrapper).toBeTruthy(); + }); + + it('contains 7 buttons', () => { + const wrapper = mount(Footer, { + global: { + plugins: [createTestingPinia(), PrimeVue] + } + }); + + const btn = wrapper.findAll('button'); + expect(btn).toHaveLength(7); + }); +}); diff --git a/bcbox/frontend/tests/unit/components/layout/Header.spec.ts b/bcbox/frontend/tests/unit/components/layout/Header.spec.ts new file mode 100644 index 00000000..3ccdd48c --- /dev/null +++ b/bcbox/frontend/tests/unit/components/layout/Header.spec.ts @@ -0,0 +1,43 @@ +import { createTestingPinia } from '@pinia/testing'; +import { shallowMount } from '@vue/test-utils'; + +import PrimeVue from 'primevue/config'; +import Header from '@/components/layout/Header.vue'; +import { StorageKey } from '@/utils/constants'; + +// Mock router calls +vi.mock('vue-router', () => ({ + useRouter: () => ({ + push: vi.fn() + }) +})); + +beforeEach(() => { + sessionStorage.setItem( + StorageKey.CONFIG, + JSON.stringify({ + oidc: { + authority: 'abc', + clientId: '123' + } + }) + ); + + vi.clearAllMocks(); +}); + +afterEach(() => { + sessionStorage.clear(); +}); + +// TODO: Figure out Auth service mocking +describe('Header.vue', () => { + it('renders', () => { + const wrapper = shallowMount(Header, { + global: { + plugins: [createTestingPinia(), PrimeVue] + } + }); + expect(wrapper).toBeTruthy(); + }); +}); diff --git a/bcbox/frontend/tests/unit/components/layout/LoginButton.spec.ts b/bcbox/frontend/tests/unit/components/layout/LoginButton.spec.ts new file mode 100644 index 00000000..8facd3b3 --- /dev/null +++ b/bcbox/frontend/tests/unit/components/layout/LoginButton.spec.ts @@ -0,0 +1,152 @@ +import { createTestingPinia } from '@pinia/testing'; +import { mount, shallowMount } from '@vue/test-utils'; +import { useRouter, useRoute } from 'vue-router'; + +import PrimeVue from 'primevue/config'; +import LoginButton from '@/components/layout/LoginButton.vue'; +import { StorageKey } from '@/utils/constants'; +import { RouteNames } from '@/utils/constants'; + +// Mock router calls +vi.mock('vue-router', () => ({ + useRoute: vi.fn(), + useRouter: vi.fn(() => ({ + push: () => {} + })) +})); + +beforeEach(() => { + sessionStorage.setItem( + StorageKey.CONFIG, + JSON.stringify({ + oidc: { + authority: 'abc', + clientId: '123' + } + }) + ); + + vi.clearAllMocks(); +}); + +afterEach(() => { + sessionStorage.clear(); +}); + +describe('LoginButton.vue', () => { + it('renders', () => { + const wrapper = shallowMount(LoginButton, { + global: { + plugins: [ + createTestingPinia({ + initialState: { + auth: { user: {} } + } + }), + PrimeVue + ] + } + }); + expect(wrapper).toBeTruthy(); + }); + + describe('unauthenticated', () => { + it('renders login button', () => { + const wrapper = mount(LoginButton, { + global: { + plugins: [ + createTestingPinia({ + initialState: { + auth: { isAuthenticated: false } + } + }), + PrimeVue + ] + } + }); + + const btn = wrapper.getComponent({ name: 'Button' }); + expect(btn.text()).toBe('Log in'); + }); + + it('navigates to login on click', async () => { + (useRoute as any).mockImplementation(() => ({ + params: { name: RouteNames.LOGIN } + })); + + const push = vi.fn(); + (useRouter as any).mockImplementation(() => ({ + push + })); + + const wrapper = shallowMount(LoginButton, { + global: { + plugins: [ + createTestingPinia({ + initialState: { + auth: { isAuthenticated: false } + } + }), + PrimeVue + ], + stubs: ['router-link', 'router-view'] + } + }); + + const btn = wrapper.getComponent({ name: 'Button' }); + await btn.trigger('click'); + expect(push).toBeCalledTimes(1); + expect(push).toBeCalledWith({ name: RouteNames.LOGIN }); + }); + }); + + describe('authenticated', () => { + it('renders logout button', () => { + const wrapper = mount(LoginButton, { + global: { + plugins: [ + createTestingPinia({ + initialState: { + auth: { isAuthenticated: true } + } + }), + PrimeVue + ] + } + }); + + const btn = wrapper.getComponent({ name: 'Button' }); + expect(btn.text()).toBe('Log out'); + }); + + it('navigates to logout on click', async () => { + (useRoute as any).mockImplementation(() => ({ + params: { name: RouteNames.LOGOUT } + })); + + const push = vi.fn(); + (useRouter as any).mockImplementation(() => ({ + push + })); + + const wrapper = shallowMount(LoginButton, { + global: { + plugins: [ + createTestingPinia({ + initialState: { + auth: { isAuthenticated: true } + } + }), + PrimeVue + ], + stubs: ['router-link', 'router-view'] + } + }); + + const btn = wrapper.getComponent({ name: 'Button' }); + await btn.trigger('click'); + expect(push).toBeCalledTimes(1); + expect(push).toBeCalledWith({ name: RouteNames.LOGOUT }); + }); + }); +}); diff --git a/bcbox/frontend/tests/unit/components/layout/Navbar.spec.ts b/bcbox/frontend/tests/unit/components/layout/Navbar.spec.ts new file mode 100644 index 00000000..db86313e --- /dev/null +++ b/bcbox/frontend/tests/unit/components/layout/Navbar.spec.ts @@ -0,0 +1,83 @@ +import { createTestingPinia } from '@pinia/testing'; +import { mount, shallowMount, RouterLinkStub } from '@vue/test-utils'; + +import Navbar from '@/components/layout/Navbar.vue'; +import { StorageKey } from '@/utils/constants'; +import PrimeVue from 'primevue/config'; + +beforeEach(() => { + sessionStorage.setItem( + StorageKey.CONFIG, + JSON.stringify({ + oidc: { + authority: 'abc', + clientId: '123' + } + }) + ); + + vi.clearAllMocks(); +}); + +afterEach(() => { + sessionStorage.clear(); +}); + +describe('Navbar.vue', () => { + it('renders', () => { + const wrapper = shallowMount(Navbar, { + global: { + plugins: [createTestingPinia(), PrimeVue], + stubs: { + RouterLink: RouterLinkStub + } + } + }); + expect(wrapper).toBeTruthy(); + }); + + it('tests isAuthenticated true', () => { + const wrapper = mount(Navbar, { + global: { + plugins: [ + createTestingPinia({ + initialState: { + auth: { isAuthenticated: true } + } + }), + PrimeVue + ], + stubs: { + RouterLink: RouterLinkStub + } + } + }); + const linkEle = wrapper.findAll('a'); + expect(linkEle).toHaveLength(3); + expect(linkEle[0].text()).toBe('Home'); + expect(linkEle[1].text()).toBe('My Buckets'); + expect(linkEle[2].text()).toBe('Help'); + }); + + it('tests isAuthenticated false', async () => { + const wrapper = mount(Navbar, { + global: { + plugins: [ + createTestingPinia({ + initialState: { + auth: { isAuthenticated: false } + } + }), + PrimeVue + ], + stubs: { + RouterLink: RouterLinkStub + } + } + }); + const linkEle = wrapper.findAll('a'); + expect(linkEle).toHaveLength(2); + expect(linkEle[0].text()).toBe('Home'); + expect(linkEle[1].text()).toBe('Help'); + }); +}); diff --git a/bcbox/frontend/tests/unit/components/layout/ProgressLoader.spec.ts b/bcbox/frontend/tests/unit/components/layout/ProgressLoader.spec.ts new file mode 100644 index 00000000..98f6f8ba --- /dev/null +++ b/bcbox/frontend/tests/unit/components/layout/ProgressLoader.spec.ts @@ -0,0 +1,24 @@ +import { createTestingPinia } from '@pinia/testing'; +import { mount } from '@vue/test-utils'; + +import { ProgressBar } from '@/lib/primevue'; +import PrimeVue from 'primevue/config'; + +beforeEach(() => { + vi.clearAllMocks(); +}); + +afterEach(() => { + sessionStorage.clear(); +}); + +describe('ProgressBar.vue', () => { + it('renders', () => { + const wrapper = mount(ProgressBar, { + global: { + plugins: [createTestingPinia(), PrimeVue] + } + }); + expect(wrapper).toBeTruthy(); + }); +}); diff --git a/bcbox/frontend/tests/unit/components/layout/Spinner.spec.ts b/bcbox/frontend/tests/unit/components/layout/Spinner.spec.ts new file mode 100644 index 00000000..20802bf3 --- /dev/null +++ b/bcbox/frontend/tests/unit/components/layout/Spinner.spec.ts @@ -0,0 +1,23 @@ +import { mount } from '@vue/test-utils'; + +import { ProgressSpinner } from '@/lib/primevue'; +import PrimeVue from 'primevue/config'; + +beforeEach(() => { + vi.clearAllMocks(); +}); + +afterEach(() => { + sessionStorage.clear(); +}); + +describe('ProgressSpinner.vue', () => { + it('renders', () => { + const wrapper = mount(ProgressSpinner, { + global: { + plugins: [PrimeVue] + } + }); + expect(wrapper).toBeTruthy(); + }); +}); diff --git a/bcbox/frontend/tests/unit/store/appStore.spec.ts b/bcbox/frontend/tests/unit/store/appStore.spec.ts new file mode 100644 index 00000000..ff0ba2c2 --- /dev/null +++ b/bcbox/frontend/tests/unit/store/appStore.spec.ts @@ -0,0 +1,48 @@ +import { setActivePinia, createPinia } from 'pinia'; + +import { useAppStore } from '@/store'; + +import type { StoreGeneric } from 'pinia'; + +beforeEach(() => { + setActivePinia(createPinia()); + vi.clearAllMocks(); +}); + +describe('App Store', () => { + let appStore: StoreGeneric; + + beforeEach(() => { + appStore = useAppStore(); + }); + + it('beginDeterminateLoading', () => { + appStore.beginDeterminateLoading(); + expect(appStore.getLoadingValue).toBe(0); + expect(appStore.getLoadingCalls).toBe(1); + expect(appStore.getLoadingMode).toBe('determinate'); + expect(appStore.getIsLoading).toBeTruthy(); + }); + + it('beginIndeterminateLoading', () => { + appStore.beginIndeterminateLoading(); + expect(appStore.getLoadingCalls).toBe(1); + expect(appStore.getLoadingMode).toBe('indeterminate'); + expect(appStore.getIsLoading).toBeTruthy(); + }); + + it('endDeterminateLoading', () => { + appStore.endDeterminateLoading(); + expect(appStore.getLoadingValue).toBe(100); + setTimeout(() => { + expect(appStore.getLoadingCalls).toBe(-1); + }, 300); + }); + + it('endIndeterminateLoading', () => { + appStore.endIndeterminateLoading(); + setTimeout(() => { + expect(appStore.getLoadingCalls).toBe(-1); + }, 300); + }); +}); diff --git a/bcbox/frontend/tests/unit/store/authStore.spec.ts b/bcbox/frontend/tests/unit/store/authStore.spec.ts new file mode 100644 index 00000000..c62c4f4f --- /dev/null +++ b/bcbox/frontend/tests/unit/store/authStore.spec.ts @@ -0,0 +1,72 @@ +import { setActivePinia, createPinia } from 'pinia'; + +// import { useAuthStore } from '@/store'; +// import { AuthService } from '@/services'; + +describe.skip('Auth Store', () => { + //const noop = () => { }; + + beforeEach(() => { + // Creates a fresh pinia and make it active so it's automatically picked + // up by any useStore() call without having to pass it to it: + // `useStore(pinia)` + setActivePinia(createPinia()); + }); + + it('_registerEvents', () => { + // const p = AuthService.prototype; + // const o = p.getOidcSettings; + // const oo = o(); + // const store = useAuthStore(); + // const userManager = AuthService.getUserManager(); + // const addAccessTokenExpiredSpy = jest.spyOn(userManager.events, 'addAccessTokenExpired'); + // const addAccessTokenExpiringSpy = jest.spyOn(userManager.events, 'addAccessTokenExpiring'); + // const addSilentRenewErrorSpy = jest.spyOn(userManager.events, 'addSilentRenewError'); + // const addUserLoadedSpy = jest.spyOn(userManager.events, 'addUserLoaded'); + // const addUserSessionChangedSpy = jest.spyOn(userManager.events, 'addUserSessionChanged'); + // const addUserSignedInSpy = jest.spyOn(userManager.events, 'addUserSignedIn'); + // const addUserSignedOutSpy = jest.spyOn(userManager.events, 'addUserSignedOut'); + // const addUserUnloadedSpy = jest.spyOn(userManager.events, 'addUserUnloaded'); + // addAccessTokenExpiredSpy.mockImplementation(() => { + // return jest.fn(); + // }); + // addAccessTokenExpiringSpy.mockImplementation(() => { + // return jest.fn(); + // }); + // addSilentRenewErrorSpy.mockImplementation(() => { + // return jest.fn(); + // }); + // addUserLoadedSpy.mockImplementation(() => { + // return jest.fn(); + // }); + // addUserSessionChangedSpy.mockImplementation(() => { + // return jest.fn(); + // }); + // addUserSignedInSpy.mockImplementation(() => { + // return jest.fn(); + // }); + // addUserSignedOutSpy.mockImplementation(() => { + // return jest.fn(); + // }); + // addUserUnloadedSpy.mockImplementation(() => { + // return jest.fn(); + // }); + // addAccessTokenExpiredSpy.mockReturnValue(noop); + // addAccessTokenExpiringSpy.mockReturnValue(noop); + // addSilentRenewErrorSpy.mockReturnValue(noop); + // addUserLoadedSpy.mockReturnValue(noop); + // addUserSessionChangedSpy.mockReturnValue(noop); + // addUserSignedInSpy.mockReturnValue(noop); + // addUserSignedOutSpy.mockReturnValue(noop); + // addUserUnloadedSpy.mockReturnValue(noop); + // store._registerEvents(); + // expect(addAccessTokenExpiredSpy).toHaveBeenCalledTimes(1); + // expect(addAccessTokenExpiringSpy).toHaveBeenCalledTimes(1); + // expect(addSilentRenewErrorSpy).toHaveBeenCalledTimes(1); + // expect(addUserLoadedSpy).toHaveBeenCalledTimes(1); + // expect(addUserSessionChangedSpy).toHaveBeenCalledTimes(1); + // expect(addUserSignedInSpy).toHaveBeenCalledTimes(1); + // expect(addUserSignedOutSpy).toHaveBeenCalledTimes(1); + // expect(addUserUnloadedSpy).toHaveBeenCalledTimes(1); + }); +}); diff --git a/bcbox/frontend/tests/unit/store/bucketStore.spec.ts b/bcbox/frontend/tests/unit/store/bucketStore.spec.ts new file mode 100644 index 00000000..5d1cb7ad --- /dev/null +++ b/bcbox/frontend/tests/unit/store/bucketStore.spec.ts @@ -0,0 +1,178 @@ +import { setActivePinia, createPinia } from 'pinia'; + +import * as primevue from '@/lib/primevue'; +import { bucketService } from '@/services'; +import { useAppStore, useBucketStore, usePermissionStore } from '@/store'; +import { StorageKey } from '@/utils/constants'; + +import type { StoreGeneric } from 'pinia'; +import type { SpyInstance } from 'vitest'; +import type { Bucket } from '@/types'; + +const bucket: Bucket = { + active: true, + accessKeyId: 'foo', + bucket: 'bcbox', + bucketId: '000', + bucketName: 'unit', + endpoint: 'https://not.a.url', + key: 'test', + region: 'us-east-1', + secretAccessKey: '123' +}; + +const bucket2: Bucket = { + active: true, + accessKeyId: 'bar', + bucket: 'bcbox', + bucketId: '111', + bucketName: 'unit2', + endpoint: 'https://not.a.url', + key: 'test', + region: 'us-east-1', + secretAccessKey: '456' +}; + +const readPerm = { + bucketId: '000', + id: '0', + permCode: 'READ', + userId: '123' +}; + +const mockToast = vi.fn(); +const useToastSpy = vi.spyOn(primevue, 'useToast'); + +beforeEach(() => { + setActivePinia(createPinia()); + + sessionStorage.setItem( + StorageKey.CONFIG, + JSON.stringify({ + oidc: { + authority: 'abc', + clientId: '123' + } + }) + ); + + vi.clearAllMocks(); + + useToastSpy.mockImplementation(() => ({ error: mockToast, info: mockToast, success: mockToast, warn: mockToast })); +}); + +afterEach(() => { + sessionStorage.clear(); +}); + +describe('Bucket Store', () => { + let appStore: StoreGeneric; + let bucketStore: StoreGeneric; + let permissionStore: StoreGeneric; + + let beginIndeterminateLoadingSpy: SpyInstance; + let endIndeterminateLoadingSpy: SpyInstance; + let fetchBucketPermissionsSpy: SpyInstance; + + let createBucketSpy: SpyInstance; + let searchBucketsSpy: SpyInstance; + let updateBucketSpy: SpyInstance; + + beforeEach(() => { + appStore = useAppStore(); + bucketStore = useBucketStore(); + permissionStore = usePermissionStore(); + + beginIndeterminateLoadingSpy = vi.spyOn(appStore, 'beginIndeterminateLoading'); + endIndeterminateLoadingSpy = vi.spyOn(appStore, 'endIndeterminateLoading'); + fetchBucketPermissionsSpy = vi.spyOn(permissionStore, 'fetchBucketPermissions'); + + createBucketSpy = vi.spyOn(bucketService, 'createBucket'); + searchBucketsSpy = vi.spyOn(bucketService, 'searchBuckets'); + updateBucketSpy = vi.spyOn(bucketService, 'updateBucket'); + }); + + describe('createBucket', () => { + it('calls the service', async () => { + createBucketSpy.mockReturnValue({ data: {} } as any); + + await bucketStore.createBucket(bucket); + + expect(beginIndeterminateLoadingSpy).toHaveBeenCalledTimes(1); + expect(createBucketSpy).toHaveBeenCalledTimes(1); + expect(createBucketSpy).toHaveBeenCalledWith(bucket); + expect(endIndeterminateLoadingSpy).toHaveBeenCalledTimes(1); + }); + }); + + describe('fetchBuckets', () => { + it('gets the bucket list', async () => { + permissionStore.bucketPermissions = [readPerm]; + + searchBucketsSpy.mockReturnValue({ data: [bucket] } as any); + fetchBucketPermissionsSpy.mockReturnValue([readPerm] as any); + + await bucketStore.fetchBuckets({ userId: '123', objectPerms: true }); + + expect(beginIndeterminateLoadingSpy).toHaveBeenCalledTimes(1); + expect(fetchBucketPermissionsSpy).toHaveBeenCalledTimes(1); + expect(fetchBucketPermissionsSpy).toBeCalledWith({ userId: '123', objectPerms: true }); + expect(searchBucketsSpy).toHaveBeenCalledTimes(1); + expect(searchBucketsSpy).toBeCalledWith({ bucketId: ['000'] }); + expect(endIndeterminateLoadingSpy).toHaveBeenCalledTimes(1); + expect(bucketStore.getBuckets).toStrictEqual([bucket]); + }); + + it('does not change state on error', async () => { + permissionStore.bucketPermissions = [readPerm]; + + searchBucketsSpy.mockImplementation(() => { + throw new Error(); + }); + fetchBucketPermissionsSpy.mockReturnValue([readPerm] as any); + + await bucketStore.fetchBuckets({ userId: '123', objectPerms: true }); + + expect(beginIndeterminateLoadingSpy).toHaveBeenCalledTimes(1); + expect(fetchBucketPermissionsSpy).toHaveBeenCalledTimes(1); + expect(fetchBucketPermissionsSpy).toBeCalledWith({ userId: '123', objectPerms: true }); + expect(searchBucketsSpy).toHaveBeenCalledTimes(1); + expect(searchBucketsSpy).toBeCalledWith({ bucketId: ['000'] }); + expect(mockToast).toHaveBeenCalledTimes(1); + expect(mockToast).toHaveBeenCalledWith('Fetching buckets', new Error()); + expect(endIndeterminateLoadingSpy).toHaveBeenCalledTimes(1); + expect(bucketStore.getBuckets).toStrictEqual([]); + }); + }); + + describe('findBucketById', () => { + it('returns a matching bucket', () => { + bucketStore.buckets = [bucket]; + + const result = bucketStore.findBucketById('000'); + + expect(result).toStrictEqual(bucket); + }); + + it('returns undefined when no matching bucket is found', () => { + bucketStore.buckets = [bucket]; + + const result = bucketStore.findBucketById('foo'); + + expect(result).toStrictEqual(undefined); + }); + }); + + describe('updateBucket', () => { + it('updates the bucket', async () => { + updateBucketSpy.mockReturnValue({ data: {} } as any); + + await bucketStore.updateBucket('111', bucket2); + + expect(beginIndeterminateLoadingSpy).toHaveBeenCalledTimes(1); + expect(updateBucketSpy).toHaveBeenCalledTimes(1); + expect(updateBucketSpy).toHaveBeenCalledWith('111', bucket2); + expect(endIndeterminateLoadingSpy).toHaveBeenCalledTimes(1); + }); + }); +}); diff --git a/bcbox/frontend/tests/unit/store/configStore.spec.ts b/bcbox/frontend/tests/unit/store/configStore.spec.ts new file mode 100644 index 00000000..acd0a6c3 --- /dev/null +++ b/bcbox/frontend/tests/unit/store/configStore.spec.ts @@ -0,0 +1,56 @@ +import { setActivePinia, createPinia } from 'pinia'; + +import { ConfigService } from '@/services'; +import { useConfigStore } from '@/store'; +import { StorageKey } from '@/utils/constants'; + +import type { StoreGeneric } from 'pinia'; +import type { SpyInstance } from 'vitest'; + +beforeEach(() => { + setActivePinia(createPinia()); + + sessionStorage.setItem( + StorageKey.CONFIG, + JSON.stringify({ + oidc: { + authority: 'abc', + clientId: '123' + } + }) + ); + + vi.clearAllMocks(); +}); + +afterEach(() => { + sessionStorage.clear(); +}); + +describe('Config Store', () => { + let configService: ConfigService; + let configStore: StoreGeneric; + + let configServiceInitSpy: SpyInstance; + let configServiceGetConfigSpy: SpyInstance; + + beforeEach(() => { + configService = new ConfigService(); + configStore = useConfigStore(); + + configServiceInitSpy = vi.spyOn(ConfigService, 'init'); + configServiceGetConfigSpy = vi.spyOn(configService, 'getConfig'); + }); + + describe('init', () => { + it('initializes the service and sets the state', async () => { + configServiceGetConfigSpy.mockReturnValue(sessionStorage.getItem(StorageKey.CONFIG)); + + await configStore.init(); + + expect(configServiceInitSpy).toHaveBeenCalledTimes(1); + expect(configServiceGetConfigSpy).toHaveBeenCalledTimes(1); + expect(configStore.getConfig).toStrictEqual(sessionStorage.getItem(StorageKey.CONFIG)); + }); + }); +}); diff --git a/bcbox/frontend/tests/unit/store/metadataStore.spec.ts b/bcbox/frontend/tests/unit/store/metadataStore.spec.ts new file mode 100644 index 00000000..0e5c7745 --- /dev/null +++ b/bcbox/frontend/tests/unit/store/metadataStore.spec.ts @@ -0,0 +1,116 @@ +import { setActivePinia, createPinia } from 'pinia'; + +import * as primevue from '@/lib/primevue'; +import { objectService } from '@/services'; +import { useAppStore, useMetadataStore } from '@/store'; + +import type { StoreGeneric } from 'pinia'; +import type { SpyInstance } from 'vitest'; +import type { Metadata } from '@/types'; + +const meta: Metadata = { + metadata: [ + { key: 'foo', value: 'bar' }, + { key: 'baz', value: 'bam' } + ], + objectId: '000' +}; + +const mockToast = vi.fn(); +const useToastSpy = vi.spyOn(primevue, 'useToast'); + +beforeEach(() => { + setActivePinia(createPinia()); + vi.clearAllMocks(); + useToastSpy.mockImplementation(() => ({ error: mockToast, info: mockToast, success: mockToast, warn: mockToast })); +}); + +afterEach(() => { + sessionStorage.clear(); +}); + +describe('Metadata Store', () => { + let appStore: StoreGeneric; + let metadataStore: StoreGeneric; + + let beginIndeterminateLoadingSpy: SpyInstance; + let endIndeterminateLoadingSpy: SpyInstance; + + let getMetadataSpy: SpyInstance; + + beforeEach(() => { + appStore = useAppStore(); + metadataStore = useMetadataStore(); + + beginIndeterminateLoadingSpy = vi.spyOn(appStore, 'beginIndeterminateLoading'); + endIndeterminateLoadingSpy = vi.spyOn(appStore, 'endIndeterminateLoading'); + + getMetadataSpy = vi.spyOn(objectService, 'getMetadata'); + }); + + describe('fetchMetadata', () => { + it('fetches the metadata', async () => { + getMetadataSpy.mockReturnValue({ data: [meta] } as any); + + await metadataStore.fetchMetadata({ objectId: '000' }); + + expect(beginIndeterminateLoadingSpy).toHaveBeenCalledTimes(1); + expect(getMetadataSpy).toHaveBeenCalledTimes(1); + expect(getMetadataSpy).toHaveBeenCalledWith(null, { objectId: '000' }); + expect(endIndeterminateLoadingSpy).toHaveBeenCalledTimes(1); + expect(metadataStore.getMetadata).toStrictEqual([meta]); + }); + + it('does not change state on error', async () => { + getMetadataSpy.mockImplementation(() => { + throw new Error(); + }); + + await metadataStore.fetchMetadata({ objectId: '000' }); + + expect(beginIndeterminateLoadingSpy).toHaveBeenCalledTimes(1); + expect(getMetadataSpy).toHaveBeenCalledTimes(1); + expect(getMetadataSpy).toHaveBeenCalledWith(null, { objectId: '000' }); + expect(mockToast).toHaveBeenCalledTimes(1); + expect(mockToast).toHaveBeenCalledWith('Fetching metadata', new Error()); + expect(endIndeterminateLoadingSpy).toHaveBeenCalledTimes(1); + expect(metadataStore.getMetadata).toStrictEqual([]); + }); + }); + + describe('findMetadataByObjectId', () => { + it('returns matching metadata', async () => { + metadataStore.metadata = [meta]; + + const result = metadataStore.findMetadataByObjectId('000'); + + expect(result).toStrictEqual(meta); + }); + + it('returns undefined when no match found', async () => { + metadataStore.metadata = [meta]; + + const result = metadataStore.findMetadataByObjectId('111'); + + expect(result).toStrictEqual(undefined); + }); + }); + + describe('findValue', () => { + it('returns matching metadata', async () => { + metadataStore.metadata = [meta]; + + const result = metadataStore.findValue('000', 'foo'); + + expect(result).toStrictEqual('bar'); + }); + + it('returns undefined when no match found', async () => { + metadataStore.metadata = [meta]; + + const result = metadataStore.findValue('111', 'foo'); + + expect(result).toStrictEqual(undefined); + }); + }); +}); diff --git a/bcbox/frontend/tests/unit/store/objectStore.spec.ts b/bcbox/frontend/tests/unit/store/objectStore.spec.ts new file mode 100644 index 00000000..6bc2d20d --- /dev/null +++ b/bcbox/frontend/tests/unit/store/objectStore.spec.ts @@ -0,0 +1,282 @@ +import { setActivePinia, createPinia } from 'pinia'; + +import * as primevue from '@/lib/primevue'; +import { objectService } from '@/services'; +import { useAppStore, useObjectStore, usePermissionStore } from '@/store'; +import { StorageKey } from '@/utils/constants'; + +import type { StoreGeneric } from 'pinia'; +import type { SpyInstance } from 'vitest'; +import type { COMSObject } from '@/types'; + +const obj: COMSObject = { + active: true, + bucketId: '000', + id: '000', + name: 'object1', + path: 'dev/test', + public: false +}; + +const obj2: COMSObject = { + active: true, + bucketId: '000', + id: '111', + name: 'object2', + path: 'dev/test', + public: false +}; + +const readPerm = { + objectId: '000', + id: '0', + permCode: 'READ', + userId: '123' +}; + +const mockToast = vi.fn(); +const useToastSpy = vi.spyOn(primevue, 'useToast'); + +beforeEach(() => { + setActivePinia(createPinia()); + + sessionStorage.setItem( + StorageKey.CONFIG, + JSON.stringify({ + oidc: { + authority: 'abc', + clientId: '123' + } + }) + ); + + vi.clearAllMocks(); + + useToastSpy.mockImplementation(() => ({ error: mockToast, info: mockToast, success: mockToast, warn: mockToast })); +}); + +afterEach(() => { + sessionStorage.clear(); +}); + +describe('Object Store', () => { + let appStore: StoreGeneric; + let objectStore: StoreGeneric; + let permissionStore: StoreGeneric; + + let beginIndeterminateLoadingSpy: SpyInstance; + let endIndeterminateLoadingSpy: SpyInstance; + let fetchObjectPermissionsSpy: SpyInstance; + + let createObjectSpy: SpyInstance; + let deleteObjectSpy: SpyInstance; + // let fetchObjectsSpy: SpyInstance; + let getObjectSpy: SpyInstance; + let searchObjectsSpy: SpyInstance; + + beforeEach(() => { + appStore = useAppStore(); + objectStore = useObjectStore(); + permissionStore = usePermissionStore(); + + beginIndeterminateLoadingSpy = vi.spyOn(appStore, 'beginIndeterminateLoading'); + endIndeterminateLoadingSpy = vi.spyOn(appStore, 'endIndeterminateLoading'); + fetchObjectPermissionsSpy = vi.spyOn(permissionStore, 'fetchObjectPermissions'); + + createObjectSpy = vi.spyOn(objectService, 'createObject'); + deleteObjectSpy = vi.spyOn(objectService, 'deleteObject'); + // fetchObjectsSpy = vi.spyOn(objectStore, 'fetchObjects'); + getObjectSpy = vi.spyOn(objectService, 'getObject'); + searchObjectsSpy = vi.spyOn(objectService, 'searchObjects'); + }); + + describe('createObject', () => { + it('creates the object', async () => { + const blob: any = { + some: 'data' + }; + + createObjectSpy.mockReturnValue({} as any); + + await objectStore.createObject(blob, { metadata: [] }, { bucketId: '000', tagset: [] }, { timeout: 0 }); + + expect(beginIndeterminateLoadingSpy).toHaveBeenCalledTimes(1); + expect(createObjectSpy).toHaveBeenCalledTimes(1); + expect(createObjectSpy).toHaveBeenCalledWith( + blob, + { + metadata: [] + }, + { + bucketId: '000', + tagset: [] + }, + { timeout: 0 } + ); + expect(endIndeterminateLoadingSpy).toHaveBeenCalledTimes(1); + }); + }); + + describe('deleteObject', () => { + // TODO: Figure out why we can't mock fetchObjects here + // TODO: Figure out why endIndeterminateLoadingSpy is only being called twice + it('deletes the object', async () => { + objectStore.objects = [obj, obj2]; + + deleteObjectSpy.mockImplementation(vi.fn()); + + await objectStore.deleteObject(obj.id); + + expect(beginIndeterminateLoadingSpy).toHaveBeenCalledTimes(3); + expect(deleteObjectSpy).toHaveBeenCalledTimes(1); + expect(endIndeterminateLoadingSpy).toHaveBeenCalledTimes(2); + }); + }); + + describe('downloadObject', () => { + it('gets the most recent object', async () => { + getObjectSpy.mockReturnValue({} as any); + + await objectStore.downloadObject(obj.id); + + expect(beginIndeterminateLoadingSpy).toHaveBeenCalledTimes(1); + expect(getObjectSpy).toHaveBeenCalledTimes(1); + expect(getObjectSpy).toHaveBeenCalledWith(obj.id, undefined); + expect(endIndeterminateLoadingSpy).toHaveBeenCalledTimes(1); + }); + + it('gets the object by version', async () => { + getObjectSpy.mockReturnValue({} as any); + + await objectStore.downloadObject(obj.id, '1'); + + expect(beginIndeterminateLoadingSpy).toHaveBeenCalledTimes(1); + expect(getObjectSpy).toHaveBeenCalledTimes(1); + expect(getObjectSpy).toHaveBeenCalledWith(obj.id, '1'); + expect(endIndeterminateLoadingSpy).toHaveBeenCalledTimes(1); + }); + + it('displays a toast on error', async () => { + getObjectSpy.mockImplementation(() => { + throw new Error(); + }); + + await objectStore.downloadObject(obj.id); + + expect(beginIndeterminateLoadingSpy).toHaveBeenCalledTimes(1); + expect(getObjectSpy).toHaveBeenCalledTimes(1); + expect(getObjectSpy).toHaveBeenCalledWith(obj.id, undefined); + expect(mockToast).toHaveBeenCalledTimes(1); + expect(mockToast).toHaveBeenCalledWith('Downloading object', new Error()); + expect(endIndeterminateLoadingSpy).toHaveBeenCalledTimes(1); + }); + }); + + describe('fetchObjects', () => { + it('gets the object list', async () => { + permissionStore.objectPermissions = [readPerm]; + + searchObjectsSpy.mockResolvedValue({ data: [obj] } as any); + fetchObjectPermissionsSpy.mockReturnValue([readPerm] as any); + + await objectStore.fetchObjects({ bucketId: '000', userId: '123', bucketPerms: true }); + + expect(beginIndeterminateLoadingSpy).toHaveBeenCalledTimes(1); + expect(fetchObjectPermissionsSpy).toHaveBeenCalledTimes(1); + expect(fetchObjectPermissionsSpy).toBeCalledWith({ bucketId: '000', userId: '123', bucketPerms: true }); + expect(searchObjectsSpy).toHaveBeenCalledTimes(1); + expect(searchObjectsSpy).toBeCalledWith( + { + bucketId: ['000'], + objectId: ['000'], + deleteMarker: false, + latest: true + }, + {} + ); + expect(endIndeterminateLoadingSpy).toHaveBeenCalledTimes(1); + expect(objectStore.getObjects).toStrictEqual([obj]); + }); + + it('does not change state on error', async () => { + permissionStore.objectPermissions = [readPerm]; + + searchObjectsSpy.mockImplementation(() => { + throw new Error(); + }); + fetchObjectPermissionsSpy.mockReturnValue([readPerm] as any); + + await objectStore.fetchObjects({ bucketId: '000', userId: '123', bucketPerms: true }); + + expect(beginIndeterminateLoadingSpy).toHaveBeenCalledTimes(1); + expect(fetchObjectPermissionsSpy).toHaveBeenCalledTimes(1); + expect(fetchObjectPermissionsSpy).toBeCalledWith({ bucketId: '000', userId: '123', bucketPerms: true }); + expect(searchObjectsSpy).toHaveBeenCalledTimes(1); + expect(searchObjectsSpy).toBeCalledWith( + { + bucketId: ['000'], + objectId: ['000'], + deleteMarker: false, + latest: true + }, + {} + ); + expect(mockToast).toHaveBeenCalledTimes(1); + expect(mockToast).toHaveBeenCalledWith('Fetching objects', new Error()); + expect(endIndeterminateLoadingSpy).toHaveBeenCalledTimes(1); + expect(objectStore.getObjects).toStrictEqual([]); + }); + }); + + describe('findObjectById', () => { + it('returns a matching bucket', () => { + objectStore.objects = [obj]; + + const result = objectStore.findObjectById('000'); + + expect(result).toStrictEqual(obj); + }); + + it('returns undefined when no matching bucket is found', () => { + objectStore.objects = [obj]; + + const result = objectStore.findObjectById('foo'); + + expect(result).toStrictEqual(undefined); + }); + }); + + describe('headObject', () => { + it('calls the service', async () => { + const headObjectSpy = vi.spyOn(objectService, 'headObject'); + + await objectStore.headObject('000'); + + expect(beginIndeterminateLoadingSpy).toHaveBeenCalledTimes(1); + expect(headObjectSpy).toHaveBeenCalledTimes(1); + expect(headObjectSpy).toHaveBeenLastCalledWith('000'); + expect(endIndeterminateLoadingSpy).toHaveBeenCalledTimes(1); + }); + }); + + describe('setSelectedObjects', () => { + it('sets the state', () => { + objectStore.setSelectedObjects([obj, obj2]); + + expect(objectStore.getSelectedObjects).toStrictEqual([obj, obj2]); + }); + }); + + describe('togglePublic', () => { + it('calls the service', async () => { + const togglePublicSpy = vi.spyOn(objectService, 'togglePublic'); + + await objectStore.togglePublic('000', true); + + expect(beginIndeterminateLoadingSpy).toHaveBeenCalledTimes(1); + expect(togglePublicSpy).toHaveBeenCalledTimes(1); + expect(togglePublicSpy).toHaveBeenLastCalledWith('000', true); + expect(endIndeterminateLoadingSpy).toHaveBeenCalledTimes(1); + }); + }); +}); diff --git a/bcbox/frontend/tests/unit/store/tagStore.spec.ts b/bcbox/frontend/tests/unit/store/tagStore.spec.ts new file mode 100644 index 00000000..21da589f --- /dev/null +++ b/bcbox/frontend/tests/unit/store/tagStore.spec.ts @@ -0,0 +1,98 @@ +import { setActivePinia, createPinia } from 'pinia'; + +import * as primevue from '@/lib/primevue'; +import { objectService } from '@/services'; +import { useAppStore, useTagStore } from '@/store'; + +import type { StoreGeneric } from 'pinia'; +import type { SpyInstance } from 'vitest'; +import type { Tagging } from '@/types'; + +const mockToast = vi.fn(); +const useToastSpy = vi.spyOn(primevue, 'useToast'); + +const tag: Tagging = { + tagset: [ + { key: 'foo', value: 'bar' }, + { key: 'baz', value: 'bam' } + ], + objectId: '000' +}; + +beforeEach(() => { + setActivePinia(createPinia()); + vi.clearAllMocks(); + useToastSpy.mockImplementation(() => ({ error: mockToast, info: mockToast, success: mockToast, warn: mockToast })); +}); + +afterEach(() => { + sessionStorage.clear(); +}); + +describe('Config Store', () => { + let appStore: StoreGeneric; + let tagStore: StoreGeneric; + + let beginIndeterminateLoadingSpy: SpyInstance; + let endIndeterminateLoadingSpy: SpyInstance; + + let getTaggingSpy: SpyInstance; + + beforeEach(() => { + appStore = useAppStore(); + tagStore = useTagStore(); + + beginIndeterminateLoadingSpy = vi.spyOn(appStore, 'beginIndeterminateLoading'); + endIndeterminateLoadingSpy = vi.spyOn(appStore, 'endIndeterminateLoading'); + + getTaggingSpy = vi.spyOn(objectService, 'getObjectTagging'); + }); + + describe('fetchTagging', () => { + it('fetches the tags', async () => { + getTaggingSpy.mockReturnValue({ data: [tag] } as any); + + await tagStore.fetchTagging({ objectId: '000' }); + + expect(beginIndeterminateLoadingSpy).toHaveBeenCalledTimes(1); + expect(getTaggingSpy).toHaveBeenCalledTimes(1); + expect(getTaggingSpy).toHaveBeenCalledWith({ objectId: '000' }); + expect(endIndeterminateLoadingSpy).toHaveBeenCalledTimes(1); + expect(tagStore.getTagging).toStrictEqual([tag]); + }); + + it('does not change state on error', async () => { + getTaggingSpy.mockImplementation(() => { + throw new Error(); + }); + + await tagStore.fetchTagging({ objectId: '000' }); + + expect(beginIndeterminateLoadingSpy).toHaveBeenCalledTimes(1); + expect(getTaggingSpy).toHaveBeenCalledTimes(1); + expect(getTaggingSpy).toHaveBeenCalledWith({ objectId: '000' }); + expect(mockToast).toHaveBeenCalledTimes(1); + expect(mockToast).toHaveBeenCalledWith('Fetching tags', new Error()); + expect(endIndeterminateLoadingSpy).toHaveBeenCalledTimes(1); + expect(tagStore.getTagging).toStrictEqual([]); + }); + }); + + describe('findTaggingByObjectId', () => { + it('returns matching metadata', async () => { + tagStore.tagging = [tag]; + + const result = tagStore.findTaggingByObjectId('000'); + + expect(result).toStrictEqual(tag); + }); + + it('returns undefined when no match found', async () => { + tagStore.tagging = [tag]; + + const result = tagStore.findTaggingByObjectId('111'); + + expect(result).toStrictEqual(undefined); + }); + }); +}); diff --git a/bcbox/frontend/tests/unit/store/userStore.spec.ts b/bcbox/frontend/tests/unit/store/userStore.spec.ts new file mode 100644 index 00000000..4f3b4c2e --- /dev/null +++ b/bcbox/frontend/tests/unit/store/userStore.spec.ts @@ -0,0 +1,143 @@ +import { setActivePinia, createPinia } from 'pinia'; + +import * as primevue from '@/lib/primevue'; +import { userService } from '@/services'; +import { useAppStore, useUserStore } from '@/store'; + +import type { StoreGeneric } from 'pinia'; +import type { SpyInstance } from 'vitest'; +import type { User } from '@/types'; + +const user: User = { + active: true, + email: 'foo@bar.com', + firstName: 'foo', + fullName: 'foo bar', + identityId: 'someId', + idp: 'someIdp', + lastName: 'bar', + userId: '000', + username: 'foobar', + elevatedRights: true +}; + +const user2: User = { + active: true, + email: 'test@dev.com', + firstName: 'baz', + fullName: 'baz bam', + identityId: 'someId', + idp: 'someIdp', + lastName: 'bam', + userId: '111', + username: 'bazbam', + elevatedRights: true +}; + +const noIdUser: User = { + active: true, + email: 'foo@bar.com', + firstName: 'foo', + fullName: 'foo bar', + identityId: null, + idp: '', + lastName: 'bar', + userId: '000', + username: 'foobar', + elevatedRights: true +}; + +const mockToast = vi.fn(); +const useToastSpy = vi.spyOn(primevue, 'useToast'); + +beforeEach(() => { + setActivePinia(createPinia()); + vi.clearAllMocks(); + useToastSpy.mockImplementation(() => ({ error: mockToast, info: mockToast, success: mockToast, warn: mockToast })); +}); + +afterEach(() => { + sessionStorage.clear(); +}); + +describe('User Store', () => { + let appStore: StoreGeneric; + let userStore: StoreGeneric; + + let beginIndeterminateLoadingSpy: SpyInstance; + let endIndeterminateLoadingSpy: SpyInstance; + + let searchForUsersSpy: SpyInstance; + + beforeEach(() => { + appStore = useAppStore(); + userStore = useUserStore(); + + beginIndeterminateLoadingSpy = vi.spyOn(appStore, 'beginIndeterminateLoading'); + endIndeterminateLoadingSpy = vi.spyOn(appStore, 'endIndeterminateLoading'); + + searchForUsersSpy = vi.spyOn(userService, 'searchForUsers'); + }); + + describe('fetchUsers', () => { + it('sets the state from the search results', async () => { + searchForUsersSpy.mockReturnValue({ data: [user] } as any); + + await userStore.fetchUsers({ lastName: 'bar' }); + + expect(beginIndeterminateLoadingSpy).toHaveBeenCalledTimes(1); + expect(searchForUsersSpy).toHaveBeenCalledTimes(1); + expect(searchForUsersSpy).toHaveBeenCalledWith({ lastName: 'bar' }); + expect(endIndeterminateLoadingSpy).toHaveBeenCalledTimes(1); + expect(userStore.userSearch).toStrictEqual([user]); + }); + + it('filters out users without an IDP', async () => { + searchForUsersSpy.mockReturnValue({ data: [noIdUser] } as any); + + await userStore.fetchUsers({ lastName: 'bar' }); + + expect(beginIndeterminateLoadingSpy).toHaveBeenCalledTimes(1); + expect(searchForUsersSpy).toHaveBeenCalledTimes(1); + expect(searchForUsersSpy).toHaveBeenCalledWith({ lastName: 'bar' }); + expect(endIndeterminateLoadingSpy).toHaveBeenCalledTimes(1); + expect(userStore.userSearch).toStrictEqual([]); + }); + + it('displays toast on error', async () => { + searchForUsersSpy.mockImplementation(() => { + throw new Error(); + }); + + await userStore.fetchUsers({ lastName: 'bar' }); + + expect(beginIndeterminateLoadingSpy).toHaveBeenCalledTimes(1); + expect(searchForUsersSpy).toHaveBeenCalledTimes(1); + expect(searchForUsersSpy).toHaveBeenCalledWith({ lastName: 'bar' }); + expect(useToastSpy).toHaveBeenCalledTimes(1); + expect(mockToast).toHaveBeenCalledTimes(1); + expect(mockToast).toHaveBeenCalledWith('Searching users', new Error()); + expect(endIndeterminateLoadingSpy).toHaveBeenCalledTimes(1); + expect(userStore.userSearch).toStrictEqual([]); + }); + }); + + describe('clearSearch', () => { + it('empties the search state', async () => { + userStore.userSearch = [user]; + + expect(userStore.getUserSearch).toStrictEqual([user]); + userStore.clearSearch(); + expect(userStore.getUserSearch).toStrictEqual([]); + }); + }); + + describe('findUsersById', () => { + it('returns the correct subset', async () => { + userStore.userSearch = [user, user2]; + + const result = userStore.findUsersById(['000']); + expect(result).toStrictEqual([user]); + }); + }); +}); diff --git a/bcbox/frontend/tests/unit/store/versionStore.spec.ts b/bcbox/frontend/tests/unit/store/versionStore.spec.ts new file mode 100644 index 00000000..c9379a25 --- /dev/null +++ b/bcbox/frontend/tests/unit/store/versionStore.spec.ts @@ -0,0 +1,278 @@ +import { setActivePinia, createPinia } from 'pinia'; + +import * as primevue from '@/lib/primevue'; +import { objectService, versionService } from '@/services'; +import { useAppStore, useVersionStore } from '@/store'; + +import type { StoreGeneric } from 'pinia'; +import type { SpyInstance } from 'vitest'; +import type { Metadata, Tagging, Version } from '@/types'; + +const meta: Metadata = { + metadata: [ + { key: 'foo', value: 'bar' }, + { key: 'baz', value: 'bam' } + ], + versionId: '000' +}; + +const tag: Tagging = { + tagset: [ + { key: 'foo', value: 'bar' }, + { key: 'baz', value: 'bam' } + ], + versionId: '000' +}; + +const version: Version = { + deleteMarker: false, + id: '123', + mimeType: 'image/jpg', + objectId: '000', + s3VersionId: 's3123', + createdAt: '2023-05-01T22:18:12.553Z' +}; + +const versionOld: Version = { + deleteMarker: false, + id: '110', + mimeType: 'image/jpg', + objectId: '000', + s3VersionId: 's2000', + createdAt: '2022-05-01T18:25:42.462Z' +}; + +const mockToast = vi.fn(); +const useToastSpy = vi.spyOn(primevue, 'useToast'); + +beforeEach(() => { + setActivePinia(createPinia()); + vi.clearAllMocks(); + useToastSpy.mockImplementation(() => ({ error: mockToast, info: mockToast, success: mockToast, warn: mockToast })); +}); + +afterEach(() => { + sessionStorage.clear(); +}); + +describe('Version Store', () => { + let appStore: StoreGeneric; + let versionStore: StoreGeneric; + + let beginIndeterminateLoadingSpy: SpyInstance; + let endIndeterminateLoadingSpy: SpyInstance; + + let getMetadataSpy: SpyInstance; + let getTaggingSpy: SpyInstance; + let getVersionsSpy: SpyInstance; + + beforeEach(() => { + appStore = useAppStore(); + versionStore = useVersionStore(); + + beginIndeterminateLoadingSpy = vi.spyOn(appStore, 'beginIndeterminateLoading'); + endIndeterminateLoadingSpy = vi.spyOn(appStore, 'endIndeterminateLoading'); + + getMetadataSpy = vi.spyOn(versionService, 'getMetadata'); + getTaggingSpy = vi.spyOn(versionService, 'getObjectTagging'); + getVersionsSpy = vi.spyOn(objectService, 'listObjectVersion'); + }); + + describe('fetchMetadata', () => { + it('fetches the metadata', async () => { + getMetadataSpy.mockReturnValue({ data: [meta] } as any); + + await versionStore.fetchMetadata({ versionId: '000' }); + + expect(beginIndeterminateLoadingSpy).toHaveBeenCalledTimes(1); + expect(getMetadataSpy).toHaveBeenCalledTimes(1); + expect(getMetadataSpy).toHaveBeenCalledWith(null, { versionId: '000' }); + expect(endIndeterminateLoadingSpy).toHaveBeenCalledTimes(1); + expect(versionStore.getMetadata).toStrictEqual([meta]); + }); + + it('does not change state on error', async () => { + getMetadataSpy.mockImplementation(() => { + throw new Error(); + }); + + await versionStore.fetchMetadata({ versionId: '000' }); + + expect(beginIndeterminateLoadingSpy).toHaveBeenCalledTimes(1); + expect(getMetadataSpy).toHaveBeenCalledTimes(1); + expect(getMetadataSpy).toHaveBeenCalledWith(null, { versionId: '000' }); + expect(mockToast).toHaveBeenCalledTimes(1); + expect(mockToast).toHaveBeenCalledWith('Fetching metadata', new Error()); + expect(endIndeterminateLoadingSpy).toHaveBeenCalledTimes(1); + expect(versionStore.getMetadata).toStrictEqual([]); + }); + }); + + describe('fetchTagging', () => { + it('fetches the tags', async () => { + getTaggingSpy.mockReturnValue({ data: [tag] } as any); + + await versionStore.fetchTagging({ versionId: '000' }); + + expect(beginIndeterminateLoadingSpy).toHaveBeenCalledTimes(1); + expect(getTaggingSpy).toHaveBeenCalledTimes(1); + expect(getTaggingSpy).toHaveBeenCalledWith({ versionId: '000' }); + expect(endIndeterminateLoadingSpy).toHaveBeenCalledTimes(1); + expect(versionStore.getTagging).toStrictEqual([tag]); + }); + + it('does not change state on error', async () => { + getTaggingSpy.mockImplementation(() => { + throw new Error(); + }); + + await versionStore.fetchTagging({ versionId: '000' }); + + expect(beginIndeterminateLoadingSpy).toHaveBeenCalledTimes(1); + expect(getTaggingSpy).toHaveBeenCalledTimes(1); + expect(getTaggingSpy).toHaveBeenCalledWith({ versionId: '000' }); + expect(mockToast).toHaveBeenCalledTimes(1); + expect(mockToast).toHaveBeenCalledWith('Fetching tags', new Error()); + expect(endIndeterminateLoadingSpy).toHaveBeenCalledTimes(1); + expect(versionStore.getTagging).toStrictEqual([]); + }); + }); + + describe('fetchVersions', () => { + it('fetches the versions', async () => { + getVersionsSpy.mockReturnValue({ data: [version] } as any); + + await versionStore.fetchVersions({ objectId: '000' }); + + expect(beginIndeterminateLoadingSpy).toHaveBeenCalledTimes(1); + expect(getVersionsSpy).toHaveBeenCalledTimes(1); + expect(getVersionsSpy).toHaveBeenCalledWith('000'); + expect(endIndeterminateLoadingSpy).toHaveBeenCalledTimes(1); + expect(versionStore.getVersions).toStrictEqual([version]); + }); + + it('does not change state on error', async () => { + getVersionsSpy.mockImplementation(() => { + throw new Error(); + }); + + await versionStore.fetchVersions({ objectId: '000' }); + + expect(beginIndeterminateLoadingSpy).toHaveBeenCalledTimes(1); + expect(getVersionsSpy).toHaveBeenCalledTimes(1); + expect(getVersionsSpy).toHaveBeenCalledWith('000'); + expect(mockToast).toHaveBeenCalledTimes(1); + expect(mockToast).toHaveBeenCalledWith('Fetching versions', new Error()); + expect(endIndeterminateLoadingSpy).toHaveBeenCalledTimes(1); + expect(versionStore.getTagging).toStrictEqual([]); + }); + }); + + describe('findLatestVersionIdByObjectId', () => { + it('returns latest version', async () => { + versionStore.versions = [versionOld, version]; + + const result = versionStore.findLatestVersionIdByObjectId('000'); + + expect(result).toStrictEqual('123'); + }); + + it('returns undefined when no matches found', async () => { + versionStore.versions = [versionOld, version]; + + const result = versionStore.findLatestVersionIdByObjectId('111'); + + expect(result).toStrictEqual(undefined); + }); + }); + + describe('findMetadataByVersionId', () => { + it('returns matching metadata', async () => { + versionStore.metadata = [meta]; + + const result = versionStore.findMetadataByVersionId('000'); + + expect(result).toStrictEqual(meta); + }); + + it('returns undefined when no match found', async () => { + versionStore.metadata = [meta]; + + const result = versionStore.findMetadataByVersionId('111'); + + expect(result).toStrictEqual(undefined); + }); + }); + + describe('findMetadataValue', () => { + it('returns matching metadata', async () => { + versionStore.metadata = [meta]; + + const result = versionStore.findMetadataValue('000', 'foo'); + + expect(result).toStrictEqual('bar'); + }); + + it('returns undefined when no match found', async () => { + versionStore.metadata = [meta]; + + const result = versionStore.findMetadataValue('111', 'foo'); + + expect(result).toStrictEqual(undefined); + }); + }); + + describe('findTaggingByVersionId', () => { + it('returns matching metadata', async () => { + versionStore.tagging = [tag]; + + const result = versionStore.findTaggingByVersionId('000'); + + expect(result).toStrictEqual(tag); + }); + + it('returns undefined when no match found', async () => { + versionStore.tagging = [tag]; + + const result = versionStore.findTaggingByVersionId('111'); + + expect(result).toStrictEqual(undefined); + }); + }); + + describe('findVersionById', () => { + it('returns matching version', async () => { + versionStore.versions = [version]; + + const result = versionStore.findVersionById('123'); + + expect(result).toStrictEqual(version); + }); + + it('returns undefined when no match found', async () => { + versionStore.versions = [version]; + + const result = versionStore.findVersionById('100'); + + expect(result).toStrictEqual(undefined); + }); + }); + + describe('findVersionsByObjectId', () => { + it('returns matching versions', async () => { + versionStore.versions = [version]; + + const result = versionStore.findVersionsByObjectId('000'); + + expect(result).toStrictEqual([version]); + }); + + it('returns empty array when no matches found', async () => { + versionStore.versions = [version]; + + const result = versionStore.findVersionsByObjectId('999'); + + expect(result).toStrictEqual([]); + }); + }); +}); diff --git a/bcbox/frontend/tests/unit/utils/formatters.spec.ts b/bcbox/frontend/tests/unit/utils/formatters.spec.ts new file mode 100644 index 00000000..73d3cb87 --- /dev/null +++ b/bcbox/frontend/tests/unit/utils/formatters.spec.ts @@ -0,0 +1,14 @@ +import { toKebabCase } from '@/utils/formatters'; + +describe('formatters.ts toKebabCase', () => { + it('returns the expected UUID values', () => { + expect(toKebabCase('descriptive Variable name')).toEqual('descriptive-variable-name'); + expect(toKebabCase('INTERESTING FILE')).toEqual('interesting-file'); + expect(toKebabCase('abc')).toEqual('abc'); + }); + + it('returns blanks if blank provided', () => { + expect(toKebabCase('')).toEqual(''); + expect(toKebabCase(null)).toEqual(''); + }); +}); diff --git a/bcbox/frontend/tsconfig.config.json b/bcbox/frontend/tsconfig.config.json new file mode 100644 index 00000000..8a74e46a --- /dev/null +++ b/bcbox/frontend/tsconfig.config.json @@ -0,0 +1,12 @@ +{ + "extends": "@tsconfig/node18/tsconfig.json", + "include": [ + "vite.config.*", + "vitest.config.*", + "cypress.config.*", + "playwright.config.*" + ], + "compilerOptions": { + "composite": true + } +} diff --git a/bcbox/frontend/tsconfig.json b/bcbox/frontend/tsconfig.json new file mode 100644 index 00000000..d261d04d --- /dev/null +++ b/bcbox/frontend/tsconfig.json @@ -0,0 +1,125 @@ +{ + "compilerOptions": { + /* Visit https://aka.ms/tsconfig to read more about this file */ + /* Projects */ + // "incremental": true, /* Save .tsbuildinfo files to allow for incremental compilation of projects. */ + // "composite": true, /* Enable constraints that allow a TypeScript project to be used with project references. */ + // "tsBuildInfoFile": "./.tsbuildinfo", /* Specify the path to .tsbuildinfo incremental compilation file. */ + // "disableSourceOfProjectReferenceRedirect": true, /* Disable preferring source files instead of declaration files when referencing composite projects. */ + // "disableSolutionSearching": true, /* Opt a project out of multi-project reference checking when editing. */ + // "disableReferencedProjectLoad": true, /* Reduce the number of projects loaded automatically by TypeScript. */ + /* Language and Environment */ + "target": "ES2020" /* Set the JavaScript language version for emitted JavaScript and include compatible library declarations. */, + "lib": [ + "ES2020", + "DOM" + ], /* Specify a set of bundled library declaration files that describe the target runtime environment. */ + // "jsx": "preserve", /* Specify what JSX code is generated. */ + // "experimentalDecorators": true, /* Enable experimental support for TC39 stage 2 draft decorators. */ + // "emitDecoratorMetadata": true, /* Emit design-type metadata for decorated declarations in source files. */ + // "jsxFactory": "", /* Specify the JSX factory function used when targeting React JSX emit, e.g. 'React.createElement' or 'h'. */ + // "jsxFragmentFactory": "", /* Specify the JSX Fragment reference used for fragments when targeting React JSX emit e.g. 'React.Fragment' or 'Fragment'. */ + // "jsxImportSource": "", /* Specify module specifier used to import the JSX factory functions when using 'jsx: react-jsx*'. */ + // "reactNamespace": "", /* Specify the object invoked for 'createElement'. This only applies when targeting 'react' JSX emit. */ + // "noLib": true, /* Disable including any library files, including the default lib.d.ts. */ + // "useDefineForClassFields": true, /* Emit ECMAScript-standard-compliant class fields. */ + // "moduleDetection": "auto", /* Control what method is used to detect module-format JS files. */ + /* Modules */ + "module": "ES2020" /* Specify what module code is generated. */, + // "rootDir": "./", /* Specify the root folder within your source files. */ + "moduleResolution": "Bundler" /* Specify how TypeScript looks up a file from a given module specifier. */, + "baseUrl": "." /* Specify the base directory to resolve non-relative module names. */, + "paths": { + "@/*": [ + "./src/*" + ] + } /* Specify a set of entries that re-map imports to additional lookup locations. */, + // "rootDirs": [], /* Allow multiple folders to be treated as one when resolving modules. */ + // "typeRoots": [], /* Specify multiple folders that act like './node_modules/@types'. */ + "types": [ + "vite/client", + "vitest/globals", + "node" + ] /* Specify type package names to be included without being referenced in a source file. */, + // "allowUmdGlobalAccess": true, /* Allow accessing UMD globals from modules. */ + // "moduleSuffixes": [], /* List of file name suffixes to search when resolving a module. */ + "resolveJsonModule": true /* Enable importing .json files. */, + // "noResolve": true, /* Disallow 'import's, 'require's or ''s from expanding the number of files TypeScript should add to a project. */ + /* JavaScript Support */ + // "allowJs": true, /* Allow JavaScript files to be a part of your program. Use the 'checkJS' option to get errors from these files. */ + // "checkJs": true, /* Enable error reporting in type-checked JavaScript files. */ + // "maxNodeModuleJsDepth": 1, /* Specify the maximum folder depth used for checking JavaScript files from 'node_modules'. Only applicable with 'allowJs'. */ + /* Emit */ + // "declaration": true, /* Generate .d.ts files from TypeScript and JavaScript files in your project. */ + // "declarationMap": true, /* Create sourcemaps for d.ts files. */ + // "emitDeclarationOnly": true, /* Only output d.ts files and not JavaScript files. */ + "sourceMap": true, /* Create source map files for emitted JavaScript files. */ + // "outFile": "./", /* Specify a file that bundles all outputs into one JavaScript file. If 'declaration' is true, also designates a file that bundles all .d.ts output. */ + "outDir": "./dist" /* Specify an output folder for all emitted files. */, + // "removeComments": true, /* Disable emitting comments. */ + "noEmit": true, /* Disable emitting files from a compilation. */ + // "importHelpers": true, /* Allow importing helper functions from tslib once per project, instead of including them per-file. */ + // "importsNotUsedAsValues": "remove", /* Specify emit/checking behavior for imports that are only used for types. */ + // "downlevelIteration": true, /* Emit more compliant, but verbose and less performant JavaScript for iteration. */ + // "sourceRoot": "", /* Specify the root path for debuggers to find the reference source code. */ + // "mapRoot": "", /* Specify the location where debugger should locate map files instead of generated locations. */ + // "inlineSourceMap": true, /* Include sourcemap files inside the emitted JavaScript. */ + // "inlineSources": true, /* Include source code in the sourcemaps inside the emitted JavaScript. */ + // "emitBOM": true, /* Emit a UTF-8 Byte Order Mark (BOM) in the beginning of output files. */ + // "newLine": "crlf", /* Set the newline character for emitting files. */ + // "stripInternal": true, /* Disable emitting declarations that have '@internal' in their JSDoc comments. */ + // "noEmitHelpers": true, /* Disable generating custom helper functions like '__extends' in compiled output. */ + // "noEmitOnError": true, /* Disable emitting files if any type checking errors are reported. */ + // "preserveConstEnums": true, /* Disable erasing 'const enum' declarations in generated code. */ + // "declarationDir": "./", /* Specify the output directory for generated declaration files. */ + "preserveValueImports": false /* Preserve unused imported values in the JavaScript output that would otherwise be removed. */, + /* Interop Constraints */ + // "isolatedModules": true, /* Ensure that each file can be safely transpiled without relying on other imports. */ + // "allowSyntheticDefaultImports": true, /* Allow 'import x from y' when a module doesn't have a default export. */ + "esModuleInterop": true /* Emit additional JavaScript to ease support for importing CommonJS modules. This enables 'allowSyntheticDefaultImports' for type compatibility. */, + // "preserveSymlinks": true, /* Disable resolving symlinks to their realpath. This correlates to the same flag in node. */ + "forceConsistentCasingInFileNames": true /* Ensure that casing is correct in imports. */, + /* Type Checking */ + "strict": true /* Enable all strict type-checking options. */, + // "noImplicitAny": true, /* Enable error reporting for expressions and declarations with an implied 'any' type. */ + // "strictNullChecks": true, /* When type checking, take into account 'null' and 'undefined'. */ + // "strictFunctionTypes": true, /* When assigning functions, check to ensure parameters and the return values are subtype-compatible. */ + // "strictBindCallApply": true, /* Check that the arguments for 'bind', 'call', and 'apply' methods match the original function. */ + // "strictPropertyInitialization": true, /* Check for class properties that are declared but not set in the constructor. */ + // "noImplicitThis": true, /* Enable error reporting when 'this' is given the type 'any'. */ + // "useUnknownInCatchVariables": true, /* Default catch clause variables as 'unknown' instead of 'any'. */ + // "alwaysStrict": true, /* Ensure 'use strict' is always emitted. */ + // "noUnusedLocals": true, /* Enable error reporting when local variables aren't read. */ + // "noUnusedParameters": true, /* Raise an error when a function parameter isn't read. */ + // "exactOptionalPropertyTypes": true, /* Interpret optional property types as written, rather than adding 'undefined'. */ + // "noImplicitReturns": true, /* Enable error reporting for codepaths that do not explicitly return in a function. */ + // "noFallthroughCasesInSwitch": true, /* Enable error reporting for fallthrough cases in switch statements. */ + // "noUncheckedIndexedAccess": true, /* Add 'undefined' to a type when accessed using an index. */ + // "noImplicitOverride": true, /* Ensure overriding members in derived classes are marked with an override modifier. */ + // "noPropertyAccessFromIndexSignature": true, /* Enforces using indexed accessors for keys declared using an indexed type. */ + // "allowUnusedLabels": true, /* Disable error reporting for unused labels. */ + // "allowUnreachableCode": true, /* Disable error reporting for unreachable code. */ + /* Completeness */ + // "skipDefaultLibCheck": true, /* Skip type checking .d.ts files that are included with TypeScript. */ + "skipLibCheck": true, /* Skip type checking all .d.ts files. */ + "allowImportingTsExtensions": true, + "allowArbitraryExtensions": true, + "verbatimModuleSyntax": true + }, + "exclude": [ + "node_modules" + ], + "extends": "@vue/tsconfig/tsconfig.dom.json", + "include": [ + "src/**/*.ts", + "src/**/*.d.ts", + "src/**/*.tsx", + "src/**/*.vue", + "tests/**/*.ts" + ], + "references": [ + { + "path": "./tsconfig.config.json" + } + ] +} diff --git a/bcbox/frontend/vite.config.ts b/bcbox/frontend/vite.config.ts new file mode 100644 index 00000000..57566161 --- /dev/null +++ b/bcbox/frontend/vite.config.ts @@ -0,0 +1,46 @@ +/// +import { fileURLToPath, URL } from 'node:url'; +import { defineConfig } from 'vite'; +import { configDefaults } from 'vitest/config'; +import vue from '@vitejs/plugin-vue'; +import vueJsx from '@vitejs/plugin-vue-jsx'; + +const proxyObject = { + target: 'http://localhost:8080', + ws: true, + changeOrigin: true +}; + +// https://vitejs.dev/config/ +export default defineConfig({ + // base: './', + css: { + preprocessorOptions: { + scss: { + additionalData: "@import '@/assets/variables.scss';" // eslint-disable-line quotes + } + } + }, + plugins: [vue(), vueJsx()], + resolve: { + alias: { + '@': fileURLToPath(new URL('./src', import.meta.url)) + } + }, + server: { + proxy: { + '/api': proxyObject, + '/config': proxyObject + } + }, + test: { + globals: true, + environment: 'happy-dom', + include: ['**/*.{test,spec}.{js,mjs,cjs,ts,mts,cts,jsx,tsx,vue}'], + exclude: [...configDefaults.exclude, 'packages/template/*'], + coverage: { + provider: 'istanbul', // 'istanbul' or 'c8' + reporter: ['text', 'json', 'html', 'clover', 'lcov'] + } + } +}); diff --git a/bcbox/frontend/volar.config.js b/bcbox/frontend/volar.config.js new file mode 100644 index 00000000..7d9d494d --- /dev/null +++ b/bcbox/frontend/volar.config.js @@ -0,0 +1,14 @@ +const baseConfig = require('./.eslintrc.js'); + +module.exports = { + plugins: [ + require('volar-service-eslint').default(program => ({ + ...baseConfig, + ignorePatterns: ['**/*.vue.*'], // ignore virtual files: *.vue.ts, *.vue.html, *.vue.css + parserOptions: { + ...baseConfig.parserOptions, + programs: [program], // replace eslint typescript program + }, + })), + ], +}; diff --git a/comsapi/.codeclimate.yml b/comsapi/.codeclimate.yml new file mode 100644 index 00000000..38b87fd3 --- /dev/null +++ b/comsapi/.codeclimate.yml @@ -0,0 +1,53 @@ +version: "2" +exclude_patterns: + - config/ + - db/ + - dist/ + - features/ + - "**/node_modules/" + - script/ + - "**/spec/" + - "**/test/" + - "**/tests/" + - Tests/ + - "**/vendor/" + - "**/*_test.go" + - "**/*.d.ts" +plugins: + csslint: + enabled: true + editorconfig: + enabled: true + checks: + END_OF_LINE: + enabled: false + INDENTATION_SPACES: + enabled: false + INDENTATION_SPACES_AMOUNT: + enabled: false + TRAILINGSPACES: + enabled: false + eslint: + enabled: true + channel: "eslint-7" + config: + config: app/.eslintrc.js + fixme: + enabled: true + git-legal: + enabled: true + markdownlint: + enabled: true + checks: + MD002: + enabled: false + MD013: + enabled: false + MD029: + enabled: false + MD046: + enabled: false + nodesecurity: + enabled: true + sass-lint: + enabled: true diff --git a/comsapi/.dockerignore b/comsapi/.dockerignore new file mode 100644 index 00000000..d95ee2a1 --- /dev/null +++ b/comsapi/.dockerignore @@ -0,0 +1,42 @@ +# Editor directories and files +.DS_Store +.gradle +.nyc_output +.scannerwork +build +coverage +dist +files +**/e2e/videos +node_modules +# Ignore only top-level package-lock.json +/package-lock.json + +# Ignore Helm subcharts +charts/**/charts +Chart.lock + +# local env files +local.* +local-*.* +.env.local +.env.*.local + +# Log files +npm-debug.log* +yarn-debug.log* +yarn-error.log* + +# Editor directories and files +.idea +.vscode +*.iml +*.suo +*.ntvs* +*.njsproj +*.sln +*.sw? +*.mp4 + +# temp office files +~$* diff --git a/comsapi/.editorconfig b/comsapi/.editorconfig new file mode 100644 index 00000000..689f0b86 --- /dev/null +++ b/comsapi/.editorconfig @@ -0,0 +1,23 @@ +root = true + +[*] +charset = utf-8 +end_of_line = lf +insert_final_newline = true +trim_trailing_whitespace = true + +[*.html] +indent_style = space +indent_size = 2 + +[*.{css,js,json,jsx,scss,ts,tsx,vue}] +indent_style = space +indent_size = 2 + +[.{babelrc,eslintrc}] +indent_style = space +indent_size = 2 + +[Jenkinsfile*] +indent_style = space +indent_size = 2 diff --git a/comsapi/.gitattributes b/comsapi/.gitattributes new file mode 100644 index 00000000..fd57b60d --- /dev/null +++ b/comsapi/.gitattributes @@ -0,0 +1,8 @@ +# Autodetect text files and forces unix eols, so Windows does not break them +* text=auto eol=lf + +# Force images/fonts to be handled as binaries +*.jpg binary +*.jpeg binary +*.gif binary +*.png binary diff --git a/comsapi/.gitignore b/comsapi/.gitignore new file mode 100644 index 00000000..9aada915 --- /dev/null +++ b/comsapi/.gitignore @@ -0,0 +1,41 @@ +# Editor directories and files +.DS_Store +.gradle +.nyc_output +.scannerwork +build +coverage +dist +files +**/e2e/videos +node_modules +# Ignore only top-level package-lock.json +/package-lock.json + +# Ignore Helm subcharts +charts/**/charts +Chart.lock + +# local env files +local.* +local-*.* +.env.local +.env.*.local + +# Log files +npm-debug.log* +yarn-debug.log* +yarn-error.log* + +# Editor directories and files +.idea +*.iml +*.suo +*.ntvs* +*.njsproj +*.sln +*.sw? +*.mp4 + +# temp office files +~$* diff --git a/comsapi/.vscode/extensions.json b/comsapi/.vscode/extensions.json new file mode 100644 index 00000000..a14db9f6 --- /dev/null +++ b/comsapi/.vscode/extensions.json @@ -0,0 +1,11 @@ +{ + "recommendations": [ + "bierner.markdown-preview-github-styles", + "davidanson.vscode-markdownlint", + "dbaeumer.vscode-eslint", + "eamodio.gitlens", + "editorconfig.editorconfig", + "redhat.vscode-yaml", + "ryanluker.vscode-coverage-gutters", + ] +} diff --git a/comsapi/.vscode/launch.json b/comsapi/.vscode/launch.json new file mode 100644 index 00000000..c2569eac --- /dev/null +++ b/comsapi/.vscode/launch.json @@ -0,0 +1,17 @@ +{ + // Use IntelliSense to learn about possible attributes. + // Hover to view descriptions of existing attributes. + // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 + "version": "0.2.0", + "configurations": [ + { + "name": "Attach", + "port": 9229, + "request": "attach", + "skipFiles": [ + "/**" + ], + "type": "node" + } + ] +} diff --git a/comsapi/.vscode/settings.json b/comsapi/.vscode/settings.json new file mode 100644 index 00000000..1884b3a5 --- /dev/null +++ b/comsapi/.vscode/settings.json @@ -0,0 +1,23 @@ +{ + "[html]": { + "editor.defaultFormatter": "vscode.html-language-features" + }, + "[javascript]": { + "editor.defaultFormatter": "dbaeumer.vscode-eslint" + }, + "[json]": { + "editor.defaultFormatter": "vscode.json-language-features" + }, + "[jsonc]": { + "editor.defaultFormatter": "vscode.json-language-features" + }, + "[typescript]": { + "editor.defaultFormatter": "vscode.typescript-language-features" + }, + "coverage-gutters.showGutterCoverage": false, + "coverage-gutters.showLineCoverage": true, + "editor.defaultFormatter": "dbaeumer.vscode-eslint", + "editor.formatOnSave": true, + "eslint.format.enable": true, + "files.insertFinalNewline": true, +} diff --git a/comsapi/CODE-OF-CONDUCT.md b/comsapi/CODE-OF-CONDUCT.md new file mode 100644 index 00000000..0c1e1bbe --- /dev/null +++ b/comsapi/CODE-OF-CONDUCT.md @@ -0,0 +1,46 @@ +# Contributor Covenant Code of Conduct + +## Our Pledge + +In the interest of fostering an open and welcoming environment, we as contributors and maintainers pledge to making participation in our project and our community a harassment-free experience for everyone, regardless of age, body size, disability, ethnicity, gender identity and expression, level of experience, nationality, personal appearance, race, religion, or sexual identity and orientation. + +## Our Standards + +Examples of behavior that contributes to creating a positive environment include: + +* Using welcoming and inclusive language +* Being respectful of differing viewpoints and experiences +* Gracefully accepting constructive criticism +* Focusing on what is best for the community +* Showing empathy towards other community members + +Examples of unacceptable behavior by participants include: + +* The use of sexualized language or imagery and unwelcome sexual attention or advances +* Trolling, insulting/derogatory comments, and personal or political attacks +* Public or private harassment +* Publishing others' private information, such as a physical or electronic address, without explicit permission +* Other conduct which could reasonably be considered inappropriate in a professional setting + +## Our Responsibilities + +Project maintainers are responsible for clarifying the standards of acceptable behavior and are expected to take appropriate and fair corrective action in response to any instances of unacceptable behavior. + +Project maintainers have the right and responsibility to remove, edit, or reject comments, commits, code, wiki edits, issues, and other contributions that are not aligned to this Code of Conduct, or to ban temporarily or permanently any contributor for other behaviors that they deem inappropriate, threatening, offensive, or harmful. + +## Scope + +This Code of Conduct applies both within project spaces and in public spaces when an individual is representing the project or its community. Examples of representing a project or community include using an official project e-mail address, posting via an official social media account, or acting as an appointed representative at an online or offline event. Representation of a project may be further defined and clarified by project maintainers. + +## Enforcement + +Instances of abusive, harassing, or otherwise unacceptable behavior may be reported by contacting the project team at matthew.hall@gov.bc.ca. The project team will review and investigate all complaints, and will respond in a way that it deems appropriate to the circumstances. The project team is obligated to maintain confidentiality with regard to the reporter of an incident. Further details of specific enforcement policies may be posted separately. + +Project maintainers who do not follow or enforce the Code of Conduct in good faith may face temporary or permanent repercussions as determined by other members of the project's leadership. + +## Attribution + +This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4, available at [http://contributor-covenant.org/version/1/4][version] + +[homepage]: http://contributor-covenant.org +[version]: http://contributor-covenant.org/version/1/4/ diff --git a/comsapi/COMPLIANCE.yaml b/comsapi/COMPLIANCE.yaml new file mode 100644 index 00000000..6ed4873c --- /dev/null +++ b/comsapi/COMPLIANCE.yaml @@ -0,0 +1,11 @@ +name: compliance +description: | + This document is used to track a projects PIA and STRA + compliance. +spec: + - name: PIA + status: TBD + last-updated: "2022-01-26T00:00:00.000Z" + - name: STRA + status: TBD + last-updated: "2022-01-26T00:00:00.000Z" diff --git a/comsapi/CONTRIBUTING.md b/comsapi/CONTRIBUTING.md new file mode 100644 index 00000000..43d4d4a7 --- /dev/null +++ b/comsapi/CONTRIBUTING.md @@ -0,0 +1,9 @@ +# How to contribute + +Government employees, public and members of the private sector are encouraged to contribute to the repository by **forking and submitting a pull request**. + +(If you are new to GitHub, you might start with a [basic tutorial](https://help.github.com/articles/set-up-git) and check out a more detailed guide to [pull requests](https://help.github.com/articles/using-pull-requests/).) + +Pull requests will be evaluated by the repository guardians on a schedule and if deemed beneficial will be committed to the master. + +All contributors retain the original copyright to their stuff, but by contributing to this project, you grant a world-wide, royalty-free, perpetual, irrevocable, non-exclusive, transferable license to all users **under the terms of the [license](./LICENSE) under which this project is distributed**. diff --git a/comsapi/Dockerfile b/comsapi/Dockerfile new file mode 100644 index 00000000..25ff4efa --- /dev/null +++ b/comsapi/Dockerfile @@ -0,0 +1,20 @@ +FROM docker.io/node:18.18.2-alpine + +ARG APP_ROOT=/opt/app-root/src +ENV APP_PORT=8080 \ + NO_UPDATE_NOTIFIER=true +WORKDIR ${APP_ROOT} + +# NPM Permission Fix +RUN mkdir -p /.npm +RUN chown -R 1001:0 /.npm + +# Install Application +COPY . ${APP_ROOT} +RUN chown -R 1001:0 ${APP_ROOT} +USER 1001 +WORKDIR ${APP_ROOT}/app +RUN npm ci --omit=dev + +EXPOSE ${APP_PORT} +CMD ["npm", "run", "start"] diff --git a/comsapi/LICENSE b/comsapi/LICENSE new file mode 100644 index 00000000..62dc6edd --- /dev/null +++ b/comsapi/LICENSE @@ -0,0 +1,201 @@ + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright 2022 Province of British Columbia + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. diff --git a/comsapi/README.md b/comsapi/README.md new file mode 100644 index 00000000..7877a2bb --- /dev/null +++ b/comsapi/README.md @@ -0,0 +1,81 @@ + +# Common Object Management Service + +[![License](https://img.shields.io/badge/License-Apache%202.0-blue.svg)](LICENSE) [![Lifecycle:Stable](https://img.shields.io/badge/Lifecycle-Stable-97ca00)](https://github.com/bcgov/repomountie/blob/master/doc/lifecycle-badges.md) + +![Tests](https://github.com/bcgov/common-object-management-service/workflows/Tests/badge.svg) +[![Maintainability](https://api.codeclimate.com/v1/badges/91d2b0aebc348a1d5d0a/maintainability)](https://codeclimate.com/github/bcgov/common-object-management-service/maintainability) +[![Test Coverage](https://api.codeclimate.com/v1/badges/91d2b0aebc348a1d5d0a/test_coverage)](https://codeclimate.com/github/bcgov/common-object-management-service/test_coverage) + +[![version](https://img.shields.io/docker/v/bcgovimages/common-object-management-service.svg?sort=semver)](https://hub.docker.com/r/bcgovimages/common-object-management-service) +[![pulls](https://img.shields.io/docker/pulls/bcgovimages/common-object-management-service.svg)](https://hub.docker.com/r/bcgovimages/common-object-management-service) +[![size](https://img.shields.io/docker/image-size/bcgovimages/common-object-management-service.svg)](https://hub.docker.com/r/bcgovimages/common-object-management-service) + +A microservice for managing access control to S3 Objects + +To learn more about the **Common Services** available visit the [Common Services Showcase](https://bcgov.github.io/common-service-showcase/) page. + +## Directory Structure + +```txt +.github/ - PR, Issue templates and CI/CD +app/ - Application Root +├── config/ - configuration exposed as environment variables +├── src/ - Node.js web application +│ ├── components/ - Components Layer +│ ├── controllers/ - Controller Layer +│ ├── db/ - Database Layer +│ ├── docs/ - OpenAPI 3.0 Specification +│ ├── middleware/ - Middleware Layer +│ ├── routes/ - API Route Layer +│ └── services/ - Services Layer +│ └── validators/ - data validation schemas +└── tests/ - Node.js web application tests +charts/ - General Helm Charts +└── coms/ - COMS Helm Chart Repository + └── templates/ - COMS Helm Chart Template manifests +k6/ - sample load testing scripts +bcgovpubcode.yml - BCGov public code asset tracking +CODE-OF-CONDUCT.md - Code of Conduct +COMPLIANCE.yaml - BCGov PIA/STRA compliance status +CONTRIBUTING.md - Contributing Guidelines +Dockerfile - Dockerfile Image definition +LICENSE - License +SECURITY.md - Security Policy and Reporting +``` + +## Documentation + +* [Application Readme](app/README.md) +* [API Specification](app/README.md#openapi-specification) +* [Product Roadmap](https://github.com/bcgov/common-object-management-service/wiki/Product-Roadmap) +* [Product Wiki](https://github.com/bcgov/common-object-management-service/wiki) +* [Security Reporting](SECURITY.md) + +## Getting Help or Reporting an Issue + +To report bugs/issues/features requests, please file an [issue](https://github.com/bcgov/common-object-management-service/issues). + +## How to Contribute + +If you would like to contribute, please see our [contributing](CONTRIBUTING.md) guidelines. + +Please note that this project is released with a [Contributor Code of Conduct](CODE-OF-CONDUCT.md). By participating in this project you agree to abide by its terms. + +## License + +```txt +Copyright 2022 Province of British Columbia + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +``` diff --git a/comsapi/SECURITY.md b/comsapi/SECURITY.md new file mode 100644 index 00000000..7d7d1721 --- /dev/null +++ b/comsapi/SECURITY.md @@ -0,0 +1,52 @@ +# Security Policies and Procedures + +This document outlines security procedures and general policies for the Common +Object Management Service project. + +- [Supported Versions](#supported-versions) +- [Reporting a Bug](#reporting-a-bug) +- [Disclosure Policy](#disclosure-policy) +- [Comments on this Policy](#comments-on-this-policy) + +## Supported Versions + +At this time, only the latest version of Common Object Management Service is supported. + +| Version | Supported | +| ------- | ------------------ | +| 0.7.0 | :white_check_mark: | +| < 0.7.x | :x: | + +## Reporting a Bug + +The `CSS` team and community take all security bugs in `COMS` seriously. +Thank you for improving the security of `COMS`. We appreciate your efforts and +responsible disclosure and will make every effort to acknowledge your +contributions. + +Report security bugs by sending an email to . + +The `CSS` team will acknowledge your email within 48 hours, and will send a +more detailed response within 48 hours indicating the next steps in handling +your report. After the initial reply to your report, the security team will +endeavor to keep you informed of the progress towards a fix and full +announcement, and may ask for additional information or guidance. + +Report security bugs in third-party modules to the person or team maintaining +the module. + +## Disclosure Policy + +When the security team receives a security bug report, they will assign it to a +primary handler. This person will coordinate the fix and release process, +involving the following steps: + +- Confirm the problem and determine the affected versions. +- Audit code to find any potential similar problems. +- Prepare fixes for all releases still under maintenance. These fixes will be + released as fast as possible. + +## Comments on this Policy + +If you have suggestions on how this process could be improved please submit a +pull request. diff --git a/comsapi/_config.yaml b/comsapi/_config.yaml new file mode 100644 index 00000000..277f1f2c --- /dev/null +++ b/comsapi/_config.yaml @@ -0,0 +1 @@ +theme: jekyll-theme-cayman diff --git a/comsapi/app/.editorconfig b/comsapi/app/.editorconfig new file mode 100644 index 00000000..689f0b86 --- /dev/null +++ b/comsapi/app/.editorconfig @@ -0,0 +1,23 @@ +root = true + +[*] +charset = utf-8 +end_of_line = lf +insert_final_newline = true +trim_trailing_whitespace = true + +[*.html] +indent_style = space +indent_size = 2 + +[*.{css,js,json,jsx,scss,ts,tsx,vue}] +indent_style = space +indent_size = 2 + +[.{babelrc,eslintrc}] +indent_style = space +indent_size = 2 + +[Jenkinsfile*] +indent_style = space +indent_size = 2 diff --git a/comsapi/app/.eslintignore b/comsapi/app/.eslintignore new file mode 100644 index 00000000..eb531145 --- /dev/null +++ b/comsapi/app/.eslintignore @@ -0,0 +1,4 @@ +dist +frontend +node_modules +public/js diff --git a/comsapi/app/.eslintrc.js b/comsapi/app/.eslintrc.js new file mode 100644 index 00000000..a6357904 --- /dev/null +++ b/comsapi/app/.eslintrc.js @@ -0,0 +1,41 @@ +module.exports = { + root: true, + env: { + commonjs: true, + es2020: true, + jest: true, + node: true + }, + extends: ['eslint:recommended'], + globals: { + Atomics: 'readonly', + SharedArrayBuffer: 'readonly', + _: false + }, + parserOptions: { + ecmaVersion: 11 + }, + rules: { + 'eol-last': ['error', 'always'], + indent: ['error', 2, { + 'SwitchCase': 1 + }], + 'linebreak-style': ['error', 'unix'], + 'max-len': ['warn', { code: 120, comments: 120, ignoreUrls: true }], + 'no-console': process.env.NODE_ENV === 'production' ? 'error' : 'warn', + 'no-debugger': process.env.NODE_ENV === 'production' ? 'error' : 'warn', + quotes: ['error', 'single'], + semi: ['error', 'always'] + }, + overrides: [ + { + files: [ + '**/__tests__/*.{j,t}s?(x)', + '**/tests/unit/**/*.spec.{j,t}s?(x)' + ], + env: { + jest: true + } + } + ] +}; diff --git a/comsapi/app/.prettierrc b/comsapi/app/.prettierrc new file mode 100644 index 00000000..937375d2 --- /dev/null +++ b/comsapi/app/.prettierrc @@ -0,0 +1,4 @@ +{ + "semi": true, + "singleQuote": true +} diff --git a/comsapi/app/README.md b/comsapi/app/README.md new file mode 100644 index 00000000..906d1e16 --- /dev/null +++ b/comsapi/app/README.md @@ -0,0 +1,312 @@ +# Common Object Management Service + +[![License](https://img.shields.io/badge/License-Apache%202.0-blue.svg)](LICENSE) [![Lifecycle:Stable](https://img.shields.io/badge/Lifecycle-Stable-97ca00)](https://github.com/bcgov/repomountie/blob/master/doc/lifecycle-badges.md) + +![Tests](https://github.com/bcgov/common-object-management-service/workflows/Tests/badge.svg) +[![Maintainability](https://api.codeclimate.com/v1/badges/91d2b0aebc348a1d5d0a/maintainability)](https://codeclimate.com/github/bcgov/common-object-management-service/maintainability) +[![Test Coverage](https://api.codeclimate.com/v1/badges/91d2b0aebc348a1d5d0a/test_coverage)](https://codeclimate.com/github/bcgov/common-object-management-service/test_coverage) + +[![version](https://img.shields.io/docker/v/bcgovimages/common-object-management-service.svg?sort=semver)](https://hub.docker.com/r/bcgovimages/common-object-management-service) +[![pulls](https://img.shields.io/docker/pulls/bcgovimages/common-object-management-service.svg)](https://hub.docker.com/r/bcgovimages/common-object-management-service) +[![size](https://img.shields.io/docker/image-size/bcgovimages/common-object-management-service.svg)](https://hub.docker.com/r/bcgovimages/common-object-management-service) + +A microservice for managing access control to S3 Objects + +To learn more about the **Common Services** available visit the [Common Services Showcase](https://bcgov.github.io/common-service-showcase/) page. + +## Table of Contents + +- [OpenAPI Specification](#openapi-specification) +- [Environment Variables](#environment-variables) + - [Basic Auth Variables](#basic-auth-variables) + - [Database Variables](#database-variables) + - [Keycloak Variables](#keycloak-variables) + - [Object Storage Variables](#object-storage-variables) + - [Server Variables](#server-variables) +- [Quick Start](#quick-start) + - [Docker](#docker) + - [Local Machine](#local-machine) +- [License](#license) + +## OpenAPI Specification + +This API is defined and described in OpenAPI 3.0 specification. + +When the API is running, you should be able to view the specification through ReDoc at (assuming you are running this microservice locally). The specification source file can be found [here](src/docs/v1.api-spec.yaml) + +## Environment Variables + +COMS supports a large array of environment variables to configure how it will behave. Depending on which sections below are enabled, you will have certain subsets of functionality available. Visit our wiki documentation on [Authentication modes](https://github.com/bcgov/common-object-management-service/wiki/Deployment-Guide#authentication-modes) for more details on the impacts of enabling each section. + +### Basic Auth Variables + +The following variables enable and enforce the use of Basic Authentication for requests to COMS + +| Config Var | Env Var | Default | Notes | +| --- | --- | --- | --- | +| `enabled` | `BASICAUTH_ENABLED` | | Whether to run COMS in Basic Auth mode | +| `username` | `BASICAUTH_USERNAME` | | An arbitrary Username provided in a Basic Auth header of requests from your COMS client application | +| `password` | `BASICAUTH_PASSWORD` | | An arbitrary Password provided in a Basic Auth header of requests from your COMS client application | + +### Database Variables + +The following variables configure the use of a backend database to support user-based access control, tagging and other advanced features + +| Config Var | Env Var | Default | Notes | +| --- | --- | --- | --- | +| `database` | `DB_DATABASE` | coms | COMS database name | +| `host` | `DB_HOST` | localhost | Database conection hostname | +| `username` | `DB_USERNAME` | app | Database account username | +| `password` | `DB_PASSWORD` | | Database account password | +| `port` | `DB_PORT` | 5432 | Database connection port | +| `poolMin` | `DB_POOL_MIN` | 2 | avalable connections | +| `poolMax` | `DB_POOL_MAX` | 10 | available connections | + +### Keycloak Variables + +The following variables enable and enforce the use of OIDC Bearer Authentication for requests to COMS + +| Config Var | Env Var | Default | Notes | +| --- | --- | --- | --- | +| `enabled` | `KC_ENABLED` | | Whether to run COMS in OIDC mode, required for user-based access controls and integration with OIDC users | +| `clientId` | `KC_CLIENTID` | | Keycloak service client ID for COMS | +| `clientSecret` | `KC_CLIENTSECRET` | | Keycloak service client secret | +| `identityKey` | `KC_IDENTITYKEY` | | Specify using alternative JWT claims for user identification instead of the standard jwt.sub. Multiple claim attributes may be specified via a comma-separated list. COMS will attempt to search for the custom claim ordered based on how it is specified in this variable before falling back to jwt.sub if none are found. | +| `publicKey` | `KC_PUBLICKEY` | | If specified, verify all incoming JWT signatures off of the provided public key | +| `realm` | `KC_REALM` | | Keycloak realm ID for COMS | +| `serverUrl` | `KC_SERVERURL` | | Keycloak server url for COMS authentication | + +### Object Storage Variables + +The following variables enable and enforce the use of OIDC Bearer Authentication for requests to COMS + +| Config Var | Env Var | Default | Notes | +| --- | --- | --- | --- | +| `enabled` | `OBJECTSTORAGE_ENABLED` | | Whether to run COMS with a default bucket | +| `accessKeyId` | `OBJECTSTORAGE_ACCESSKEYID` | | The Access Key for your S3 compatible object storage account | +| `bucket` | `OBJECTSTORAGE_BUCKET` | | The object storage bucket name | +| `endpoint` | `OBJECTSTORAGE_ENDPOINT` | | Object store URL. eg: `https://nrs.objectstore.gov.bc.ca` | +| `key` | `OBJECTSTORAGE_KEY` | | The base path for storage location | +| `secretAccessKey` | `OBJECTSTORAGE_SECRETACCESSKEY` | | The Secret Access Key for your S3 compatible object storage account | + +### Server Variables + +The following variables alter the general Express application behavior. For most situations, the defaults should be sufficient. + +| Config Var | Env Var | Default | Notes | +| --- | --- | --- | --- | +| `bodyLimit` | `SERVER_BODYLIMIT` | 30mb | Maximum body size accepted for parsing to JSON body | +| `defaultTempExpiresIn` | `SERVER_TEMP_EXPIRESIN` | 300 | The expiry time for pre-signed S3 URLs to objects in seconds | +| `logFile` | `SERVER_LOGFILE` | | Writes logs to the following file only if defined | +| `logLevel` | `SERVER_LOGLEVEL` | http | The logging level of COMS | +| `passphrase` | `SERVER_PASSPHRASE` | | A key to encrypt/decrypt bucket secretAccessKey's saved to the database | +| `port` | `SERVER_PORT` | 3000 | The port that COMS application will bind to | +| `privacyMask` | `SERVER_PRIVACY_MASK` | | Strict content privacy controls | + +## Quick Start + +The following sections provide you a quick way to get COMS set up and running either through Docker or directly as a node application. + +### Docker + +This section assumes you have a recent version of Docker available to work with on your environment. Make sure to have an understanding of what environment variables are passed into the application before proceeding. + +Note: change the `latest` tag to specific version if needed. Avoid using the latest tag in Production to ensure consistency with your existing infrastructure. + +Get COMS image: + +``` sh +docker pull docker.io/bcgovimages/common-object-management-service:latest +``` + +Run COMS in **Unauthenticated mode** (replace environment values as necessary) + +``` sh +docker run -it --rm -p 3000:3000 \ + -e OBJECTSTORAGE_ENABLED=true \ + -e OBJECTSTORAGE_ACCESSKEYID= \ + -e OBJECTSTORAGE_BUCKET= \ + -e OBJECTSTORAGE_ENDPOINT= \ + -e OBJECTSTORAGE_KEY= \ + -e OBJECTSTORAGE_SECRETACCESSKEY= \ + docker.io/bcgovimages/common-object-management-service:latest +``` + +Run COMS in **Basic Auth mode** (replace environment values as necessary) + +``` sh +docker run -it --rm -p 3000:3000 \ + -e OBJECTSTORAGE_ENABLED=true \ + -e OBJECTSTORAGE_ACCESSKEYID= \ + -e OBJECTSTORAGE_BUCKET= \ + -e OBJECTSTORAGE_ENDPOINT= \ + -e OBJECTSTORAGE_KEY= \ + -e OBJECTSTORAGE_SECRETACCESSKEY= \ + -e BASICAUTH_ENABLED=true \ + -e BASICAUTH_USERNAME= \ + -e BASICAUTH_PASSWORD= \ + docker.io/bcgovimages/common-object-management-service:latest +``` + +--- + +Before running the application, you must make sure that your database is up to date with the latest schema migration. Run the following first before starting up the COMS app as a maintenance task: + +``` sh +docker run -it --rm --entrypoint '/bin/sh' -c 'npm run migrate' \ + docker.io/bcgovimages/common-object-management-service:latest +``` + +Run COMS in **OIDC Auth Mode** (replace environment values as necessary) + +``` sh +docker run -it --rm -p 3000:3000 \ + -e OBJECTSTORAGE_ENABLED=true \ + -e OBJECTSTORAGE_ACCESSKEYID= \ + -e OBJECTSTORAGE_BUCKET= \ + -e OBJECTSTORAGE_ENDPOINT= \ + -e OBJECTSTORAGE_KEY= \ + -e OBJECTSTORAGE_SECRETACCESSKEY= \ + -e KC_ENABLED=true \ + -e KC_CLIENTID= \ + -e KC_CLIENTSECRET= \ + -e KC_PUBLICKEY= \ + -e KC_REALM= \ + -e KC_SERVERURL= \ + -e DB_PASSWORD= \ + -e DB_PORT= \ + docker.io/bcgovimages/common-object-management-service:latest +``` + +Run COMS in **Full Auth Mode** (replace environment values as necessary) + +``` sh +docker run -it --rm -p 3000:3000 \ + -e OBJECTSTORAGE_ENABLED=true \ + -e OBJECTSTORAGE_ACCESSKEYID= \ + -e OBJECTSTORAGE_BUCKET= \ + -e OBJECTSTORAGE_ENDPOINT= \ + -e OBJECTSTORAGE_KEY= \ + -e OBJECTSTORAGE_SECRETACCESSKEY= \ + -e BASICAUTH_ENABLED=true \ + -e BASICAUTH_USERNAME= \ + -e BASICAUTH_PASSWORD= \ + -e KC_ENABLED=true \ + -e KC_CLIENTID= \ + -e KC_CLIENTSECRET= \ + -e KC_PUBLICKEY= \ + -e KC_REALM= \ + -e KC_SERVERURL= \ + -e DB_PASSWORD= \ + -e DB_PORT= \ + docker.io/bcgovimages/common-object-management-service:latest +``` + +### Local Machine + +This section assumes you have a recent version of Node.js (12.x or higher) installed as well as Postgres (12.x or higher). Make sure to have an understanding of what environment variables are passed into the application before proceeding. + +#### Configuration + +Configuration management is done using the [config](https://www.npmjs.com/package/config) library. There are two ways to configure: + +1. Configure via `local.json` file. Look at [custom-environment-variables.json](/app/config/custom-environment-variables.json) and ensure you have the environment variables locally set. Create a `local.json` file in the config folder. This file should never be added to source control. Consider creating a `local-test.json` file in the config folder if you want to use different configurations while running unit tests. +2. Configure via environment variables. Look at [custom-environment-variables.json](/app/config/custom-environment-variables.json) and use the explicit environment variables in your environment as mentioned [above](#environment-variables) to configure your application behavior. + +For more information on how the config library loads and searches for environment variables, take a look [here](https://github.com/lorenwest/node-config/wiki/Configuration-Files). + +At a minimum (when running COMS in 'Unauthenticated mode'), you are required to have configuration values for your Object Storage. +To run COMS in Full Auth mode you will want your `local.json` to have the following values defined, with your own values as needed: + +``` json +{ + "basicAuth": { + "enabled": true, + "username": "", + "password": "" + }, + "db": { + "username": "", + "password": "", + "port": "" + }, + "keycloak": { + "enabled": true, + "clientId": "", + "clientSecret": "", + "realm": "", + "serverUrl": "" + }, + "objectStorage": { + "enabled": true, + "secretAccessKey": "", + "key": "", + "accessKeyId": "", + "bucket": "", + "endpoint": "" + }, + "server": { + "port": "" + } +} +``` + +#### Database Setup + +Before starting up the COMS app, run the following command to ensure your database is up to date with the latest database schema migration: + +``` sh +npm run migrate +``` + +#### Common Commands + +Install node dependencies with `npm ci`. You may use `npm install` if you are updating or changing the dependencies instead. Once your application is configured, make sure to run a database migration before starting up the COMS application. + +Run the server with hot-reloading for development + +``` sh +npm run serve +``` + +Run the server + +``` sh +npm run start +``` + +Migrate Database + +``` sh +npm run migrate +``` + +Lint the codebase + +``` sh +npm run lint +``` + +Run your tests + +``` sh +npm run test +``` + +## License + +```txt +Copyright 2022 Province of British Columbia + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +``` diff --git a/comsapi/app/app.js b/comsapi/app/app.js new file mode 100644 index 00000000..46d5b6fb --- /dev/null +++ b/comsapi/app/app.js @@ -0,0 +1,247 @@ +const Problem = require('api-problem'); +const compression = require('compression'); +const config = require('config'); +const cors = require('cors'); +const express = require('express'); + +const { AuthMode, DEFAULTCORS } = require('./src/components/constants'); +const log = require('./src/components/log')(module.filename); +const httpLogger = require('./src/components/log').httpLogger; +const QueueManager = require('./src/components/queueManager'); +const { getAppAuthMode, getGitRevision } = require('./src/components/utils'); +const DataConnection = require('./src/db/dataConnection'); +const v1Router = require('./src/routes/v1'); +const { readUnique } = require('./src/services/bucket'); + +const dataConnection = new DataConnection(); +const queueManager = new QueueManager(); + +const apiRouter = express.Router(); +const state = { + authMode: getAppAuthMode(), + connections: {}, + gitRev: getGitRevision(), + ready: false, + shutdown: false +}; + +let probeId; +let queueId; + +const app = express(); +app.use(compression()); +app.use(cors(DEFAULTCORS)); +app.use(express.urlencoded({ extended: true })); + +// Skip if running tests +if (process.env.NODE_ENV !== 'test') { + // Initialize connections and exit if unsuccessful + initializeConnections(); + app.use(httpLogger); +} + +// Application authentication modes +switch (state.authMode) { + case AuthMode.NOAUTH: + log.info('Running COMS in public no-auth mode'); + break; + case AuthMode.BASICAUTH: + log.info('Running COMS in basic auth mode'); + break; + case AuthMode.OIDCAUTH: + log.info('Running COMS in oidc auth mode'); + break; + case AuthMode.FULLAUTH: + log.info('Running COMS in full (basic + oidc) auth mode'); + break; +} +if (state.authMode === AuthMode.OIDCAUTH || state.authMode === AuthMode.FULLAUTH) { + if (!config.has('keycloak.publicKey')) { + log.error('OIDC environment variable KC_PUBLICKEY or keycloak.publicKey must be defined'); + process.exitCode = 1; + shutdown(); + } +} + +// Application privacy Mode mode +if (config.has('server.privacyMask')) { + log.info('Running COMS with strict content privacy masking'); +} else { + log.info('Running COMS with permissive content privacy masking'); +} + +// Block requests until service is ready +app.use((_req, _res, next) => { + if (state.shutdown) { + throw new Problem(503, { detail: 'Server is shutting down' }); + } else if (!state.ready) { + throw new Problem(503, { detail: 'Server is not ready' }); + } else { + next(); + } +}); + +// Base API Directory +apiRouter.get('/', (_req, res) => { + if (state.shutdown) { + throw new Error('Server shutting down'); + } else { + res.status(200).json({ + app: { + authMode: state.authMode, + gitRev: state.gitRev, + name: process.env.npm_package_name, + nodeVersion: process.version, + privacyMask: config.has('server.privacyMask'), + version: process.env.npm_package_version + }, + endpoints: ['/api/v1'], + versions: [1] + }); + } +}); + +// v1 Router +apiRouter.use('/v1', v1Router); + +// Root level Router +app.use(/(\/api)?/, apiRouter); + +// Handle 404 +app.use((req, _res) => { // eslint-disable-line no-unused-vars + throw new Problem(404, { instance: req.originalUrl }); +}); + +// Handle Problem Responses +app.use((err, req, res, _next) => { // eslint-disable-line no-unused-vars + if (err instanceof Problem) { + err.send(res); + } else { + if (err.stack) log.error(err); // Only log unexpected errors + new Problem(500, { detail: err.message ?? err, instance: req.originalUrl }).send(res); + } +}); + +// Ensure unhandled errors gracefully shut down the application +process.on('unhandledRejection', err => { + log.error(`Unhandled Rejection: ${err.message ?? err}`, { function: 'onUnhandledRejection' }); + fatalErrorHandler(); +}); +process.on('uncaughtException', err => { + log.error(`Unhandled Exception: ${err.message ?? err}`, { function: 'onUncaughtException' }); + fatalErrorHandler(); +}); + +// Graceful shutdown support +['SIGHUP', 'SIGINT', 'SIGTERM', 'SIGQUIT', 'SIGUSR1', 'SIGUSR2'] + .forEach(signal => process.on(signal, shutdown)); +process.on('exit', code => { + log.info(`Exiting with code ${code}`, { function: 'onExit' }); +}); + +/** + * @function cleanup + * Cleans up connections in this application. + */ +function cleanup() { + log.info('Cleaning up', { function: 'cleanup' }); + // Set 10 seconds max deadline before hard exiting + setTimeout(process.exit, 10000).unref(); // Prevents the timeout from registering on event loop + + clearInterval(probeId); + clearInterval(queueId); + queueManager.close(() => setTimeout(() => { + dataConnection.close(process.exit); + }, 4050)); +} + +/** + * @function checkConnections + * Checks Database connectivity + * This may force the application to exit if a connection fails + */ +function checkConnections() { + const wasReady = state.ready; + if (!state.shutdown) { + dataConnection.checkConnection().then(results => { + state.connections.data = results; + state.ready = Object.values(state.connections).every(x => x); + if (!wasReady && state.ready) log.info('Application ready to accept traffic', { function: 'checkConnections' }); + if (wasReady && !state.ready) log.warn('Application not ready to accept traffic', { function: 'checkConnections' }); + log.silly('App state', { function: 'checkConnections', state: state }); + if (!state.ready) notReadyHandler(); + }); + } +} + +/** + * @function fatalErrorHandler + * Forces the application to shutdown + */ +function fatalErrorHandler() { + process.exitCode = 1; + shutdown(); +} + +/** + * @function initializeConnections + * Initializes the database connections + * This may force the application to exit if it fails + */ +function initializeConnections() { + // Initialize connections and exit if unsuccessful + dataConnection.checkAll() + .then(results => { + state.connections.data = results; + + if (state.connections.data) { + log.info('DataConnection Reachable', { function: 'initializeConnections' }); + } + if (config.has('objectStorage.enabled')) { + readUnique(config.get('objectStorage')).then(() => { + log.error('Default bucket cannot also exist in database', { function: 'initializeConnections' }); + fatalErrorHandler(); + }).catch(() => { }); + } + }) + .catch(error => { + log.error(`Initialization failed: Database OK = ${state.connections.data}`, { function: 'initializeConnections' }); + log.error('Connection initialization failure', error.message, { function: 'initializeConnections' }); + if (!state.ready) notReadyHandler(); + }) + .finally(() => { + state.ready = Object.values(state.connections).every(x => x); + if (state.ready) log.info('Application ready to accept traffic', { function: 'initializeConnections' }); + + // Start periodic 10 second connection probes + probeId = setInterval(checkConnections, 10000); + queueId = setInterval(() => { + if (state.ready) queueManager.checkQueue(); + }, 10000); + }); +} + +/** + * @function notReadyHandler + * Forces an application shutdown if `server.hardReset` is defined. + * Otherwise will flush and attempt to reset the connection pool. + */ +function notReadyHandler() { + if (config.has('server.hardReset')) fatalErrorHandler(); + else dataConnection.resetConnection(); +} + +/** + * @function shutdown + * Begins the shutdown procedure for this application + */ +function shutdown() { + log.info('Shutting down', { function: 'shutdown' }); + if (!state.shutdown) { + state.shutdown = true; + log.info('Application no longer accepting traffic', { function: 'shutdown' }); + cleanup(); + } +} + +module.exports = app; diff --git a/comsapi/app/bin/www b/comsapi/app/bin/www new file mode 100644 index 00000000..82e22007 --- /dev/null +++ b/comsapi/app/bin/www @@ -0,0 +1,71 @@ +#!/usr/bin/env node + +/** Module dependencies */ +const config = require('config'); +const http = require('http'); + +const app = require('../app'); +const log = require('../src/components/log')(module.filename); + +/** Normalize a port into a number, string, or false. */ +const normalizePort = val => { + const port = parseInt(val, 10); + + if (isNaN(port)) { + // named pipe + return val; + } + + if (port >= 0) { + // port number + return port; + } + + return false; +}; + +/** Event listener for HTTP server "error" event. */ +const onError = error => { + if (error.syscall !== 'listen') { + throw error; + } + + const bind = typeof port === 'string' ? + 'Pipe ' + port : + 'Port ' + port; + + // handle specific listen errors with friendly messages + switch (error.code) { + case 'EACCES': + log.error(bind + ' requires elevated privileges'); + process.exit(1); + break; + case 'EADDRINUSE': + log.error(bind + ' is already in use'); + process.exit(1); + break; + default: + throw error; + } +}; + +/** Event listener for HTTP server "listening" event. */ +const onListening = () => { + const addr = server.address(); + const bind = typeof addr === 'string' ? + 'pipe ' + addr : + 'port ' + addr.port; + log.info('Listening on ' + bind); +}; + +/** Get port from environment and store in Express. */ +const port = normalizePort(config.get('server.port')); +app.set('port', port); + +/** Create HTTP server. */ +const server = http.createServer(app); + +/** Listen on provided port, on all network interfaces. */ +server.listen(port); +server.on('error', onError); +server.on('listening', onListening); diff --git a/comsapi/app/config/custom-environment-variables.json b/comsapi/app/config/custom-environment-variables.json new file mode 100644 index 00000000..961352fc --- /dev/null +++ b/comsapi/app/config/custom-environment-variables.json @@ -0,0 +1,43 @@ +{ + "basicAuth": { + "enabled": "BASICAUTH_ENABLED", + "password": "BASICAUTH_PASSWORD", + "username": "BASICAUTH_USERNAME" + }, + "db": { + "database": "DB_DATABASE", + "host": "DB_HOST", + "password": "DB_PASSWORD", + "poolMin": "DB_POOL_MIN", + "poolMax": "DB_POOL_MAX", + "port": "DB_PORT", + "username": "DB_USERNAME" + }, + "keycloak": { + "enabled": "KC_ENABLED", + "clientId": "KC_CLIENTID", + "clientSecret": "KC_CLIENTSECRET", + "identityKey": "KC_IDENTITYKEY", + "publicKey": "KC_PUBLICKEY", + "realm": "KC_REALM", + "serverUrl": "KC_SERVERURL" + }, + "objectStorage": { + "accessKeyId": "OBJECTSTORAGE_ACCESSKEYID", + "bucket": "OBJECTSTORAGE_BUCKET", + "enabled": "OBJECTSTORAGE_ENABLED", + "endpoint": "OBJECTSTORAGE_ENDPOINT", + "key": "OBJECTSTORAGE_KEY", + "secretAccessKey": "OBJECTSTORAGE_SECRETACCESSKEY" + }, + "server": { + "defaultTempExpiresIn": "SERVER_TEMP_EXPIRESIN", + "hardReset": "SERVER_HARDRESET", + "logFile": "SERVER_LOGFILE", + "logLevel": "SERVER_LOGLEVEL", + "maxRetries": "SERVER_MAXRETRIES", + "passphrase": "SERVER_PASSPHRASE", + "port": "SERVER_PORT", + "privacyMask": "SERVER_PRIVACY_MASK" + } +} diff --git a/comsapi/app/config/default.json b/comsapi/app/config/default.json new file mode 100644 index 00000000..c8926004 --- /dev/null +++ b/comsapi/app/config/default.json @@ -0,0 +1,26 @@ +{ + "db": { + "database": "coms", + "host": "host.docker.internal", + "port": "8001", + "poolMin": "2", + "poolMax": "10", + "username": "postgres", + "password": "456789" + }, + "server": { + "defaultTempExpiresIn": "300", + "logLevel": "http", + "maxRetries": "3", + "port": "4050" + }, + "keycloak": { + "enabled": "true", + "clientId": "coms-api", + "clientSecret": "xxxxx", + "identityKey": "xxxxx", + "realm": "forms-flow-ai", + "serverUrl": "https://epd-keycloak-dev.apps.silver.devops.gov.bc.ca/auth", + "publicKey": "xxxxx" + } +} diff --git a/comsapi/app/config/production.json b/comsapi/app/config/production.json new file mode 100644 index 00000000..0967ef42 --- /dev/null +++ b/comsapi/app/config/production.json @@ -0,0 +1 @@ +{} diff --git a/comsapi/app/config/test.json b/comsapi/app/config/test.json new file mode 100644 index 00000000..3fcda06e --- /dev/null +++ b/comsapi/app/config/test.json @@ -0,0 +1,5 @@ +{ + "server": { + "logLevel": "silent" + } +} diff --git a/comsapi/app/jest.config.js b/comsapi/app/jest.config.js new file mode 100644 index 00000000..7b1a1f91 --- /dev/null +++ b/comsapi/app/jest.config.js @@ -0,0 +1,16 @@ +module.exports = { + clearMocks: true, + collectCoverage: true, + collectCoverageFrom: ['src/**/*.js', '!src/db/migrations/*.js', '!src/db/seeds/*.js'], + moduleFileExtensions: ['js', 'json'], + moduleNameMapper: { + '^@/(.*)$': '/src/$1' + }, + testMatch: [ + '**/tests/unit/**/*.spec.(js|jsx|ts|tsx)|**/__tests__/*.(js|jsx|ts|tsx)' + ], + testPathIgnorePatterns: [], + testEnvironmentOptions: { + url: 'http://localhost/' + } +}; diff --git a/comsapi/app/knexfile.js b/comsapi/app/knexfile.js new file mode 100644 index 00000000..221267c3 --- /dev/null +++ b/comsapi/app/knexfile.js @@ -0,0 +1,65 @@ +const config = require('config'); +const { format } = require('date-fns'); + +const log = require('./src/components/log')(module.filename); + +/** + * Knex configuration + * Set database configuration for application and knex configuration for migrations + * and seeding. Since configuration values can change via env. vars or property + * files, we only need one runtime 'environment' for knex. + * + * Note: it appears that the knexfile must be in the root for the config values + * to be read correctly when running the 'npm run migrate:*' scripts. + * @module knexfile + * @see module:knex + * @see module:config + */ + +const types = require('pg').types; +// To handle JSON Schema validation, we treat dates and timestamps outside of the database as strings. +// Prevent the automatic conversion of dates/timestamps into Objects, leave as strings. +types.setTypeParser(1082, (value) => { + // dates must be in the date only part of 2020-05-16T13:18:27.160Z + return format(value, 'yyyy-MM-dd'); +}); +// timestamps... +types.setTypeParser(1114, (value) => new Date(value).toISOString()); +// timestamps with zone +types.setTypeParser(1184, (value) => new Date(value).toISOString()); + +const logWrapper = (level, msg) => log.log(level, msg); + +module.exports = { + client: 'pg', + connection: { + host: config.get('db.host'), + user: config.get('db.username'), + password: config.get('db.password'), + database: config.get('db.database'), + port: config.get('db.port') + }, + debug: ['silly', 'debug'].includes(config.get('server.logLevel')), + log: { + debug: (msg) => logWrapper('debug', msg), + deprecate: (msg) => logWrapper('warn', msg), + error: (msg) => logWrapper('error', msg), + info: (msg) => logWrapper('info', msg), + silly: (msg) => logWrapper('silly', msg), + verbose: (msg) => logWrapper('verbose', msg), + warn: (msg) => logWrapper('warn', msg), + }, + migrations: { + directory: __dirname + '/src/db/migrations' + }, + pool: { + min: parseInt(config.get('db.poolMin')), + max: parseInt(config.get('db.poolMax')) + // This shouldn't be here: https://github.com/knex/knex/issues/3455#issuecomment-535554401 + // propagateCreateError: false + }, + searchPath: ['public', 'queue'], // Define postgres schemas to match on + seeds: { + directory: __dirname + '/src/db/seeds' + } +}; diff --git a/comsapi/app/lcov-fix.js b/comsapi/app/lcov-fix.js new file mode 100644 index 00000000..24d01862 --- /dev/null +++ b/comsapi/app/lcov-fix.js @@ -0,0 +1,15 @@ +// Jest 25.x onwards emits coverage reports on a different source path +// https://stackoverflow.com/q/60323177 +const fs = require('fs'); +const file = './coverage/lcov.info'; + +fs.readFile(file, 'utf8', (err, data) => { + if (err) { + return console.error(err); // eslint-disable-line no-console + } + const result = data.replace(/src/g, `${process.cwd()}/src`); + + fs.writeFile(file, result, 'utf8', err => { + if (err) return console.error(err); // eslint-disable-line no-console + }); +}); diff --git a/comsapi/app/nodemon.json b/comsapi/app/nodemon.json new file mode 100644 index 00000000..1dff0087 --- /dev/null +++ b/comsapi/app/nodemon.json @@ -0,0 +1,9 @@ +{ + "ignore": [ + "frontend/node_modules", + "frontend/src", + "frontend/tests", + "node_modules/**/node_modules", + "tests" + ] +} diff --git a/comsapi/app/package-lock.json b/comsapi/app/package-lock.json new file mode 100644 index 00000000..03c686d4 --- /dev/null +++ b/comsapi/app/package-lock.json @@ -0,0 +1,10857 @@ +{ + "name": "common-object-management-service", + "version": "0.7.0", + "lockfileVersion": 1, + "requires": true, + "dependencies": { + "@aashutoshrathi/word-wrap": { + "version": "1.2.6", + "resolved": "https://registry.npmjs.org/@aashutoshrathi/word-wrap/-/word-wrap-1.2.6.tgz", + "integrity": "sha512-1Yjs2SvM8TflER/OD3cOjhWWOZb58A2t7wpE2S9XfBYTiIl+XFhQG2bjy4Pu1I+EAlCNUzRDYDdFwFYUKvXcIA==", + "dev": true + }, + "@aitodotai/json-stringify-pretty-compact": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/@aitodotai/json-stringify-pretty-compact/-/json-stringify-pretty-compact-1.3.0.tgz", + "integrity": "sha512-K+whdCBlVjzx8zCK2ZUohGJb5bUOxRpiEAfD1NCUgH0mApdDZD9c7VHXJVzWlt3wfV1X4OFyCRmTqbPd6U87lQ==", + "dev": true + }, + "@ampproject/remapping": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/@ampproject/remapping/-/remapping-2.2.1.tgz", + "integrity": "sha512-lFMjJTrFL3j7L9yBxwYfCq2k6qqwHyzuUl/XBnif78PWTJYyL/dfowQHWE3sp6U6ZzqWiiIZnpTMO96zhkjwtg==", + "dev": true, + "requires": { + "@jridgewell/gen-mapping": "^0.3.0", + "@jridgewell/trace-mapping": "^0.3.9" + } + }, + "@aws-crypto/crc32": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/@aws-crypto/crc32/-/crc32-3.0.0.tgz", + "integrity": "sha512-IzSgsrxUcsrejQbPVilIKy16kAT52EwB6zSaI+M3xxIhKh5+aldEyvI+z6erM7TCLB2BJsFrtHjp6/4/sr+3dA==", + "requires": { + "@aws-crypto/util": "^3.0.0", + "@aws-sdk/types": "^3.222.0", + "tslib": "^1.11.1" + } + }, + "@aws-crypto/crc32c": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/@aws-crypto/crc32c/-/crc32c-3.0.0.tgz", + "integrity": "sha512-ENNPPManmnVJ4BTXlOjAgD7URidbAznURqD0KvfREyc4o20DPYdEldU1f5cQ7Jbj0CJJSPaMIk/9ZshdB3210w==", + "requires": { + "@aws-crypto/util": "^3.0.0", + "@aws-sdk/types": "^3.222.0", + "tslib": "^1.11.1" + } + }, + "@aws-crypto/ie11-detection": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/@aws-crypto/ie11-detection/-/ie11-detection-3.0.0.tgz", + "integrity": "sha512-341lBBkiY1DfDNKai/wXM3aujNBkXR7tq1URPQDL9wi3AUbI80NR74uF1TXHMm7po1AcnFk8iu2S2IeU/+/A+Q==", + "requires": { + "tslib": "^1.11.1" + } + }, + "@aws-crypto/sha1-browser": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/@aws-crypto/sha1-browser/-/sha1-browser-3.0.0.tgz", + "integrity": "sha512-NJth5c997GLHs6nOYTzFKTbYdMNA6/1XlKVgnZoaZcQ7z7UJlOgj2JdbHE8tiYLS3fzXNCguct77SPGat2raSw==", + "requires": { + "@aws-crypto/ie11-detection": "^3.0.0", + "@aws-crypto/supports-web-crypto": "^3.0.0", + "@aws-crypto/util": "^3.0.0", + "@aws-sdk/types": "^3.222.0", + "@aws-sdk/util-locate-window": "^3.0.0", + "@aws-sdk/util-utf8-browser": "^3.0.0", + "tslib": "^1.11.1" + } + }, + "@aws-crypto/sha256-browser": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/@aws-crypto/sha256-browser/-/sha256-browser-3.0.0.tgz", + "integrity": "sha512-8VLmW2B+gjFbU5uMeqtQM6Nj0/F1bro80xQXCW6CQBWgosFWXTx77aeOF5CAIAmbOK64SdMBJdNr6J41yP5mvQ==", + "requires": { + "@aws-crypto/ie11-detection": "^3.0.0", + "@aws-crypto/sha256-js": "^3.0.0", + "@aws-crypto/supports-web-crypto": "^3.0.0", + "@aws-crypto/util": "^3.0.0", + "@aws-sdk/types": "^3.222.0", + "@aws-sdk/util-locate-window": "^3.0.0", + "@aws-sdk/util-utf8-browser": "^3.0.0", + "tslib": "^1.11.1" + } + }, + "@aws-crypto/sha256-js": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/@aws-crypto/sha256-js/-/sha256-js-3.0.0.tgz", + "integrity": "sha512-PnNN7os0+yd1XvXAy23CFOmTbMaDxgxXtTKHybrJ39Y8kGzBATgBFibWJKH6BhytLI/Zyszs87xCOBNyBig6vQ==", + "requires": { + "@aws-crypto/util": "^3.0.0", + "@aws-sdk/types": "^3.222.0", + "tslib": "^1.11.1" + } + }, + "@aws-crypto/supports-web-crypto": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/@aws-crypto/supports-web-crypto/-/supports-web-crypto-3.0.0.tgz", + "integrity": "sha512-06hBdMwUAb2WFTuGG73LSC0wfPu93xWwo5vL2et9eymgmu3Id5vFAHBbajVWiGhPO37qcsdCap/FqXvJGJWPIg==", + "requires": { + "tslib": "^1.11.1" + } + }, + "@aws-crypto/util": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/@aws-crypto/util/-/util-3.0.0.tgz", + "integrity": "sha512-2OJlpeJpCR48CC8r+uKVChzs9Iungj9wkZrl8Z041DWEWvyIHILYKCPNzJghKsivj+S3mLo6BVc7mBNzdxA46w==", + "requires": { + "@aws-sdk/types": "^3.222.0", + "@aws-sdk/util-utf8-browser": "^3.0.0", + "tslib": "^1.11.1" + } + }, + "@aws-sdk/client-s3": { + "version": "3.423.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/client-s3/-/client-s3-3.423.0.tgz", + "integrity": "sha512-Sn/6fotTDGp+uUfPU0JrKszHT/cYwZonly6Ahi4R/uxASLQnOEAF7MwVSjms+/LGu72Qs0Tt7B7RKW76GI4OIA==", + "requires": { + "@aws-crypto/sha1-browser": "3.0.0", + "@aws-crypto/sha256-browser": "3.0.0", + "@aws-crypto/sha256-js": "3.0.0", + "@aws-sdk/client-sts": "3.423.0", + "@aws-sdk/credential-provider-node": "3.423.0", + "@aws-sdk/middleware-bucket-endpoint": "3.418.0", + "@aws-sdk/middleware-expect-continue": "3.418.0", + "@aws-sdk/middleware-flexible-checksums": "3.418.0", + "@aws-sdk/middleware-host-header": "3.418.0", + "@aws-sdk/middleware-location-constraint": "3.418.0", + "@aws-sdk/middleware-logger": "3.418.0", + "@aws-sdk/middleware-recursion-detection": "3.418.0", + "@aws-sdk/middleware-sdk-s3": "3.418.0", + "@aws-sdk/middleware-signing": "3.418.0", + "@aws-sdk/middleware-ssec": "3.418.0", + "@aws-sdk/middleware-user-agent": "3.418.0", + "@aws-sdk/region-config-resolver": "3.418.0", + "@aws-sdk/signature-v4-multi-region": "3.418.0", + "@aws-sdk/types": "3.418.0", + "@aws-sdk/util-endpoints": "3.418.0", + "@aws-sdk/util-user-agent-browser": "3.418.0", + "@aws-sdk/util-user-agent-node": "3.418.0", + "@aws-sdk/xml-builder": "3.310.0", + "@smithy/config-resolver": "^2.0.10", + "@smithy/eventstream-serde-browser": "^2.0.9", + "@smithy/eventstream-serde-config-resolver": "^2.0.9", + "@smithy/eventstream-serde-node": "^2.0.9", + "@smithy/fetch-http-handler": "^2.1.5", + "@smithy/hash-blob-browser": "^2.0.9", + "@smithy/hash-node": "^2.0.9", + "@smithy/hash-stream-node": "^2.0.9", + "@smithy/invalid-dependency": "^2.0.9", + "@smithy/md5-js": "^2.0.9", + "@smithy/middleware-content-length": "^2.0.11", + "@smithy/middleware-endpoint": "^2.0.9", + "@smithy/middleware-retry": "^2.0.12", + "@smithy/middleware-serde": "^2.0.9", + "@smithy/middleware-stack": "^2.0.2", + "@smithy/node-config-provider": "^2.0.12", + "@smithy/node-http-handler": "^2.1.5", + "@smithy/protocol-http": "^3.0.5", + "@smithy/smithy-client": "^2.1.6", + "@smithy/types": "^2.3.3", + "@smithy/url-parser": "^2.0.9", + "@smithy/util-base64": "^2.0.0", + "@smithy/util-body-length-browser": "^2.0.0", + "@smithy/util-body-length-node": "^2.1.0", + "@smithy/util-defaults-mode-browser": "^2.0.10", + "@smithy/util-defaults-mode-node": "^2.0.12", + "@smithy/util-retry": "^2.0.2", + "@smithy/util-stream": "^2.0.12", + "@smithy/util-utf8": "^2.0.0", + "@smithy/util-waiter": "^2.0.9", + "fast-xml-parser": "4.2.5", + "tslib": "^2.5.0" + }, + "dependencies": { + "tslib": { + "version": "2.5.2", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.5.2.tgz", + "integrity": "sha512-5svOrSA2w3iGFDs1HibEVBGbDrAY82bFQ3HZ3ixB+88nsbsWQoKqDRb5UBYAUPEzbBn6dAp5gRNXglySbx1MlA==" + } + } + }, + "@aws-sdk/client-sso": { + "version": "3.423.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/client-sso/-/client-sso-3.423.0.tgz", + "integrity": "sha512-znIufHkwhCIePgaYciIs3x/+BpzR57CZzbCKHR9+oOvGyufEPPpUT5bFLvbwTgfiVkTjuk6sG/ES3U5Bc+xtrA==", + "requires": { + "@aws-crypto/sha256-browser": "3.0.0", + "@aws-crypto/sha256-js": "3.0.0", + "@aws-sdk/middleware-host-header": "3.418.0", + "@aws-sdk/middleware-logger": "3.418.0", + "@aws-sdk/middleware-recursion-detection": "3.418.0", + "@aws-sdk/middleware-user-agent": "3.418.0", + "@aws-sdk/region-config-resolver": "3.418.0", + "@aws-sdk/types": "3.418.0", + "@aws-sdk/util-endpoints": "3.418.0", + "@aws-sdk/util-user-agent-browser": "3.418.0", + "@aws-sdk/util-user-agent-node": "3.418.0", + "@smithy/config-resolver": "^2.0.10", + "@smithy/fetch-http-handler": "^2.1.5", + "@smithy/hash-node": "^2.0.9", + "@smithy/invalid-dependency": "^2.0.9", + "@smithy/middleware-content-length": "^2.0.11", + "@smithy/middleware-endpoint": "^2.0.9", + "@smithy/middleware-retry": "^2.0.12", + "@smithy/middleware-serde": "^2.0.9", + "@smithy/middleware-stack": "^2.0.2", + "@smithy/node-config-provider": "^2.0.12", + "@smithy/node-http-handler": "^2.1.5", + "@smithy/protocol-http": "^3.0.5", + "@smithy/smithy-client": "^2.1.6", + "@smithy/types": "^2.3.3", + "@smithy/url-parser": "^2.0.9", + "@smithy/util-base64": "^2.0.0", + "@smithy/util-body-length-browser": "^2.0.0", + "@smithy/util-body-length-node": "^2.1.0", + "@smithy/util-defaults-mode-browser": "^2.0.10", + "@smithy/util-defaults-mode-node": "^2.0.12", + "@smithy/util-retry": "^2.0.2", + "@smithy/util-utf8": "^2.0.0", + "tslib": "^2.5.0" + }, + "dependencies": { + "tslib": { + "version": "2.6.2", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.6.2.tgz", + "integrity": "sha512-AEYxH93jGFPn/a2iVAwW87VuUIkR1FVUKB77NwMF7nBTDkDrrT/Hpt/IrCJ0QXhW27jTBDcf5ZY7w6RiqTMw2Q==" + } + } + }, + "@aws-sdk/client-sts": { + "version": "3.423.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/client-sts/-/client-sts-3.423.0.tgz", + "integrity": "sha512-EcpkKu02QZbRX6dQE0u7a8RgWrn/5riz1qAlKd7rM8FZJpr/D6GGX8ZzWxjgp7pRUgfNvinTmIudDnyQY3v9Mg==", + "requires": { + "@aws-crypto/sha256-browser": "3.0.0", + "@aws-crypto/sha256-js": "3.0.0", + "@aws-sdk/credential-provider-node": "3.423.0", + "@aws-sdk/middleware-host-header": "3.418.0", + "@aws-sdk/middleware-logger": "3.418.0", + "@aws-sdk/middleware-recursion-detection": "3.418.0", + "@aws-sdk/middleware-sdk-sts": "3.418.0", + "@aws-sdk/middleware-signing": "3.418.0", + "@aws-sdk/middleware-user-agent": "3.418.0", + "@aws-sdk/region-config-resolver": "3.418.0", + "@aws-sdk/types": "3.418.0", + "@aws-sdk/util-endpoints": "3.418.0", + "@aws-sdk/util-user-agent-browser": "3.418.0", + "@aws-sdk/util-user-agent-node": "3.418.0", + "@smithy/config-resolver": "^2.0.10", + "@smithy/fetch-http-handler": "^2.1.5", + "@smithy/hash-node": "^2.0.9", + "@smithy/invalid-dependency": "^2.0.9", + "@smithy/middleware-content-length": "^2.0.11", + "@smithy/middleware-endpoint": "^2.0.9", + "@smithy/middleware-retry": "^2.0.12", + "@smithy/middleware-serde": "^2.0.9", + "@smithy/middleware-stack": "^2.0.2", + "@smithy/node-config-provider": "^2.0.12", + "@smithy/node-http-handler": "^2.1.5", + "@smithy/protocol-http": "^3.0.5", + "@smithy/smithy-client": "^2.1.6", + "@smithy/types": "^2.3.3", + "@smithy/url-parser": "^2.0.9", + "@smithy/util-base64": "^2.0.0", + "@smithy/util-body-length-browser": "^2.0.0", + "@smithy/util-body-length-node": "^2.1.0", + "@smithy/util-defaults-mode-browser": "^2.0.10", + "@smithy/util-defaults-mode-node": "^2.0.12", + "@smithy/util-retry": "^2.0.2", + "@smithy/util-utf8": "^2.0.0", + "fast-xml-parser": "4.2.5", + "tslib": "^2.5.0" + }, + "dependencies": { + "tslib": { + "version": "2.6.2", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.6.2.tgz", + "integrity": "sha512-AEYxH93jGFPn/a2iVAwW87VuUIkR1FVUKB77NwMF7nBTDkDrrT/Hpt/IrCJ0QXhW27jTBDcf5ZY7w6RiqTMw2Q==" + } + } + }, + "@aws-sdk/credential-provider-env": { + "version": "3.418.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-env/-/credential-provider-env-3.418.0.tgz", + "integrity": "sha512-e74sS+x63EZUBO+HaI8zor886YdtmULzwKdctsZp5/37Xho1CVUNtEC+fYa69nigBD9afoiH33I4JggaHgrekQ==", + "requires": { + "@aws-sdk/types": "3.418.0", + "@smithy/property-provider": "^2.0.0", + "@smithy/types": "^2.3.3", + "tslib": "^2.5.0" + }, + "dependencies": { + "tslib": { + "version": "2.6.2", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.6.2.tgz", + "integrity": "sha512-AEYxH93jGFPn/a2iVAwW87VuUIkR1FVUKB77NwMF7nBTDkDrrT/Hpt/IrCJ0QXhW27jTBDcf5ZY7w6RiqTMw2Q==" + } + } + }, + "@aws-sdk/credential-provider-ini": { + "version": "3.423.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-ini/-/credential-provider-ini-3.423.0.tgz", + "integrity": "sha512-7CsFWz8g7dQmblp57XzzxMirO4ClowGZIOwAheBkmk6q1XHbllcHFnbh2kdPyQQ0+JmjDg6waztIc7dY7Ycfvw==", + "requires": { + "@aws-sdk/credential-provider-env": "3.418.0", + "@aws-sdk/credential-provider-process": "3.418.0", + "@aws-sdk/credential-provider-sso": "3.423.0", + "@aws-sdk/credential-provider-web-identity": "3.418.0", + "@aws-sdk/types": "3.418.0", + "@smithy/credential-provider-imds": "^2.0.0", + "@smithy/property-provider": "^2.0.0", + "@smithy/shared-ini-file-loader": "^2.0.6", + "@smithy/types": "^2.3.3", + "tslib": "^2.5.0" + }, + "dependencies": { + "tslib": { + "version": "2.6.2", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.6.2.tgz", + "integrity": "sha512-AEYxH93jGFPn/a2iVAwW87VuUIkR1FVUKB77NwMF7nBTDkDrrT/Hpt/IrCJ0QXhW27jTBDcf5ZY7w6RiqTMw2Q==" + } + } + }, + "@aws-sdk/credential-provider-node": { + "version": "3.423.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-node/-/credential-provider-node-3.423.0.tgz", + "integrity": "sha512-lygbGJJUnDpgo8OEqdoYd51BKkyBVQ1Catiua/m0aHvL+SCmVrHiYPQPawWYGxpH8X3DXdXa0nd0LkEaevrHRg==", + "requires": { + "@aws-sdk/credential-provider-env": "3.418.0", + "@aws-sdk/credential-provider-ini": "3.423.0", + "@aws-sdk/credential-provider-process": "3.418.0", + "@aws-sdk/credential-provider-sso": "3.423.0", + "@aws-sdk/credential-provider-web-identity": "3.418.0", + "@aws-sdk/types": "3.418.0", + "@smithy/credential-provider-imds": "^2.0.0", + "@smithy/property-provider": "^2.0.0", + "@smithy/shared-ini-file-loader": "^2.0.6", + "@smithy/types": "^2.3.3", + "tslib": "^2.5.0" + }, + "dependencies": { + "tslib": { + "version": "2.6.2", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.6.2.tgz", + "integrity": "sha512-AEYxH93jGFPn/a2iVAwW87VuUIkR1FVUKB77NwMF7nBTDkDrrT/Hpt/IrCJ0QXhW27jTBDcf5ZY7w6RiqTMw2Q==" + } + } + }, + "@aws-sdk/credential-provider-process": { + "version": "3.418.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-process/-/credential-provider-process-3.418.0.tgz", + "integrity": "sha512-xPbdm2WKz1oH6pTkrJoUmr3OLuqvvcPYTQX0IIlc31tmDwDWPQjXGGFD/vwZGIZIkKaFpFxVMgAzfFScxox7dw==", + "requires": { + "@aws-sdk/types": "3.418.0", + "@smithy/property-provider": "^2.0.0", + "@smithy/shared-ini-file-loader": "^2.0.6", + "@smithy/types": "^2.3.3", + "tslib": "^2.5.0" + }, + "dependencies": { + "tslib": { + "version": "2.6.2", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.6.2.tgz", + "integrity": "sha512-AEYxH93jGFPn/a2iVAwW87VuUIkR1FVUKB77NwMF7nBTDkDrrT/Hpt/IrCJ0QXhW27jTBDcf5ZY7w6RiqTMw2Q==" + } + } + }, + "@aws-sdk/credential-provider-sso": { + "version": "3.423.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-sso/-/credential-provider-sso-3.423.0.tgz", + "integrity": "sha512-zAH68IjRMmW22USbsCVQ5Q6AHqhmWABwLbZAMocSGMasddTGv/nkA/nUiVCJ/B4LI3P81FoPQVrG5JxNmkNH0w==", + "requires": { + "@aws-sdk/client-sso": "3.423.0", + "@aws-sdk/token-providers": "3.418.0", + "@aws-sdk/types": "3.418.0", + "@smithy/property-provider": "^2.0.0", + "@smithy/shared-ini-file-loader": "^2.0.6", + "@smithy/types": "^2.3.3", + "tslib": "^2.5.0" + }, + "dependencies": { + "tslib": { + "version": "2.6.2", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.6.2.tgz", + "integrity": "sha512-AEYxH93jGFPn/a2iVAwW87VuUIkR1FVUKB77NwMF7nBTDkDrrT/Hpt/IrCJ0QXhW27jTBDcf5ZY7w6RiqTMw2Q==" + } + } + }, + "@aws-sdk/credential-provider-web-identity": { + "version": "3.418.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-web-identity/-/credential-provider-web-identity-3.418.0.tgz", + "integrity": "sha512-do7ang565n9p3dS1JdsQY01rUfRx8vkxQqz5M8OlcEHBNiCdi2PvSjNwcBdrv/FKkyIxZb0TImOfBSt40hVdxQ==", + "requires": { + "@aws-sdk/types": "3.418.0", + "@smithy/property-provider": "^2.0.0", + "@smithy/types": "^2.3.3", + "tslib": "^2.5.0" + }, + "dependencies": { + "tslib": { + "version": "2.6.2", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.6.2.tgz", + "integrity": "sha512-AEYxH93jGFPn/a2iVAwW87VuUIkR1FVUKB77NwMF7nBTDkDrrT/Hpt/IrCJ0QXhW27jTBDcf5ZY7w6RiqTMw2Q==" + } + } + }, + "@aws-sdk/lib-storage": { + "version": "3.423.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/lib-storage/-/lib-storage-3.423.0.tgz", + "integrity": "sha512-EyE1d/99BxUdfHs144u7h5gkb/GccApHT2uq1QwaLHqee5rw8+oZoAbhrwQ5kyysH2v3GSdcBEeTwonNJo1jSw==", + "requires": { + "@smithy/abort-controller": "^2.0.1", + "@smithy/middleware-endpoint": "^2.0.9", + "@smithy/smithy-client": "^2.1.6", + "buffer": "5.6.0", + "events": "3.3.0", + "stream-browserify": "3.0.0", + "tslib": "^2.5.0" + }, + "dependencies": { + "tslib": { + "version": "2.5.2", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.5.2.tgz", + "integrity": "sha512-5svOrSA2w3iGFDs1HibEVBGbDrAY82bFQ3HZ3ixB+88nsbsWQoKqDRb5UBYAUPEzbBn6dAp5gRNXglySbx1MlA==" + } + } + }, + "@aws-sdk/middleware-bucket-endpoint": { + "version": "3.418.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-bucket-endpoint/-/middleware-bucket-endpoint-3.418.0.tgz", + "integrity": "sha512-gj/mj1UfbKkGbQ1N4YUvjTTp8BVs5fO1QAL2AjFJ+jfJOToLReX72aNEkm7sPGbHML0TqOY4cQbJuWYy+zdD5g==", + "requires": { + "@aws-sdk/types": "3.418.0", + "@aws-sdk/util-arn-parser": "3.310.0", + "@smithy/node-config-provider": "^2.0.12", + "@smithy/protocol-http": "^3.0.5", + "@smithy/types": "^2.3.3", + "@smithy/util-config-provider": "^2.0.0", + "tslib": "^2.5.0" + }, + "dependencies": { + "tslib": { + "version": "2.6.2", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.6.2.tgz", + "integrity": "sha512-AEYxH93jGFPn/a2iVAwW87VuUIkR1FVUKB77NwMF7nBTDkDrrT/Hpt/IrCJ0QXhW27jTBDcf5ZY7w6RiqTMw2Q==" + } + } + }, + "@aws-sdk/middleware-expect-continue": { + "version": "3.418.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-expect-continue/-/middleware-expect-continue-3.418.0.tgz", + "integrity": "sha512-6x4rcIj685EmqDLQkbWoCur3Dg5DRClHMen6nHXmD3CR5Xyt3z1Gk/+jmZICxyJo9c6M4AeZht8o95BopkmYAQ==", + "requires": { + "@aws-sdk/types": "3.418.0", + "@smithy/protocol-http": "^3.0.5", + "@smithy/types": "^2.3.3", + "tslib": "^2.5.0" + }, + "dependencies": { + "tslib": { + "version": "2.6.2", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.6.2.tgz", + "integrity": "sha512-AEYxH93jGFPn/a2iVAwW87VuUIkR1FVUKB77NwMF7nBTDkDrrT/Hpt/IrCJ0QXhW27jTBDcf5ZY7w6RiqTMw2Q==" + } + } + }, + "@aws-sdk/middleware-flexible-checksums": { + "version": "3.418.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-flexible-checksums/-/middleware-flexible-checksums-3.418.0.tgz", + "integrity": "sha512-3O203dqS2JU5P1TAAbo7p1qplXQh59pevw9nqzPVb3EG8B+mSucVf2kKmF7kGHqKSk+nK/mB/4XGSsZBzGt6Wg==", + "requires": { + "@aws-crypto/crc32": "3.0.0", + "@aws-crypto/crc32c": "3.0.0", + "@aws-sdk/types": "3.418.0", + "@smithy/is-array-buffer": "^2.0.0", + "@smithy/protocol-http": "^3.0.5", + "@smithy/types": "^2.3.3", + "@smithy/util-utf8": "^2.0.0", + "tslib": "^2.5.0" + }, + "dependencies": { + "tslib": { + "version": "2.6.2", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.6.2.tgz", + "integrity": "sha512-AEYxH93jGFPn/a2iVAwW87VuUIkR1FVUKB77NwMF7nBTDkDrrT/Hpt/IrCJ0QXhW27jTBDcf5ZY7w6RiqTMw2Q==" + } + } + }, + "@aws-sdk/middleware-host-header": { + "version": "3.418.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-host-header/-/middleware-host-header-3.418.0.tgz", + "integrity": "sha512-LrMTdzalkPw/1ujLCKPLwCGvPMCmT4P+vOZQRbSEVZPnlZk+Aj++aL/RaHou0jL4kJH3zl8iQepriBt4a7UvXQ==", + "requires": { + "@aws-sdk/types": "3.418.0", + "@smithy/protocol-http": "^3.0.5", + "@smithy/types": "^2.3.3", + "tslib": "^2.5.0" + }, + "dependencies": { + "tslib": { + "version": "2.6.2", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.6.2.tgz", + "integrity": "sha512-AEYxH93jGFPn/a2iVAwW87VuUIkR1FVUKB77NwMF7nBTDkDrrT/Hpt/IrCJ0QXhW27jTBDcf5ZY7w6RiqTMw2Q==" + } + } + }, + "@aws-sdk/middleware-location-constraint": { + "version": "3.418.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-location-constraint/-/middleware-location-constraint-3.418.0.tgz", + "integrity": "sha512-cc8M3VEaESHJhDsDV8tTpt2QYUprDWhvAVVSlcL43cTdZ54Quc0W+toDiaVOUlwrAZz2Y7g5NDj22ibJGFbOvw==", + "requires": { + "@aws-sdk/types": "3.418.0", + "@smithy/types": "^2.3.3", + "tslib": "^2.5.0" + }, + "dependencies": { + "tslib": { + "version": "2.6.2", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.6.2.tgz", + "integrity": "sha512-AEYxH93jGFPn/a2iVAwW87VuUIkR1FVUKB77NwMF7nBTDkDrrT/Hpt/IrCJ0QXhW27jTBDcf5ZY7w6RiqTMw2Q==" + } + } + }, + "@aws-sdk/middleware-logger": { + "version": "3.418.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-logger/-/middleware-logger-3.418.0.tgz", + "integrity": "sha512-StKGmyPVfoO/wdNTtKemYwoJsqIl4l7oqarQY7VSf2Mp3mqaa+njLViHsQbirYpyqpgUEusOnuTlH5utxJ1NsQ==", + "requires": { + "@aws-sdk/types": "3.418.0", + "@smithy/types": "^2.3.3", + "tslib": "^2.5.0" + }, + "dependencies": { + "tslib": { + "version": "2.6.2", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.6.2.tgz", + "integrity": "sha512-AEYxH93jGFPn/a2iVAwW87VuUIkR1FVUKB77NwMF7nBTDkDrrT/Hpt/IrCJ0QXhW27jTBDcf5ZY7w6RiqTMw2Q==" + } + } + }, + "@aws-sdk/middleware-recursion-detection": { + "version": "3.418.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-recursion-detection/-/middleware-recursion-detection-3.418.0.tgz", + "integrity": "sha512-kKFrIQglBLUFPbHSDy1+bbe3Na2Kd70JSUC3QLMbUHmqipXN8KeXRfAj7vTv97zXl0WzG0buV++WcNwOm1rFjg==", + "requires": { + "@aws-sdk/types": "3.418.0", + "@smithy/protocol-http": "^3.0.5", + "@smithy/types": "^2.3.3", + "tslib": "^2.5.0" + }, + "dependencies": { + "tslib": { + "version": "2.6.2", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.6.2.tgz", + "integrity": "sha512-AEYxH93jGFPn/a2iVAwW87VuUIkR1FVUKB77NwMF7nBTDkDrrT/Hpt/IrCJ0QXhW27jTBDcf5ZY7w6RiqTMw2Q==" + } + } + }, + "@aws-sdk/middleware-sdk-s3": { + "version": "3.418.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-sdk-s3/-/middleware-sdk-s3-3.418.0.tgz", + "integrity": "sha512-rei32LF45SyqL3NlWDjEOfMwAca9A5F4QgUyXJqvASc43oWC1tJnLIhiCxNh8qkWAiRyRzFpcanTeqyaRSsZpA==", + "requires": { + "@aws-sdk/types": "3.418.0", + "@aws-sdk/util-arn-parser": "3.310.0", + "@smithy/protocol-http": "^3.0.5", + "@smithy/smithy-client": "^2.1.6", + "@smithy/types": "^2.3.3", + "tslib": "^2.5.0" + }, + "dependencies": { + "tslib": { + "version": "2.6.2", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.6.2.tgz", + "integrity": "sha512-AEYxH93jGFPn/a2iVAwW87VuUIkR1FVUKB77NwMF7nBTDkDrrT/Hpt/IrCJ0QXhW27jTBDcf5ZY7w6RiqTMw2Q==" + } + } + }, + "@aws-sdk/middleware-sdk-sts": { + "version": "3.418.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-sdk-sts/-/middleware-sdk-sts-3.418.0.tgz", + "integrity": "sha512-cW8ijrCTP+mgihvcq4+TbhAcE/we5lFl4ydRqvTdtcSnYQAVQADg47rnTScQiFsPFEB3NKq7BGeyTJF9MKolPA==", + "requires": { + "@aws-sdk/middleware-signing": "3.418.0", + "@aws-sdk/types": "3.418.0", + "@smithy/types": "^2.3.3", + "tslib": "^2.5.0" + }, + "dependencies": { + "tslib": { + "version": "2.6.2", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.6.2.tgz", + "integrity": "sha512-AEYxH93jGFPn/a2iVAwW87VuUIkR1FVUKB77NwMF7nBTDkDrrT/Hpt/IrCJ0QXhW27jTBDcf5ZY7w6RiqTMw2Q==" + } + } + }, + "@aws-sdk/middleware-signing": { + "version": "3.418.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-signing/-/middleware-signing-3.418.0.tgz", + "integrity": "sha512-onvs5KoYQE8OlOE740RxWBGtsUyVIgAo0CzRKOQO63ZEYqpL1Os+MS1CGzdNhvQnJgJruE1WW+Ix8fjN30zKPA==", + "requires": { + "@aws-sdk/types": "3.418.0", + "@smithy/property-provider": "^2.0.0", + "@smithy/protocol-http": "^3.0.5", + "@smithy/signature-v4": "^2.0.0", + "@smithy/types": "^2.3.3", + "@smithy/util-middleware": "^2.0.2", + "tslib": "^2.5.0" + }, + "dependencies": { + "tslib": { + "version": "2.6.2", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.6.2.tgz", + "integrity": "sha512-AEYxH93jGFPn/a2iVAwW87VuUIkR1FVUKB77NwMF7nBTDkDrrT/Hpt/IrCJ0QXhW27jTBDcf5ZY7w6RiqTMw2Q==" + } + } + }, + "@aws-sdk/middleware-ssec": { + "version": "3.418.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-ssec/-/middleware-ssec-3.418.0.tgz", + "integrity": "sha512-J7K+5h6aP7IYMlu/NwHEIjb0+WDu1eFvO8TCPo6j1H9xYRi8B/6h+6pa9Rk9IgRUzFnrdlDu9FazG8Tp0KKLyg==", + "requires": { + "@aws-sdk/types": "3.418.0", + "@smithy/types": "^2.3.3", + "tslib": "^2.5.0" + }, + "dependencies": { + "tslib": { + "version": "2.6.2", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.6.2.tgz", + "integrity": "sha512-AEYxH93jGFPn/a2iVAwW87VuUIkR1FVUKB77NwMF7nBTDkDrrT/Hpt/IrCJ0QXhW27jTBDcf5ZY7w6RiqTMw2Q==" + } + } + }, + "@aws-sdk/middleware-user-agent": { + "version": "3.418.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-user-agent/-/middleware-user-agent-3.418.0.tgz", + "integrity": "sha512-Jdcztg9Tal9SEAL0dKRrnpKrm6LFlWmAhvuwv0dQ7bNTJxIxyEFbpqdgy7mpQHsLVZgq1Aad/7gT/72c9igyZw==", + "requires": { + "@aws-sdk/types": "3.418.0", + "@aws-sdk/util-endpoints": "3.418.0", + "@smithy/protocol-http": "^3.0.5", + "@smithy/types": "^2.3.3", + "tslib": "^2.5.0" + }, + "dependencies": { + "tslib": { + "version": "2.6.2", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.6.2.tgz", + "integrity": "sha512-AEYxH93jGFPn/a2iVAwW87VuUIkR1FVUKB77NwMF7nBTDkDrrT/Hpt/IrCJ0QXhW27jTBDcf5ZY7w6RiqTMw2Q==" + } + } + }, + "@aws-sdk/region-config-resolver": { + "version": "3.418.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/region-config-resolver/-/region-config-resolver-3.418.0.tgz", + "integrity": "sha512-lJRZ/9TjZU6yLz+mAwxJkcJZ6BmyYoIJVo1p5+BN//EFdEmC8/c0c9gXMRzfISV/mqWSttdtccpAyN4/goHTYA==", + "requires": { + "@smithy/node-config-provider": "^2.0.12", + "@smithy/types": "^2.3.3", + "@smithy/util-config-provider": "^2.0.0", + "@smithy/util-middleware": "^2.0.2", + "tslib": "^2.5.0" + }, + "dependencies": { + "tslib": { + "version": "2.6.2", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.6.2.tgz", + "integrity": "sha512-AEYxH93jGFPn/a2iVAwW87VuUIkR1FVUKB77NwMF7nBTDkDrrT/Hpt/IrCJ0QXhW27jTBDcf5ZY7w6RiqTMw2Q==" + } + } + }, + "@aws-sdk/s3-request-presigner": { + "version": "3.423.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/s3-request-presigner/-/s3-request-presigner-3.423.0.tgz", + "integrity": "sha512-VwC5WjcFKdmPpQXYn6vKi+9iJtP6a0sY9UJu0fy6yXKK4pm9yk9ZFflq46PWT/Z6JNAR+dvi+hzAZLbvXWpSvA==", + "requires": { + "@aws-sdk/signature-v4-multi-region": "3.418.0", + "@aws-sdk/types": "3.418.0", + "@aws-sdk/util-format-url": "3.418.0", + "@smithy/middleware-endpoint": "^2.0.9", + "@smithy/protocol-http": "^3.0.5", + "@smithy/smithy-client": "^2.1.6", + "@smithy/types": "^2.3.3", + "tslib": "^2.5.0" + }, + "dependencies": { + "tslib": { + "version": "2.5.2", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.5.2.tgz", + "integrity": "sha512-5svOrSA2w3iGFDs1HibEVBGbDrAY82bFQ3HZ3ixB+88nsbsWQoKqDRb5UBYAUPEzbBn6dAp5gRNXglySbx1MlA==" + } + } + }, + "@aws-sdk/signature-v4-multi-region": { + "version": "3.418.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/signature-v4-multi-region/-/signature-v4-multi-region-3.418.0.tgz", + "integrity": "sha512-LeVYMZeUQUURFqDf4yZxTEv016g64hi0LqYBjU0mjwd8aPc0k6hckwvshezc80jCNbuLyjNfQclvlg3iFliItQ==", + "requires": { + "@aws-sdk/types": "3.418.0", + "@smithy/protocol-http": "^3.0.5", + "@smithy/signature-v4": "^2.0.0", + "@smithy/types": "^2.3.3", + "tslib": "^2.5.0" + }, + "dependencies": { + "tslib": { + "version": "2.6.2", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.6.2.tgz", + "integrity": "sha512-AEYxH93jGFPn/a2iVAwW87VuUIkR1FVUKB77NwMF7nBTDkDrrT/Hpt/IrCJ0QXhW27jTBDcf5ZY7w6RiqTMw2Q==" + } + } + }, + "@aws-sdk/token-providers": { + "version": "3.418.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/token-providers/-/token-providers-3.418.0.tgz", + "integrity": "sha512-9P7Q0VN0hEzTngy3Sz5eya2qEOEf0Q8qf1vB3um0gE6ID6EVAdz/nc/DztfN32MFxk8FeVBrCP5vWdoOzmd72g==", + "requires": { + "@aws-crypto/sha256-browser": "3.0.0", + "@aws-crypto/sha256-js": "3.0.0", + "@aws-sdk/middleware-host-header": "3.418.0", + "@aws-sdk/middleware-logger": "3.418.0", + "@aws-sdk/middleware-recursion-detection": "3.418.0", + "@aws-sdk/middleware-user-agent": "3.418.0", + "@aws-sdk/types": "3.418.0", + "@aws-sdk/util-endpoints": "3.418.0", + "@aws-sdk/util-user-agent-browser": "3.418.0", + "@aws-sdk/util-user-agent-node": "3.418.0", + "@smithy/config-resolver": "^2.0.10", + "@smithy/fetch-http-handler": "^2.1.5", + "@smithy/hash-node": "^2.0.9", + "@smithy/invalid-dependency": "^2.0.9", + "@smithy/middleware-content-length": "^2.0.11", + "@smithy/middleware-endpoint": "^2.0.9", + "@smithy/middleware-retry": "^2.0.12", + "@smithy/middleware-serde": "^2.0.9", + "@smithy/middleware-stack": "^2.0.2", + "@smithy/node-config-provider": "^2.0.12", + "@smithy/node-http-handler": "^2.1.5", + "@smithy/property-provider": "^2.0.0", + "@smithy/protocol-http": "^3.0.5", + "@smithy/shared-ini-file-loader": "^2.0.6", + "@smithy/smithy-client": "^2.1.6", + "@smithy/types": "^2.3.3", + "@smithy/url-parser": "^2.0.9", + "@smithy/util-base64": "^2.0.0", + "@smithy/util-body-length-browser": "^2.0.0", + "@smithy/util-body-length-node": "^2.1.0", + "@smithy/util-defaults-mode-browser": "^2.0.10", + "@smithy/util-defaults-mode-node": "^2.0.12", + "@smithy/util-retry": "^2.0.2", + "@smithy/util-utf8": "^2.0.0", + "tslib": "^2.5.0" + }, + "dependencies": { + "tslib": { + "version": "2.6.2", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.6.2.tgz", + "integrity": "sha512-AEYxH93jGFPn/a2iVAwW87VuUIkR1FVUKB77NwMF7nBTDkDrrT/Hpt/IrCJ0QXhW27jTBDcf5ZY7w6RiqTMw2Q==" + } + } + }, + "@aws-sdk/types": { + "version": "3.418.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/types/-/types-3.418.0.tgz", + "integrity": "sha512-y4PQSH+ulfFLY0+FYkaK4qbIaQI9IJNMO2xsxukW6/aNoApNymN1D2FSi2la8Qbp/iPjNDKsG8suNPm9NtsWXQ==", + "requires": { + "@smithy/types": "^2.3.3", + "tslib": "^2.5.0" + }, + "dependencies": { + "tslib": { + "version": "2.5.2", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.5.2.tgz", + "integrity": "sha512-5svOrSA2w3iGFDs1HibEVBGbDrAY82bFQ3HZ3ixB+88nsbsWQoKqDRb5UBYAUPEzbBn6dAp5gRNXglySbx1MlA==" + } + } + }, + "@aws-sdk/util-arn-parser": { + "version": "3.310.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/util-arn-parser/-/util-arn-parser-3.310.0.tgz", + "integrity": "sha512-jL8509owp/xB9+Or0pvn3Fe+b94qfklc2yPowZZIFAkFcCSIdkIglz18cPDWnYAcy9JGewpMS1COXKIUhZkJsA==", + "requires": { + "tslib": "^2.5.0" + }, + "dependencies": { + "tslib": { + "version": "2.6.2", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.6.2.tgz", + "integrity": "sha512-AEYxH93jGFPn/a2iVAwW87VuUIkR1FVUKB77NwMF7nBTDkDrrT/Hpt/IrCJ0QXhW27jTBDcf5ZY7w6RiqTMw2Q==" + } + } + }, + "@aws-sdk/util-endpoints": { + "version": "3.418.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/util-endpoints/-/util-endpoints-3.418.0.tgz", + "integrity": "sha512-sYSDwRTl7yE7LhHkPzemGzmIXFVHSsi3AQ1KeNEk84eBqxMHHcCc2kqklaBk2roXWe50QDgRMy1ikZUxvtzNHQ==", + "requires": { + "@aws-sdk/types": "3.418.0", + "tslib": "^2.5.0" + }, + "dependencies": { + "tslib": { + "version": "2.6.2", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.6.2.tgz", + "integrity": "sha512-AEYxH93jGFPn/a2iVAwW87VuUIkR1FVUKB77NwMF7nBTDkDrrT/Hpt/IrCJ0QXhW27jTBDcf5ZY7w6RiqTMw2Q==" + } + } + }, + "@aws-sdk/util-format-url": { + "version": "3.418.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/util-format-url/-/util-format-url-3.418.0.tgz", + "integrity": "sha512-7/Xy+8J1txuOYOKsez6vpKTIkHYIIX4c7anjp/aQgUQL23FDwkPisj56cIlevJ7useGugnYw1rUR6fMULGzQ/g==", + "requires": { + "@aws-sdk/types": "3.418.0", + "@smithy/querystring-builder": "^2.0.9", + "@smithy/types": "^2.3.3", + "tslib": "^2.5.0" + }, + "dependencies": { + "tslib": { + "version": "2.6.2", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.6.2.tgz", + "integrity": "sha512-AEYxH93jGFPn/a2iVAwW87VuUIkR1FVUKB77NwMF7nBTDkDrrT/Hpt/IrCJ0QXhW27jTBDcf5ZY7w6RiqTMw2Q==" + } + } + }, + "@aws-sdk/util-locate-window": { + "version": "3.208.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/util-locate-window/-/util-locate-window-3.208.0.tgz", + "integrity": "sha512-iua1A2+P7JJEDHVgvXrRJSvsnzG7stYSGQnBVphIUlemwl6nN5D+QrgbjECtrbxRz8asYFHSzhdhECqN+tFiBg==", + "requires": { + "tslib": "^2.3.1" + }, + "dependencies": { + "tslib": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.5.0.tgz", + "integrity": "sha512-336iVw3rtn2BUK7ORdIAHTyxHGRIHVReokCR3XjbckJMK7ms8FysBfhLR8IXnAgy7T0PTPNBWKiH514FOW/WSg==" + } + } + }, + "@aws-sdk/util-user-agent-browser": { + "version": "3.418.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/util-user-agent-browser/-/util-user-agent-browser-3.418.0.tgz", + "integrity": "sha512-c4p4mc0VV/jIeNH0lsXzhJ1MpWRLuboGtNEpqE4s1Vl9ck2amv9VdUUZUmHbg+bVxlMgRQ4nmiovA4qIrqGuyg==", + "requires": { + "@aws-sdk/types": "3.418.0", + "@smithy/types": "^2.3.3", + "bowser": "^2.11.0", + "tslib": "^2.5.0" + }, + "dependencies": { + "tslib": { + "version": "2.6.2", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.6.2.tgz", + "integrity": "sha512-AEYxH93jGFPn/a2iVAwW87VuUIkR1FVUKB77NwMF7nBTDkDrrT/Hpt/IrCJ0QXhW27jTBDcf5ZY7w6RiqTMw2Q==" + } + } + }, + "@aws-sdk/util-user-agent-node": { + "version": "3.418.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/util-user-agent-node/-/util-user-agent-node-3.418.0.tgz", + "integrity": "sha512-BXMskXFtg+dmzSCgmnWOffokxIbPr1lFqa1D9kvM3l3IFRiFGx2IyDg+8MAhq11aPDLvoa/BDuQ0Yqma5izOhg==", + "requires": { + "@aws-sdk/types": "3.418.0", + "@smithy/node-config-provider": "^2.0.12", + "@smithy/types": "^2.3.3", + "tslib": "^2.5.0" + }, + "dependencies": { + "tslib": { + "version": "2.6.2", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.6.2.tgz", + "integrity": "sha512-AEYxH93jGFPn/a2iVAwW87VuUIkR1FVUKB77NwMF7nBTDkDrrT/Hpt/IrCJ0QXhW27jTBDcf5ZY7w6RiqTMw2Q==" + } + } + }, + "@aws-sdk/util-utf8-browser": { + "version": "3.259.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/util-utf8-browser/-/util-utf8-browser-3.259.0.tgz", + "integrity": "sha512-UvFa/vR+e19XookZF8RzFZBrw2EUkQWxiBW0yYQAhvk3C+QVGl0H3ouca8LDBlBfQKXwmW3huo/59H8rwb1wJw==", + "requires": { + "tslib": "^2.3.1" + }, + "dependencies": { + "tslib": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.5.0.tgz", + "integrity": "sha512-336iVw3rtn2BUK7ORdIAHTyxHGRIHVReokCR3XjbckJMK7ms8FysBfhLR8IXnAgy7T0PTPNBWKiH514FOW/WSg==" + } + } + }, + "@aws-sdk/xml-builder": { + "version": "3.310.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/xml-builder/-/xml-builder-3.310.0.tgz", + "integrity": "sha512-TqELu4mOuSIKQCqj63fGVs86Yh+vBx5nHRpWKNUNhB2nPTpfbziTs5c1X358be3peVWA4wPxW7Nt53KIg1tnNw==", + "requires": { + "tslib": "^2.5.0" + }, + "dependencies": { + "tslib": { + "version": "2.5.2", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.5.2.tgz", + "integrity": "sha512-5svOrSA2w3iGFDs1HibEVBGbDrAY82bFQ3HZ3ixB+88nsbsWQoKqDRb5UBYAUPEzbBn6dAp5gRNXglySbx1MlA==" + } + } + }, + "@babel/code-frame": { + "version": "7.22.13", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.22.13.tgz", + "integrity": "sha512-XktuhWlJ5g+3TJXc5upd9Ks1HutSArik6jf2eAjYFyIOf4ej3RN+184cZbzDvbPnuTJIUhPKKJE3cIsYTiAT3w==", + "dev": true, + "requires": { + "@babel/highlight": "^7.22.13", + "chalk": "^2.4.2" + }, + "dependencies": { + "chalk": { + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", + "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==", + "dev": true, + "requires": { + "ansi-styles": "^3.2.1", + "escape-string-regexp": "^1.0.5", + "supports-color": "^5.3.0" + } + } + } + }, + "@babel/compat-data": { + "version": "7.22.20", + "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.22.20.tgz", + "integrity": "sha512-BQYjKbpXjoXwFW5jGqiizJQQT/aC7pFm9Ok1OWssonuguICi264lbgMzRp2ZMmRSlfkX6DsWDDcsrctK8Rwfiw==", + "dev": true + }, + "@babel/core": { + "version": "7.23.0", + "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.23.0.tgz", + "integrity": "sha512-97z/ju/Jy1rZmDxybphrBuI+jtJjFVoz7Mr9yUQVVVi+DNZE333uFQeMOqcCIy1x3WYBIbWftUSLmbNXNT7qFQ==", + "dev": true, + "requires": { + "@ampproject/remapping": "^2.2.0", + "@babel/code-frame": "^7.22.13", + "@babel/generator": "^7.23.0", + "@babel/helper-compilation-targets": "^7.22.15", + "@babel/helper-module-transforms": "^7.23.0", + "@babel/helpers": "^7.23.0", + "@babel/parser": "^7.23.0", + "@babel/template": "^7.22.15", + "@babel/traverse": "^7.23.0", + "@babel/types": "^7.23.0", + "convert-source-map": "^2.0.0", + "debug": "^4.1.0", + "gensync": "^1.0.0-beta.2", + "json5": "^2.2.3", + "semver": "^6.3.1" + } + }, + "@babel/generator": { + "version": "7.23.0", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.23.0.tgz", + "integrity": "sha512-lN85QRR+5IbYrMWM6Y4pE/noaQtg4pNiqeNGX60eqOfo6gtEj6uw/JagelB8vVztSd7R6M5n1+PQkDbHbBRU4g==", + "dev": true, + "requires": { + "@babel/types": "^7.23.0", + "@jridgewell/gen-mapping": "^0.3.2", + "@jridgewell/trace-mapping": "^0.3.17", + "jsesc": "^2.5.1" + } + }, + "@babel/helper-compilation-targets": { + "version": "7.22.15", + "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.22.15.tgz", + "integrity": "sha512-y6EEzULok0Qvz8yyLkCvVX+02ic+By2UdOhylwUOvOn9dvYc9mKICJuuU1n1XBI02YWsNsnrY1kc6DVbjcXbtw==", + "dev": true, + "requires": { + "@babel/compat-data": "^7.22.9", + "@babel/helper-validator-option": "^7.22.15", + "browserslist": "^4.21.9", + "lru-cache": "^5.1.1", + "semver": "^6.3.1" + }, + "dependencies": { + "lru-cache": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz", + "integrity": "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==", + "dev": true, + "requires": { + "yallist": "^3.0.2" + } + }, + "yallist": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz", + "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==", + "dev": true + } + } + }, + "@babel/helper-environment-visitor": { + "version": "7.22.20", + "resolved": "https://registry.npmjs.org/@babel/helper-environment-visitor/-/helper-environment-visitor-7.22.20.tgz", + "integrity": "sha512-zfedSIzFhat/gFhWfHtgWvlec0nqB9YEIVrpuwjruLlXfUSnA8cJB0miHKwqDnQ7d32aKo2xt88/xZptwxbfhA==", + "dev": true + }, + "@babel/helper-function-name": { + "version": "7.23.0", + "resolved": "https://registry.npmjs.org/@babel/helper-function-name/-/helper-function-name-7.23.0.tgz", + "integrity": "sha512-OErEqsrxjZTJciZ4Oo+eoZqeW9UIiOcuYKRJA4ZAgV9myA+pOXhhmpfNCKjEH/auVfEYVFJ6y1Tc4r0eIApqiw==", + "dev": true, + "requires": { + "@babel/template": "^7.22.15", + "@babel/types": "^7.23.0" + } + }, + "@babel/helper-hoist-variables": { + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/helper-hoist-variables/-/helper-hoist-variables-7.22.5.tgz", + "integrity": "sha512-wGjk9QZVzvknA6yKIUURb8zY3grXCcOZt+/7Wcy8O2uctxhplmUPkOdlgoNhmdVee2c92JXbf1xpMtVNbfoxRw==", + "dev": true, + "requires": { + "@babel/types": "^7.22.5" + } + }, + "@babel/helper-module-imports": { + "version": "7.22.15", + "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.22.15.tgz", + "integrity": "sha512-0pYVBnDKZO2fnSPCrgM/6WMc7eS20Fbok+0r88fp+YtWVLZrp4CkafFGIp+W0VKw4a22sgebPT99y+FDNMdP4w==", + "dev": true, + "requires": { + "@babel/types": "^7.22.15" + } + }, + "@babel/helper-module-transforms": { + "version": "7.23.0", + "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.23.0.tgz", + "integrity": "sha512-WhDWw1tdrlT0gMgUJSlX0IQvoO1eN279zrAUbVB+KpV2c3Tylz8+GnKOLllCS6Z/iZQEyVYxhZVUdPTqs2YYPw==", + "dev": true, + "requires": { + "@babel/helper-environment-visitor": "^7.22.20", + "@babel/helper-module-imports": "^7.22.15", + "@babel/helper-simple-access": "^7.22.5", + "@babel/helper-split-export-declaration": "^7.22.6", + "@babel/helper-validator-identifier": "^7.22.20" + } + }, + "@babel/helper-plugin-utils": { + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.22.5.tgz", + "integrity": "sha512-uLls06UVKgFG9QD4OeFYLEGteMIAa5kpTPcFL28yuCIIzsf6ZyKZMllKVOCZFhiZ5ptnwX4mtKdWCBE/uT4amg==", + "dev": true + }, + "@babel/helper-simple-access": { + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/helper-simple-access/-/helper-simple-access-7.22.5.tgz", + "integrity": "sha512-n0H99E/K+Bika3++WNL17POvo4rKWZ7lZEp1Q+fStVbUi8nxPQEBOlTmCOxW/0JsS56SKKQ+ojAe2pHKJHN35w==", + "dev": true, + "requires": { + "@babel/types": "^7.22.5" + } + }, + "@babel/helper-split-export-declaration": { + "version": "7.22.6", + "resolved": "https://registry.npmjs.org/@babel/helper-split-export-declaration/-/helper-split-export-declaration-7.22.6.tgz", + "integrity": "sha512-AsUnxuLhRYsisFiaJwvp1QF+I3KjD5FOxut14q/GzovUe6orHLesW2C7d754kRm53h5gqrz6sFl6sxc4BVtE/g==", + "dev": true, + "requires": { + "@babel/types": "^7.22.5" + } + }, + "@babel/helper-string-parser": { + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.22.5.tgz", + "integrity": "sha512-mM4COjgZox8U+JcXQwPijIZLElkgEpO5rsERVDJTc2qfCDfERyob6k5WegS14SX18IIjv+XD+GrqNumY5JRCDw==", + "dev": true + }, + "@babel/helper-validator-identifier": { + "version": "7.22.20", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.22.20.tgz", + "integrity": "sha512-Y4OZ+ytlatR8AI+8KZfKuL5urKp7qey08ha31L8b3BwewJAoJamTzyvxPR/5D+KkdJCGPq/+8TukHBlY10FX9A==", + "dev": true + }, + "@babel/helper-validator-option": { + "version": "7.22.15", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.22.15.tgz", + "integrity": "sha512-bMn7RmyFjY/mdECUbgn9eoSY4vqvacUnS9i9vGAGttgFWesO6B4CYWA7XlpbWgBt71iv/hfbPlynohStqnu5hA==", + "dev": true + }, + "@babel/helpers": { + "version": "7.23.1", + "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.23.1.tgz", + "integrity": "sha512-chNpneuK18yW5Oxsr+t553UZzzAs3aZnFm4bxhebsNTeshrC95yA7l5yl7GBAG+JG1rF0F7zzD2EixK9mWSDoA==", + "dev": true, + "requires": { + "@babel/template": "^7.22.15", + "@babel/traverse": "^7.23.0", + "@babel/types": "^7.23.0" + } + }, + "@babel/highlight": { + "version": "7.22.20", + "resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.22.20.tgz", + "integrity": "sha512-dkdMCN3py0+ksCgYmGG8jKeGA/8Tk+gJwSYYlFGxG5lmhfKNoAy004YpLxpS1W2J8m/EK2Ew+yOs9pVRwO89mg==", + "dev": true, + "requires": { + "@babel/helper-validator-identifier": "^7.22.20", + "chalk": "^2.4.2", + "js-tokens": "^4.0.0" + }, + "dependencies": { + "chalk": { + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", + "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==", + "dev": true, + "requires": { + "ansi-styles": "^3.2.1", + "escape-string-regexp": "^1.0.5", + "supports-color": "^5.3.0" + } + } + } + }, + "@babel/parser": { + "version": "7.23.0", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.23.0.tgz", + "integrity": "sha512-vvPKKdMemU85V9WE/l5wZEmImpCtLqbnTvqDS2U1fJ96KrxoW7KrXhNsNCblQlg8Ck4b85yxdTyelsMUgFUXiw==", + "dev": true + }, + "@babel/plugin-syntax-async-generators": { + "version": "7.8.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-async-generators/-/plugin-syntax-async-generators-7.8.4.tgz", + "integrity": "sha512-tycmZxkGfZaxhMRbXlPXuVFpdWlXpir2W4AMhSJgRKzk/eDlIXOhb2LHWoLpDF7TEHylV5zNhykX6KAgHJmTNw==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.8.0" + } + }, + "@babel/plugin-syntax-bigint": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-bigint/-/plugin-syntax-bigint-7.8.3.tgz", + "integrity": "sha512-wnTnFlG+YxQm3vDxpGE57Pj0srRU4sHE/mDkt1qv2YJJSeUAec2ma4WLUnUPeKjyrfntVwe/N6dCXpU+zL3Npg==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.8.0" + } + }, + "@babel/plugin-syntax-class-properties": { + "version": "7.12.13", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-class-properties/-/plugin-syntax-class-properties-7.12.13.tgz", + "integrity": "sha512-fm4idjKla0YahUNgFNLCB0qySdsoPiZP3iQE3rky0mBUtMZ23yDJ9SJdg6dXTSDnulOVqiF3Hgr9nbXvXTQZYA==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.12.13" + } + }, + "@babel/plugin-syntax-import-meta": { + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-import-meta/-/plugin-syntax-import-meta-7.10.4.tgz", + "integrity": "sha512-Yqfm+XDx0+Prh3VSeEQCPU81yC+JWZ2pDPFSS4ZdpfZhp4MkFMaDC1UqseovEKwSUpnIL7+vK+Clp7bfh0iD7g==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.10.4" + } + }, + "@babel/plugin-syntax-json-strings": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-json-strings/-/plugin-syntax-json-strings-7.8.3.tgz", + "integrity": "sha512-lY6kdGpWHvjoe2vk4WrAapEuBR69EMxZl+RoGRhrFGNYVK8mOPAW8VfbT/ZgrFbXlDNiiaxQnAtgVCZ6jv30EA==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.8.0" + } + }, + "@babel/plugin-syntax-jsx": { + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-jsx/-/plugin-syntax-jsx-7.22.5.tgz", + "integrity": "sha512-gvyP4hZrgrs/wWMaocvxZ44Hw0b3W8Pe+cMxc8V1ULQ07oh8VNbIRaoD1LRZVTvD+0nieDKjfgKg89sD7rrKrg==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.22.5" + } + }, + "@babel/plugin-syntax-logical-assignment-operators": { + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-logical-assignment-operators/-/plugin-syntax-logical-assignment-operators-7.10.4.tgz", + "integrity": "sha512-d8waShlpFDinQ5MtvGU9xDAOzKH47+FFoney2baFIoMr952hKOLp1HR7VszoZvOsV/4+RRszNY7D17ba0te0ig==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.10.4" + } + }, + "@babel/plugin-syntax-nullish-coalescing-operator": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-nullish-coalescing-operator/-/plugin-syntax-nullish-coalescing-operator-7.8.3.tgz", + "integrity": "sha512-aSff4zPII1u2QD7y+F8oDsz19ew4IGEJg9SVW+bqwpwtfFleiQDMdzA/R+UlWDzfnHFCxxleFT0PMIrR36XLNQ==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.8.0" + } + }, + "@babel/plugin-syntax-numeric-separator": { + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-numeric-separator/-/plugin-syntax-numeric-separator-7.10.4.tgz", + "integrity": "sha512-9H6YdfkcK/uOnY/K7/aA2xpzaAgkQn37yzWUMRK7OaPOqOpGS1+n0H5hxT9AUw9EsSjPW8SVyMJwYRtWs3X3ug==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.10.4" + } + }, + "@babel/plugin-syntax-object-rest-spread": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-object-rest-spread/-/plugin-syntax-object-rest-spread-7.8.3.tgz", + "integrity": "sha512-XoqMijGZb9y3y2XskN+P1wUGiVwWZ5JmoDRwx5+3GmEplNyVM2s2Dg8ILFQm8rWM48orGy5YpI5Bl8U1y7ydlA==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.8.0" + } + }, + "@babel/plugin-syntax-optional-catch-binding": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-optional-catch-binding/-/plugin-syntax-optional-catch-binding-7.8.3.tgz", + "integrity": "sha512-6VPD0Pc1lpTqw0aKoeRTMiB+kWhAoT24PA+ksWSBrFtl5SIRVpZlwN3NNPQjehA2E/91FV3RjLWoVTglWcSV3Q==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.8.0" + } + }, + "@babel/plugin-syntax-optional-chaining": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-optional-chaining/-/plugin-syntax-optional-chaining-7.8.3.tgz", + "integrity": "sha512-KoK9ErH1MBlCPxV0VANkXW2/dw4vlbGDrFgz8bmUsBGYkFRcbRwMh6cIJubdPrkxRwuGdtCk0v/wPTKbQgBjkg==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.8.0" + } + }, + "@babel/plugin-syntax-top-level-await": { + "version": "7.14.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-top-level-await/-/plugin-syntax-top-level-await-7.14.5.tgz", + "integrity": "sha512-hx++upLv5U1rgYfwe1xBQUhRmU41NEvpUvrp8jkrSCdvGSnM5/qdRMtylJ6PG5OFkBaHkbTAKTnd3/YyESRHFw==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.14.5" + } + }, + "@babel/plugin-syntax-typescript": { + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-typescript/-/plugin-syntax-typescript-7.22.5.tgz", + "integrity": "sha512-1mS2o03i7t1c6VzH6fdQ3OA8tcEIxwG18zIPRp+UY1Ihv6W+XZzBCVxExF9upussPXJ0xE9XRHwMoNs1ep/nRQ==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.22.5" + } + }, + "@babel/runtime": { + "version": "7.22.0", + "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.22.0.tgz", + "integrity": "sha512-TT6NB0oszYQ4oxLNUdG+FNHIc3MohXVCKA2BeyQ4WeM2VCSC6wBZ6P0Yfkdzxv+87D8Xk0LJyHeCKlWMvpZt0g==", + "requires": { + "regenerator-runtime": "^0.13.11" + } + }, + "@babel/template": { + "version": "7.22.15", + "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.22.15.tgz", + "integrity": "sha512-QPErUVm4uyJa60rkI73qneDacvdvzxshT3kksGqlGWYdOTIUOwJ7RDUL8sGqslY1uXWSL6xMFKEXDS3ox2uF0w==", + "dev": true, + "requires": { + "@babel/code-frame": "^7.22.13", + "@babel/parser": "^7.22.15", + "@babel/types": "^7.22.15" + } + }, + "@babel/traverse": { + "version": "7.23.0", + "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.23.0.tgz", + "integrity": "sha512-t/QaEvyIoIkwzpiZ7aoSKK8kObQYeF7T2v+dazAYCb8SXtp58zEVkWW7zAnju8FNKNdr4ScAOEDmMItbyOmEYw==", + "dev": true, + "requires": { + "@babel/code-frame": "^7.22.13", + "@babel/generator": "^7.23.0", + "@babel/helper-environment-visitor": "^7.22.20", + "@babel/helper-function-name": "^7.23.0", + "@babel/helper-hoist-variables": "^7.22.5", + "@babel/helper-split-export-declaration": "^7.22.6", + "@babel/parser": "^7.23.0", + "@babel/types": "^7.23.0", + "debug": "^4.1.0", + "globals": "^11.1.0" + }, + "dependencies": { + "globals": { + "version": "11.12.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-11.12.0.tgz", + "integrity": "sha512-WOBp/EEGUiIsJSp7wcv/y6MO+lV9UoncWqxuFfm8eBwzWNgyfBd6Gz+IeKQ9jCmyhoH99g15M3T+QaVHFjizVA==", + "dev": true + } + } + }, + "@babel/types": { + "version": "7.23.0", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.23.0.tgz", + "integrity": "sha512-0oIyUfKoI3mSqMvsxBdclDwxXKXAUA8v/apZbc+iSyARYou1o8ZGDxbUYyLFoW2arqS2jDGqJuZvv1d/io1axg==", + "dev": true, + "requires": { + "@babel/helper-string-parser": "^7.22.5", + "@babel/helper-validator-identifier": "^7.22.20", + "to-fast-properties": "^2.0.0" + } + }, + "@bcoe/v8-coverage": { + "version": "0.2.3", + "resolved": "https://registry.npmjs.org/@bcoe/v8-coverage/-/v8-coverage-0.2.3.tgz", + "integrity": "sha512-0hYQ8SB4Db5zvZB4axdMHGwEaQjkZzFjQiN9LVYvIFB2nSUHW9tYpxWriPrWDASIxiaXax83REcLxuSdnGPZtw==", + "dev": true + }, + "@colors/colors": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/@colors/colors/-/colors-1.5.0.tgz", + "integrity": "sha512-ooWCrlZP11i8GImSjTHYHLkvFDP48nS4+204nGb1RiX/WXYHmJA2III9/e2DWVabCESdW7hBAEzHRqUn9OUVvQ==" + }, + "@dabh/diagnostics": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/@dabh/diagnostics/-/diagnostics-2.0.2.tgz", + "integrity": "sha512-+A1YivoVDNNVCdfozHSR8v/jyuuLTMXwjWuxPFlFlUapXoGc+Gj9mDlTDDfrwl7rXCl2tNZ0kE8sIBO6YOn96Q==", + "requires": { + "colorspace": "1.1.x", + "enabled": "2.0.x", + "kuler": "^2.0.0" + } + }, + "@eslint-community/eslint-utils": { + "version": "4.4.0", + "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.4.0.tgz", + "integrity": "sha512-1/sA4dwrzBAyeUoQ6oxahHKmrZvsnLCg4RfxW3ZFGGmQkSNQPFNLV9CUEFQP1x9EYXHTo5p6xdhZM1Ne9p/AfA==", + "dev": true, + "requires": { + "eslint-visitor-keys": "^3.3.0" + }, + "dependencies": { + "eslint-visitor-keys": { + "version": "3.4.0", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.4.0.tgz", + "integrity": "sha512-HPpKPUBQcAsZOsHAFwTtIKcYlCje62XB7SEAcxjtmW6TD1WVpkS6i6/hOVtTZIl4zGj/mBqpFVGvaDneik+VoQ==", + "dev": true + } + } + }, + "@eslint-community/regexpp": { + "version": "4.6.2", + "resolved": "https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.6.2.tgz", + "integrity": "sha512-pPTNuaAG3QMH+buKyBIGJs3g/S5y0caxw0ygM3YyE6yJFySwiGGSzA+mM3KJ8QQvzeLh3blwgSonkFjgQdxzMw==", + "dev": true + }, + "@eslint/eslintrc": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-2.1.2.tgz", + "integrity": "sha512-+wvgpDsrB1YqAMdEUCcnTlpfVBH7Vqn6A/NT3D8WVXFIaKMlErPIZT3oCIAVCOtarRpMtelZLqJeU3t7WY6X6g==", + "dev": true, + "requires": { + "ajv": "^6.12.4", + "debug": "^4.3.2", + "espree": "^9.6.0", + "globals": "^13.19.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" + }, + "dependencies": { + "debug": { + "version": "4.3.4", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", + "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==", + "dev": true, + "requires": { + "ms": "2.1.2" + } + }, + "globals": { + "version": "13.21.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-13.21.0.tgz", + "integrity": "sha512-ybyme3s4yy/t/3s35bewwXKOf7cvzfreG2lH0lZl0JB7I4GxRP2ghxOK/Nb9EkRXdbBXZLfq/p/0W2JUONB/Gg==", + "dev": true, + "requires": { + "type-fest": "^0.20.2" + } + }, + "ms": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", + "dev": true + }, + "type-fest": { + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.20.2.tgz", + "integrity": "sha512-Ne+eE4r0/iWnpAxD852z3A+N0Bt5RN//NjJwRd2VFHEmrywxf5vsZlh4R6lixl6B+wz/8d+maTSAkN1FIkI3LQ==", + "dev": true + } + } + }, + "@eslint/js": { + "version": "8.50.0", + "resolved": "https://registry.npmjs.org/@eslint/js/-/js-8.50.0.tgz", + "integrity": "sha512-NCC3zz2+nvYd+Ckfh87rA47zfu2QsQpvc6k1yzTk+b9KzRj0wkGa8LSoGOXN6Zv4lRf/EIoZ80biDh9HOI+RNQ==", + "dev": true + }, + "@hapi/hoek": { + "version": "9.3.0", + "resolved": "https://registry.npmjs.org/@hapi/hoek/-/hoek-9.3.0.tgz", + "integrity": "sha512-/c6rf4UJlmHlC9b5BaNvzAcFv7HZ2QHaV0D4/HNlBdvFnvQq8RI4kYdhyPCl7Xj+oWvTWQ8ujhqS53LIgAe6KQ==" + }, + "@hapi/topo": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/@hapi/topo/-/topo-5.1.0.tgz", + "integrity": "sha512-foQZKJig7Ob0BMAYBfcJk8d77QtOe7Wo4ox7ff1lQYoNNAb6jwcY1ncdoy2e9wQZzvNy7ODZCYJkK8kzmcAnAg==", + "requires": { + "@hapi/hoek": "^9.0.0" + } + }, + "@humanwhocodes/config-array": { + "version": "0.11.11", + "resolved": "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.11.11.tgz", + "integrity": "sha512-N2brEuAadi0CcdeMXUkhbZB84eskAc8MEX1By6qEchoVywSgXPIjou4rYsl0V3Hj0ZnuGycGCjdNgockbzeWNA==", + "dev": true, + "requires": { + "@humanwhocodes/object-schema": "^1.2.1", + "debug": "^4.1.1", + "minimatch": "^3.0.5" + } + }, + "@humanwhocodes/module-importer": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@humanwhocodes/module-importer/-/module-importer-1.0.1.tgz", + "integrity": "sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==", + "dev": true + }, + "@humanwhocodes/object-schema": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/@humanwhocodes/object-schema/-/object-schema-1.2.1.tgz", + "integrity": "sha512-ZnQMnLV4e7hDlUvw8H+U8ASL02SS2Gn6+9Ac3wGGLIe7+je2AeAOxPY+izIPJDfFDb7eDjev0Us8MO1iFRN8hA==", + "dev": true + }, + "@istanbuljs/load-nyc-config": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@istanbuljs/load-nyc-config/-/load-nyc-config-1.1.0.tgz", + "integrity": "sha512-VjeHSlIzpv/NyD3N0YuHfXOPDIixcA1q2ZV98wsMqcYlPmv2n3Yb2lYP9XMElnaFVXg5A7YLTeLu6V84uQDjmQ==", + "dev": true, + "requires": { + "camelcase": "^5.3.1", + "find-up": "^4.1.0", + "get-package-type": "^0.1.0", + "js-yaml": "^3.13.1", + "resolve-from": "^5.0.0" + }, + "dependencies": { + "argparse": { + "version": "1.0.10", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-1.0.10.tgz", + "integrity": "sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==", + "dev": true, + "requires": { + "sprintf-js": "~1.0.2" + } + }, + "find-up": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-4.1.0.tgz", + "integrity": "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==", + "dev": true, + "requires": { + "locate-path": "^5.0.0", + "path-exists": "^4.0.0" + } + }, + "js-yaml": { + "version": "3.14.1", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.14.1.tgz", + "integrity": "sha512-okMH7OXXJ7YrN9Ok3/SXrnu4iX9yOk+25nqX4imS2npuvTYDmo/QEZoqwZkYaIDk3jVvBOTOIEgEhaLOynBS9g==", + "dev": true, + "requires": { + "argparse": "^1.0.7", + "esprima": "^4.0.0" + } + }, + "locate-path": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-5.0.0.tgz", + "integrity": "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==", + "dev": true, + "requires": { + "p-locate": "^4.1.0" + } + }, + "p-limit": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz", + "integrity": "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==", + "dev": true, + "requires": { + "p-try": "^2.0.0" + } + }, + "p-locate": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-4.1.0.tgz", + "integrity": "sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==", + "dev": true, + "requires": { + "p-limit": "^2.2.0" + } + }, + "p-try": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/p-try/-/p-try-2.2.0.tgz", + "integrity": "sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==", + "dev": true + }, + "path-exists": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", + "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", + "dev": true + }, + "resolve-from": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-5.0.0.tgz", + "integrity": "sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw==", + "dev": true + } + } + }, + "@istanbuljs/schema": { + "version": "0.1.3", + "resolved": "https://registry.npmjs.org/@istanbuljs/schema/-/schema-0.1.3.tgz", + "integrity": "sha512-ZXRY4jNvVgSVQ8DL3LTcakaAtXwTVUxE81hslsyD2AtoXW/wVob10HkOJ1X/pAlcI7D+2YoZKg5do8G/w6RYgA==", + "dev": true + }, + "@jest/console": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/console/-/console-29.7.0.tgz", + "integrity": "sha512-5Ni4CU7XHQi32IJ398EEP4RrB8eV09sXP2ROqD4bksHrnTree52PsxvX8tpL8LvTZ3pFzXyPbNQReSN41CAhOg==", + "dev": true, + "requires": { + "@jest/types": "^29.6.3", + "@types/node": "*", + "chalk": "^4.0.0", + "jest-message-util": "^29.7.0", + "jest-util": "^29.7.0", + "slash": "^3.0.0" + }, + "dependencies": { + "@jest/schemas": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/@jest/schemas/-/schemas-29.6.3.tgz", + "integrity": "sha512-mo5j5X+jIZmJQveBKeS/clAueipV7KgiX1vMgCxam1RNYiqE1w62n0/tJJnHtjW8ZHcQco5gY85jA3mi0L+nSA==", + "dev": true, + "requires": { + "@sinclair/typebox": "^0.27.8" + } + }, + "@jest/types": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/@jest/types/-/types-29.6.3.tgz", + "integrity": "sha512-u3UPsIilWKOM3F9CXtrG8LEJmNxwoCQC/XVj4IKYXvvpx7QIi/Kg1LI5uDmDpKlac62NUtX7eLjRh+jVZcLOzw==", + "dev": true, + "requires": { + "@jest/schemas": "^29.6.3", + "@types/istanbul-lib-coverage": "^2.0.0", + "@types/istanbul-reports": "^3.0.0", + "@types/node": "*", + "@types/yargs": "^17.0.8", + "chalk": "^4.0.0" + } + }, + "@sinclair/typebox": { + "version": "0.27.8", + "resolved": "https://registry.npmjs.org/@sinclair/typebox/-/typebox-0.27.8.tgz", + "integrity": "sha512-+Fj43pSMwJs4KRrH/938Uf+uAELIgVBmQzg/q1YG10djyfA3TnrU8N8XzqCh/okZdszqBQTZf96idMfE5lnwTA==", + "dev": true + }, + "ansi-styles": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz", + "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==", + "dev": true + }, + "jest-message-util": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-message-util/-/jest-message-util-29.7.0.tgz", + "integrity": "sha512-GBEV4GRADeP+qtB2+6u61stea8mGcOT4mCtrYISZwfu9/ISHFJ/5zOMXYbpBE9RsS5+Gb63DW4FgmnKJ79Kf6w==", + "dev": true, + "requires": { + "@babel/code-frame": "^7.12.13", + "@jest/types": "^29.6.3", + "@types/stack-utils": "^2.0.0", + "chalk": "^4.0.0", + "graceful-fs": "^4.2.9", + "micromatch": "^4.0.4", + "pretty-format": "^29.7.0", + "slash": "^3.0.0", + "stack-utils": "^2.0.3" + } + }, + "jest-util": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-util/-/jest-util-29.7.0.tgz", + "integrity": "sha512-z6EbKajIpqGKU56y5KBUgy1dt1ihhQJgWzUlZHArA/+X2ad7Cb5iF+AK1EWVL/Bo7Rz9uurpqw6SiBCefUbCGA==", + "dev": true, + "requires": { + "@jest/types": "^29.6.3", + "@types/node": "*", + "chalk": "^4.0.0", + "ci-info": "^3.2.0", + "graceful-fs": "^4.2.9", + "picomatch": "^2.2.3" + } + }, + "pretty-format": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-29.7.0.tgz", + "integrity": "sha512-Pdlw/oPxN+aXdmM9R00JVC9WVFoCLTKJvDVLgmJ+qAffBMxsV85l/Lu7sNx4zSzPyoL2euImuEwHhOXdEgNFZQ==", + "dev": true, + "requires": { + "@jest/schemas": "^29.6.3", + "ansi-styles": "^5.0.0", + "react-is": "^18.0.0" + } + }, + "react-is": { + "version": "18.2.0", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-18.2.0.tgz", + "integrity": "sha512-xWGDIW6x921xtzPkhiULtthJHoJvBbF3q26fzloPCK0hsvxtPVelvftw3zjbHWSkR2km9Z+4uxbDDK/6Zw9B8w==", + "dev": true + } + } + }, + "@jest/core": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/core/-/core-29.7.0.tgz", + "integrity": "sha512-n7aeXWKMnGtDA48y8TLWJPJmLmmZ642Ceo78cYWEpiD7FzDgmNDV/GCVRorPABdXLJZ/9wzzgZAlHjXjxDHGsg==", + "dev": true, + "requires": { + "@jest/console": "^29.7.0", + "@jest/reporters": "^29.7.0", + "@jest/test-result": "^29.7.0", + "@jest/transform": "^29.7.0", + "@jest/types": "^29.6.3", + "@types/node": "*", + "ansi-escapes": "^4.2.1", + "chalk": "^4.0.0", + "ci-info": "^3.2.0", + "exit": "^0.1.2", + "graceful-fs": "^4.2.9", + "jest-changed-files": "^29.7.0", + "jest-config": "^29.7.0", + "jest-haste-map": "^29.7.0", + "jest-message-util": "^29.7.0", + "jest-regex-util": "^29.6.3", + "jest-resolve": "^29.7.0", + "jest-resolve-dependencies": "^29.7.0", + "jest-runner": "^29.7.0", + "jest-runtime": "^29.7.0", + "jest-snapshot": "^29.7.0", + "jest-util": "^29.7.0", + "jest-validate": "^29.7.0", + "jest-watcher": "^29.7.0", + "micromatch": "^4.0.4", + "pretty-format": "^29.7.0", + "slash": "^3.0.0", + "strip-ansi": "^6.0.0" + }, + "dependencies": { + "@jest/schemas": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/@jest/schemas/-/schemas-29.6.3.tgz", + "integrity": "sha512-mo5j5X+jIZmJQveBKeS/clAueipV7KgiX1vMgCxam1RNYiqE1w62n0/tJJnHtjW8ZHcQco5gY85jA3mi0L+nSA==", + "dev": true, + "requires": { + "@sinclair/typebox": "^0.27.8" + } + }, + "@jest/types": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/@jest/types/-/types-29.6.3.tgz", + "integrity": "sha512-u3UPsIilWKOM3F9CXtrG8LEJmNxwoCQC/XVj4IKYXvvpx7QIi/Kg1LI5uDmDpKlac62NUtX7eLjRh+jVZcLOzw==", + "dev": true, + "requires": { + "@jest/schemas": "^29.6.3", + "@types/istanbul-lib-coverage": "^2.0.0", + "@types/istanbul-reports": "^3.0.0", + "@types/node": "*", + "@types/yargs": "^17.0.8", + "chalk": "^4.0.0" + } + }, + "@sinclair/typebox": { + "version": "0.27.8", + "resolved": "https://registry.npmjs.org/@sinclair/typebox/-/typebox-0.27.8.tgz", + "integrity": "sha512-+Fj43pSMwJs4KRrH/938Uf+uAELIgVBmQzg/q1YG10djyfA3TnrU8N8XzqCh/okZdszqBQTZf96idMfE5lnwTA==", + "dev": true + }, + "ansi-styles": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz", + "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==", + "dev": true + }, + "jest-message-util": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-message-util/-/jest-message-util-29.7.0.tgz", + "integrity": "sha512-GBEV4GRADeP+qtB2+6u61stea8mGcOT4mCtrYISZwfu9/ISHFJ/5zOMXYbpBE9RsS5+Gb63DW4FgmnKJ79Kf6w==", + "dev": true, + "requires": { + "@babel/code-frame": "^7.12.13", + "@jest/types": "^29.6.3", + "@types/stack-utils": "^2.0.0", + "chalk": "^4.0.0", + "graceful-fs": "^4.2.9", + "micromatch": "^4.0.4", + "pretty-format": "^29.7.0", + "slash": "^3.0.0", + "stack-utils": "^2.0.3" + } + }, + "jest-util": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-util/-/jest-util-29.7.0.tgz", + "integrity": "sha512-z6EbKajIpqGKU56y5KBUgy1dt1ihhQJgWzUlZHArA/+X2ad7Cb5iF+AK1EWVL/Bo7Rz9uurpqw6SiBCefUbCGA==", + "dev": true, + "requires": { + "@jest/types": "^29.6.3", + "@types/node": "*", + "chalk": "^4.0.0", + "ci-info": "^3.2.0", + "graceful-fs": "^4.2.9", + "picomatch": "^2.2.3" + } + }, + "pretty-format": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-29.7.0.tgz", + "integrity": "sha512-Pdlw/oPxN+aXdmM9R00JVC9WVFoCLTKJvDVLgmJ+qAffBMxsV85l/Lu7sNx4zSzPyoL2euImuEwHhOXdEgNFZQ==", + "dev": true, + "requires": { + "@jest/schemas": "^29.6.3", + "ansi-styles": "^5.0.0", + "react-is": "^18.0.0" + } + }, + "react-is": { + "version": "18.2.0", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-18.2.0.tgz", + "integrity": "sha512-xWGDIW6x921xtzPkhiULtthJHoJvBbF3q26fzloPCK0hsvxtPVelvftw3zjbHWSkR2km9Z+4uxbDDK/6Zw9B8w==", + "dev": true + } + } + }, + "@jest/environment": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/environment/-/environment-29.7.0.tgz", + "integrity": "sha512-aQIfHDq33ExsN4jP1NWGXhxgQ/wixs60gDiKO+XVMd8Mn0NWPWgc34ZQDTb2jKaUWQ7MuwoitXAsN2XVXNMpAw==", + "dev": true, + "requires": { + "@jest/fake-timers": "^29.7.0", + "@jest/types": "^29.6.3", + "@types/node": "*", + "jest-mock": "^29.7.0" + }, + "dependencies": { + "@jest/schemas": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/@jest/schemas/-/schemas-29.6.3.tgz", + "integrity": "sha512-mo5j5X+jIZmJQveBKeS/clAueipV7KgiX1vMgCxam1RNYiqE1w62n0/tJJnHtjW8ZHcQco5gY85jA3mi0L+nSA==", + "dev": true, + "requires": { + "@sinclair/typebox": "^0.27.8" + } + }, + "@jest/types": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/@jest/types/-/types-29.6.3.tgz", + "integrity": "sha512-u3UPsIilWKOM3F9CXtrG8LEJmNxwoCQC/XVj4IKYXvvpx7QIi/Kg1LI5uDmDpKlac62NUtX7eLjRh+jVZcLOzw==", + "dev": true, + "requires": { + "@jest/schemas": "^29.6.3", + "@types/istanbul-lib-coverage": "^2.0.0", + "@types/istanbul-reports": "^3.0.0", + "@types/node": "*", + "@types/yargs": "^17.0.8", + "chalk": "^4.0.0" + } + }, + "@sinclair/typebox": { + "version": "0.27.8", + "resolved": "https://registry.npmjs.org/@sinclair/typebox/-/typebox-0.27.8.tgz", + "integrity": "sha512-+Fj43pSMwJs4KRrH/938Uf+uAELIgVBmQzg/q1YG10djyfA3TnrU8N8XzqCh/okZdszqBQTZf96idMfE5lnwTA==", + "dev": true + } + } + }, + "@jest/expect": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/expect/-/expect-29.7.0.tgz", + "integrity": "sha512-8uMeAMycttpva3P1lBHB8VciS9V0XAr3GymPpipdyQXbBcuhkLQOSe8E/p92RyAdToS6ZD1tFkX+CkhoECE0dQ==", + "dev": true, + "requires": { + "expect": "^29.7.0", + "jest-snapshot": "^29.7.0" + }, + "dependencies": { + "@jest/expect-utils": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/expect-utils/-/expect-utils-29.7.0.tgz", + "integrity": "sha512-GlsNBWiFQFCVi9QVSx7f5AgMeLxe9YCCs5PuP2O2LdjDAA8Jh9eX7lA1Jq/xdXw3Wb3hyvlFNfZIfcRetSzYcA==", + "dev": true, + "requires": { + "jest-get-type": "^29.6.3" + } + }, + "@jest/schemas": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/@jest/schemas/-/schemas-29.6.3.tgz", + "integrity": "sha512-mo5j5X+jIZmJQveBKeS/clAueipV7KgiX1vMgCxam1RNYiqE1w62n0/tJJnHtjW8ZHcQco5gY85jA3mi0L+nSA==", + "dev": true, + "requires": { + "@sinclair/typebox": "^0.27.8" + } + }, + "@jest/types": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/@jest/types/-/types-29.6.3.tgz", + "integrity": "sha512-u3UPsIilWKOM3F9CXtrG8LEJmNxwoCQC/XVj4IKYXvvpx7QIi/Kg1LI5uDmDpKlac62NUtX7eLjRh+jVZcLOzw==", + "dev": true, + "requires": { + "@jest/schemas": "^29.6.3", + "@types/istanbul-lib-coverage": "^2.0.0", + "@types/istanbul-reports": "^3.0.0", + "@types/node": "*", + "@types/yargs": "^17.0.8", + "chalk": "^4.0.0" + } + }, + "@sinclair/typebox": { + "version": "0.27.8", + "resolved": "https://registry.npmjs.org/@sinclair/typebox/-/typebox-0.27.8.tgz", + "integrity": "sha512-+Fj43pSMwJs4KRrH/938Uf+uAELIgVBmQzg/q1YG10djyfA3TnrU8N8XzqCh/okZdszqBQTZf96idMfE5lnwTA==", + "dev": true + }, + "ansi-styles": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz", + "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==", + "dev": true + }, + "diff-sequences": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/diff-sequences/-/diff-sequences-29.6.3.tgz", + "integrity": "sha512-EjePK1srD3P08o2j4f0ExnylqRs5B9tJjcp9t1krH2qRi8CCdsYfwe9JgSLurFBWwq4uOlipzfk5fHNvwFKr8Q==", + "dev": true + }, + "expect": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/expect/-/expect-29.7.0.tgz", + "integrity": "sha512-2Zks0hf1VLFYI1kbh0I5jP3KHHyCHpkfyHBzsSXRFgl/Bg9mWYfMW8oD+PdMPlEwy5HNsR9JutYy6pMeOh61nw==", + "dev": true, + "requires": { + "@jest/expect-utils": "^29.7.0", + "jest-get-type": "^29.6.3", + "jest-matcher-utils": "^29.7.0", + "jest-message-util": "^29.7.0", + "jest-util": "^29.7.0" + } + }, + "jest-diff": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-diff/-/jest-diff-29.7.0.tgz", + "integrity": "sha512-LMIgiIrhigmPrs03JHpxUh2yISK3vLFPkAodPeo0+BuF7wA2FoQbkEg1u8gBYBThncu7e1oEDUfIXVuTqLRUjw==", + "dev": true, + "requires": { + "chalk": "^4.0.0", + "diff-sequences": "^29.6.3", + "jest-get-type": "^29.6.3", + "pretty-format": "^29.7.0" + } + }, + "jest-get-type": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/jest-get-type/-/jest-get-type-29.6.3.tgz", + "integrity": "sha512-zrteXnqYxfQh7l5FHyL38jL39di8H8rHoecLH3JNxH3BwOrBsNeabdap5e0I23lD4HHI8W5VFBZqG4Eaq5LNcw==", + "dev": true + }, + "jest-matcher-utils": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-matcher-utils/-/jest-matcher-utils-29.7.0.tgz", + "integrity": "sha512-sBkD+Xi9DtcChsI3L3u0+N0opgPYnCRPtGcQYrgXmR+hmt/fYfWAL0xRXYU8eWOdfuLgBe0YCW3AFtnRLagq/g==", + "dev": true, + "requires": { + "chalk": "^4.0.0", + "jest-diff": "^29.7.0", + "jest-get-type": "^29.6.3", + "pretty-format": "^29.7.0" + } + }, + "jest-message-util": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-message-util/-/jest-message-util-29.7.0.tgz", + "integrity": "sha512-GBEV4GRADeP+qtB2+6u61stea8mGcOT4mCtrYISZwfu9/ISHFJ/5zOMXYbpBE9RsS5+Gb63DW4FgmnKJ79Kf6w==", + "dev": true, + "requires": { + "@babel/code-frame": "^7.12.13", + "@jest/types": "^29.6.3", + "@types/stack-utils": "^2.0.0", + "chalk": "^4.0.0", + "graceful-fs": "^4.2.9", + "micromatch": "^4.0.4", + "pretty-format": "^29.7.0", + "slash": "^3.0.0", + "stack-utils": "^2.0.3" + } + }, + "jest-util": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-util/-/jest-util-29.7.0.tgz", + "integrity": "sha512-z6EbKajIpqGKU56y5KBUgy1dt1ihhQJgWzUlZHArA/+X2ad7Cb5iF+AK1EWVL/Bo7Rz9uurpqw6SiBCefUbCGA==", + "dev": true, + "requires": { + "@jest/types": "^29.6.3", + "@types/node": "*", + "chalk": "^4.0.0", + "ci-info": "^3.2.0", + "graceful-fs": "^4.2.9", + "picomatch": "^2.2.3" + } + }, + "pretty-format": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-29.7.0.tgz", + "integrity": "sha512-Pdlw/oPxN+aXdmM9R00JVC9WVFoCLTKJvDVLgmJ+qAffBMxsV85l/Lu7sNx4zSzPyoL2euImuEwHhOXdEgNFZQ==", + "dev": true, + "requires": { + "@jest/schemas": "^29.6.3", + "ansi-styles": "^5.0.0", + "react-is": "^18.0.0" + } + }, + "react-is": { + "version": "18.2.0", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-18.2.0.tgz", + "integrity": "sha512-xWGDIW6x921xtzPkhiULtthJHoJvBbF3q26fzloPCK0hsvxtPVelvftw3zjbHWSkR2km9Z+4uxbDDK/6Zw9B8w==", + "dev": true + } + } + }, + "@jest/expect-utils": { + "version": "28.1.3", + "resolved": "https://registry.npmjs.org/@jest/expect-utils/-/expect-utils-28.1.3.tgz", + "integrity": "sha512-wvbi9LUrHJLn3NlDW6wF2hvIMtd4JUl2QNVrjq+IBSHirgfrR3o9RnVtxzdEGO2n9JyIWwHnLfby5KzqBGg2YA==", + "dev": true, + "requires": { + "jest-get-type": "^28.0.2" + } + }, + "@jest/fake-timers": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/fake-timers/-/fake-timers-29.7.0.tgz", + "integrity": "sha512-q4DH1Ha4TTFPdxLsqDXK1d3+ioSL7yL5oCMJZgDYm6i+6CygW5E5xVr/D1HdsGxjt1ZWSfUAs9OxSB/BNelWrQ==", + "dev": true, + "requires": { + "@jest/types": "^29.6.3", + "@sinonjs/fake-timers": "^10.0.2", + "@types/node": "*", + "jest-message-util": "^29.7.0", + "jest-mock": "^29.7.0", + "jest-util": "^29.7.0" + }, + "dependencies": { + "@jest/schemas": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/@jest/schemas/-/schemas-29.6.3.tgz", + "integrity": "sha512-mo5j5X+jIZmJQveBKeS/clAueipV7KgiX1vMgCxam1RNYiqE1w62n0/tJJnHtjW8ZHcQco5gY85jA3mi0L+nSA==", + "dev": true, + "requires": { + "@sinclair/typebox": "^0.27.8" + } + }, + "@jest/types": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/@jest/types/-/types-29.6.3.tgz", + "integrity": "sha512-u3UPsIilWKOM3F9CXtrG8LEJmNxwoCQC/XVj4IKYXvvpx7QIi/Kg1LI5uDmDpKlac62NUtX7eLjRh+jVZcLOzw==", + "dev": true, + "requires": { + "@jest/schemas": "^29.6.3", + "@types/istanbul-lib-coverage": "^2.0.0", + "@types/istanbul-reports": "^3.0.0", + "@types/node": "*", + "@types/yargs": "^17.0.8", + "chalk": "^4.0.0" + } + }, + "@sinclair/typebox": { + "version": "0.27.8", + "resolved": "https://registry.npmjs.org/@sinclair/typebox/-/typebox-0.27.8.tgz", + "integrity": "sha512-+Fj43pSMwJs4KRrH/938Uf+uAELIgVBmQzg/q1YG10djyfA3TnrU8N8XzqCh/okZdszqBQTZf96idMfE5lnwTA==", + "dev": true + }, + "@sinonjs/commons": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/@sinonjs/commons/-/commons-3.0.0.tgz", + "integrity": "sha512-jXBtWAF4vmdNmZgD5FoKsVLv3rPgDnLgPbU84LIJ3otV44vJlDRokVng5v8NFJdCf/da9legHcKaRuZs4L7faA==", + "dev": true, + "requires": { + "type-detect": "4.0.8" + } + }, + "@sinonjs/fake-timers": { + "version": "10.3.0", + "resolved": "https://registry.npmjs.org/@sinonjs/fake-timers/-/fake-timers-10.3.0.tgz", + "integrity": "sha512-V4BG07kuYSUkTCSBHG8G8TNhM+F19jXFWnQtzj+we8DrkpSBCee9Z3Ms8yiGer/dlmhe35/Xdgyo3/0rQKg7YA==", + "dev": true, + "requires": { + "@sinonjs/commons": "^3.0.0" + } + }, + "ansi-styles": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz", + "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==", + "dev": true + }, + "jest-message-util": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-message-util/-/jest-message-util-29.7.0.tgz", + "integrity": "sha512-GBEV4GRADeP+qtB2+6u61stea8mGcOT4mCtrYISZwfu9/ISHFJ/5zOMXYbpBE9RsS5+Gb63DW4FgmnKJ79Kf6w==", + "dev": true, + "requires": { + "@babel/code-frame": "^7.12.13", + "@jest/types": "^29.6.3", + "@types/stack-utils": "^2.0.0", + "chalk": "^4.0.0", + "graceful-fs": "^4.2.9", + "micromatch": "^4.0.4", + "pretty-format": "^29.7.0", + "slash": "^3.0.0", + "stack-utils": "^2.0.3" + } + }, + "jest-util": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-util/-/jest-util-29.7.0.tgz", + "integrity": "sha512-z6EbKajIpqGKU56y5KBUgy1dt1ihhQJgWzUlZHArA/+X2ad7Cb5iF+AK1EWVL/Bo7Rz9uurpqw6SiBCefUbCGA==", + "dev": true, + "requires": { + "@jest/types": "^29.6.3", + "@types/node": "*", + "chalk": "^4.0.0", + "ci-info": "^3.2.0", + "graceful-fs": "^4.2.9", + "picomatch": "^2.2.3" + } + }, + "pretty-format": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-29.7.0.tgz", + "integrity": "sha512-Pdlw/oPxN+aXdmM9R00JVC9WVFoCLTKJvDVLgmJ+qAffBMxsV85l/Lu7sNx4zSzPyoL2euImuEwHhOXdEgNFZQ==", + "dev": true, + "requires": { + "@jest/schemas": "^29.6.3", + "ansi-styles": "^5.0.0", + "react-is": "^18.0.0" + } + }, + "react-is": { + "version": "18.2.0", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-18.2.0.tgz", + "integrity": "sha512-xWGDIW6x921xtzPkhiULtthJHoJvBbF3q26fzloPCK0hsvxtPVelvftw3zjbHWSkR2km9Z+4uxbDDK/6Zw9B8w==", + "dev": true + } + } + }, + "@jest/globals": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/globals/-/globals-29.7.0.tgz", + "integrity": "sha512-mpiz3dutLbkW2MNFubUGUEVLkTGiqW6yLVTA+JbP6fI6J5iL9Y0Nlg8k95pcF8ctKwCS7WVxteBs29hhfAotzQ==", + "dev": true, + "requires": { + "@jest/environment": "^29.7.0", + "@jest/expect": "^29.7.0", + "@jest/types": "^29.6.3", + "jest-mock": "^29.7.0" + }, + "dependencies": { + "@jest/schemas": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/@jest/schemas/-/schemas-29.6.3.tgz", + "integrity": "sha512-mo5j5X+jIZmJQveBKeS/clAueipV7KgiX1vMgCxam1RNYiqE1w62n0/tJJnHtjW8ZHcQco5gY85jA3mi0L+nSA==", + "dev": true, + "requires": { + "@sinclair/typebox": "^0.27.8" + } + }, + "@jest/types": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/@jest/types/-/types-29.6.3.tgz", + "integrity": "sha512-u3UPsIilWKOM3F9CXtrG8LEJmNxwoCQC/XVj4IKYXvvpx7QIi/Kg1LI5uDmDpKlac62NUtX7eLjRh+jVZcLOzw==", + "dev": true, + "requires": { + "@jest/schemas": "^29.6.3", + "@types/istanbul-lib-coverage": "^2.0.0", + "@types/istanbul-reports": "^3.0.0", + "@types/node": "*", + "@types/yargs": "^17.0.8", + "chalk": "^4.0.0" + } + }, + "@sinclair/typebox": { + "version": "0.27.8", + "resolved": "https://registry.npmjs.org/@sinclair/typebox/-/typebox-0.27.8.tgz", + "integrity": "sha512-+Fj43pSMwJs4KRrH/938Uf+uAELIgVBmQzg/q1YG10djyfA3TnrU8N8XzqCh/okZdszqBQTZf96idMfE5lnwTA==", + "dev": true + } + } + }, + "@jest/reporters": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/reporters/-/reporters-29.7.0.tgz", + "integrity": "sha512-DApq0KJbJOEzAFYjHADNNxAE3KbhxQB1y5Kplb5Waqw6zVbuWatSnMjE5gs8FUgEPmNsnZA3NCWl9NG0ia04Pg==", + "dev": true, + "requires": { + "@bcoe/v8-coverage": "^0.2.3", + "@jest/console": "^29.7.0", + "@jest/test-result": "^29.7.0", + "@jest/transform": "^29.7.0", + "@jest/types": "^29.6.3", + "@jridgewell/trace-mapping": "^0.3.18", + "@types/node": "*", + "chalk": "^4.0.0", + "collect-v8-coverage": "^1.0.0", + "exit": "^0.1.2", + "glob": "^7.1.3", + "graceful-fs": "^4.2.9", + "istanbul-lib-coverage": "^3.0.0", + "istanbul-lib-instrument": "^6.0.0", + "istanbul-lib-report": "^3.0.0", + "istanbul-lib-source-maps": "^4.0.0", + "istanbul-reports": "^3.1.3", + "jest-message-util": "^29.7.0", + "jest-util": "^29.7.0", + "jest-worker": "^29.7.0", + "slash": "^3.0.0", + "string-length": "^4.0.1", + "strip-ansi": "^6.0.0", + "v8-to-istanbul": "^9.0.1" + }, + "dependencies": { + "@jest/schemas": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/@jest/schemas/-/schemas-29.6.3.tgz", + "integrity": "sha512-mo5j5X+jIZmJQveBKeS/clAueipV7KgiX1vMgCxam1RNYiqE1w62n0/tJJnHtjW8ZHcQco5gY85jA3mi0L+nSA==", + "dev": true, + "requires": { + "@sinclair/typebox": "^0.27.8" + } + }, + "@jest/types": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/@jest/types/-/types-29.6.3.tgz", + "integrity": "sha512-u3UPsIilWKOM3F9CXtrG8LEJmNxwoCQC/XVj4IKYXvvpx7QIi/Kg1LI5uDmDpKlac62NUtX7eLjRh+jVZcLOzw==", + "dev": true, + "requires": { + "@jest/schemas": "^29.6.3", + "@types/istanbul-lib-coverage": "^2.0.0", + "@types/istanbul-reports": "^3.0.0", + "@types/node": "*", + "@types/yargs": "^17.0.8", + "chalk": "^4.0.0" + } + }, + "@sinclair/typebox": { + "version": "0.27.8", + "resolved": "https://registry.npmjs.org/@sinclair/typebox/-/typebox-0.27.8.tgz", + "integrity": "sha512-+Fj43pSMwJs4KRrH/938Uf+uAELIgVBmQzg/q1YG10djyfA3TnrU8N8XzqCh/okZdszqBQTZf96idMfE5lnwTA==", + "dev": true + }, + "ansi-styles": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz", + "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==", + "dev": true + }, + "jest-message-util": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-message-util/-/jest-message-util-29.7.0.tgz", + "integrity": "sha512-GBEV4GRADeP+qtB2+6u61stea8mGcOT4mCtrYISZwfu9/ISHFJ/5zOMXYbpBE9RsS5+Gb63DW4FgmnKJ79Kf6w==", + "dev": true, + "requires": { + "@babel/code-frame": "^7.12.13", + "@jest/types": "^29.6.3", + "@types/stack-utils": "^2.0.0", + "chalk": "^4.0.0", + "graceful-fs": "^4.2.9", + "micromatch": "^4.0.4", + "pretty-format": "^29.7.0", + "slash": "^3.0.0", + "stack-utils": "^2.0.3" + } + }, + "jest-util": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-util/-/jest-util-29.7.0.tgz", + "integrity": "sha512-z6EbKajIpqGKU56y5KBUgy1dt1ihhQJgWzUlZHArA/+X2ad7Cb5iF+AK1EWVL/Bo7Rz9uurpqw6SiBCefUbCGA==", + "dev": true, + "requires": { + "@jest/types": "^29.6.3", + "@types/node": "*", + "chalk": "^4.0.0", + "ci-info": "^3.2.0", + "graceful-fs": "^4.2.9", + "picomatch": "^2.2.3" + } + }, + "pretty-format": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-29.7.0.tgz", + "integrity": "sha512-Pdlw/oPxN+aXdmM9R00JVC9WVFoCLTKJvDVLgmJ+qAffBMxsV85l/Lu7sNx4zSzPyoL2euImuEwHhOXdEgNFZQ==", + "dev": true, + "requires": { + "@jest/schemas": "^29.6.3", + "ansi-styles": "^5.0.0", + "react-is": "^18.0.0" + } + }, + "react-is": { + "version": "18.2.0", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-18.2.0.tgz", + "integrity": "sha512-xWGDIW6x921xtzPkhiULtthJHoJvBbF3q26fzloPCK0hsvxtPVelvftw3zjbHWSkR2km9Z+4uxbDDK/6Zw9B8w==", + "dev": true + } + } + }, + "@jest/schemas": { + "version": "28.1.3", + "resolved": "https://registry.npmjs.org/@jest/schemas/-/schemas-28.1.3.tgz", + "integrity": "sha512-/l/VWsdt/aBXgjshLWOFyFt3IVdYypu5y2Wn2rOO1un6nkqIn8SLXzgIMYXFyYsRWDyF5EthmKJMIdJvk08grg==", + "dev": true, + "requires": { + "@sinclair/typebox": "^0.24.1" + } + }, + "@jest/source-map": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/@jest/source-map/-/source-map-29.6.3.tgz", + "integrity": "sha512-MHjT95QuipcPrpLM+8JMSzFx6eHp5Bm+4XeFDJlwsvVBjmKNiIAvasGK2fxz2WbGRlnvqehFbh07MMa7n3YJnw==", + "dev": true, + "requires": { + "@jridgewell/trace-mapping": "^0.3.18", + "callsites": "^3.0.0", + "graceful-fs": "^4.2.9" + } + }, + "@jest/test-result": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/test-result/-/test-result-29.7.0.tgz", + "integrity": "sha512-Fdx+tv6x1zlkJPcWXmMDAG2HBnaR9XPSd5aDWQVsfrZmLVT3lU1cwyxLgRmXR9yrq4NBoEm9BMsfgFzTQAbJYA==", + "dev": true, + "requires": { + "@jest/console": "^29.7.0", + "@jest/types": "^29.6.3", + "@types/istanbul-lib-coverage": "^2.0.0", + "collect-v8-coverage": "^1.0.0" + }, + "dependencies": { + "@jest/schemas": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/@jest/schemas/-/schemas-29.6.3.tgz", + "integrity": "sha512-mo5j5X+jIZmJQveBKeS/clAueipV7KgiX1vMgCxam1RNYiqE1w62n0/tJJnHtjW8ZHcQco5gY85jA3mi0L+nSA==", + "dev": true, + "requires": { + "@sinclair/typebox": "^0.27.8" + } + }, + "@jest/types": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/@jest/types/-/types-29.6.3.tgz", + "integrity": "sha512-u3UPsIilWKOM3F9CXtrG8LEJmNxwoCQC/XVj4IKYXvvpx7QIi/Kg1LI5uDmDpKlac62NUtX7eLjRh+jVZcLOzw==", + "dev": true, + "requires": { + "@jest/schemas": "^29.6.3", + "@types/istanbul-lib-coverage": "^2.0.0", + "@types/istanbul-reports": "^3.0.0", + "@types/node": "*", + "@types/yargs": "^17.0.8", + "chalk": "^4.0.0" + } + }, + "@sinclair/typebox": { + "version": "0.27.8", + "resolved": "https://registry.npmjs.org/@sinclair/typebox/-/typebox-0.27.8.tgz", + "integrity": "sha512-+Fj43pSMwJs4KRrH/938Uf+uAELIgVBmQzg/q1YG10djyfA3TnrU8N8XzqCh/okZdszqBQTZf96idMfE5lnwTA==", + "dev": true + } + } + }, + "@jest/test-sequencer": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/test-sequencer/-/test-sequencer-29.7.0.tgz", + "integrity": "sha512-GQwJ5WZVrKnOJuiYiAF52UNUJXgTZx1NHjFSEB0qEMmSZKAkdMoIzw/Cj6x6NF4AvV23AUqDpFzQkN/eYCYTxw==", + "dev": true, + "requires": { + "@jest/test-result": "^29.7.0", + "graceful-fs": "^4.2.9", + "jest-haste-map": "^29.7.0", + "slash": "^3.0.0" + } + }, + "@jest/transform": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/transform/-/transform-29.7.0.tgz", + "integrity": "sha512-ok/BTPFzFKVMwO5eOHRrvnBVHdRy9IrsrW1GpMaQ9MCnilNLXQKmAX8s1YXDFaai9xJpac2ySzV0YeRRECr2Vw==", + "dev": true, + "requires": { + "@babel/core": "^7.11.6", + "@jest/types": "^29.6.3", + "@jridgewell/trace-mapping": "^0.3.18", + "babel-plugin-istanbul": "^6.1.1", + "chalk": "^4.0.0", + "convert-source-map": "^2.0.0", + "fast-json-stable-stringify": "^2.1.0", + "graceful-fs": "^4.2.9", + "jest-haste-map": "^29.7.0", + "jest-regex-util": "^29.6.3", + "jest-util": "^29.7.0", + "micromatch": "^4.0.4", + "pirates": "^4.0.4", + "slash": "^3.0.0", + "write-file-atomic": "^4.0.2" + }, + "dependencies": { + "@jest/schemas": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/@jest/schemas/-/schemas-29.6.3.tgz", + "integrity": "sha512-mo5j5X+jIZmJQveBKeS/clAueipV7KgiX1vMgCxam1RNYiqE1w62n0/tJJnHtjW8ZHcQco5gY85jA3mi0L+nSA==", + "dev": true, + "requires": { + "@sinclair/typebox": "^0.27.8" + } + }, + "@jest/types": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/@jest/types/-/types-29.6.3.tgz", + "integrity": "sha512-u3UPsIilWKOM3F9CXtrG8LEJmNxwoCQC/XVj4IKYXvvpx7QIi/Kg1LI5uDmDpKlac62NUtX7eLjRh+jVZcLOzw==", + "dev": true, + "requires": { + "@jest/schemas": "^29.6.3", + "@types/istanbul-lib-coverage": "^2.0.0", + "@types/istanbul-reports": "^3.0.0", + "@types/node": "*", + "@types/yargs": "^17.0.8", + "chalk": "^4.0.0" + } + }, + "@sinclair/typebox": { + "version": "0.27.8", + "resolved": "https://registry.npmjs.org/@sinclair/typebox/-/typebox-0.27.8.tgz", + "integrity": "sha512-+Fj43pSMwJs4KRrH/938Uf+uAELIgVBmQzg/q1YG10djyfA3TnrU8N8XzqCh/okZdszqBQTZf96idMfE5lnwTA==", + "dev": true + }, + "jest-util": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-util/-/jest-util-29.7.0.tgz", + "integrity": "sha512-z6EbKajIpqGKU56y5KBUgy1dt1ihhQJgWzUlZHArA/+X2ad7Cb5iF+AK1EWVL/Bo7Rz9uurpqw6SiBCefUbCGA==", + "dev": true, + "requires": { + "@jest/types": "^29.6.3", + "@types/node": "*", + "chalk": "^4.0.0", + "ci-info": "^3.2.0", + "graceful-fs": "^4.2.9", + "picomatch": "^2.2.3" + } + } + } + }, + "@jest/types": { + "version": "28.1.3", + "resolved": "https://registry.npmjs.org/@jest/types/-/types-28.1.3.tgz", + "integrity": "sha512-RyjiyMUZrKz/c+zlMFO1pm70DcIlST8AeWTkoUdZevew44wcNZQHsEVOiCVtgVnlFFD82FPaXycys58cf2muVQ==", + "dev": true, + "requires": { + "@jest/schemas": "^28.1.3", + "@types/istanbul-lib-coverage": "^2.0.0", + "@types/istanbul-reports": "^3.0.0", + "@types/node": "*", + "@types/yargs": "^17.0.8", + "chalk": "^4.0.0" + } + }, + "@jridgewell/gen-mapping": { + "version": "0.3.3", + "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.3.tgz", + "integrity": "sha512-HLhSWOLRi875zjjMG/r+Nv0oCW8umGb0BgEhyX3dDX3egwZtB8PqLnjz3yedt8R5StBrzcg4aBpnh8UA9D1BoQ==", + "dev": true, + "requires": { + "@jridgewell/set-array": "^1.0.1", + "@jridgewell/sourcemap-codec": "^1.4.10", + "@jridgewell/trace-mapping": "^0.3.9" + } + }, + "@jridgewell/resolve-uri": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.1.tgz", + "integrity": "sha512-dSYZh7HhCDtCKm4QakX0xFpsRDqjjtZf/kjI/v3T3Nwt5r8/qz/M19F9ySyOqU94SXBmeG9ttTul+YnR4LOxFA==", + "dev": true + }, + "@jridgewell/set-array": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@jridgewell/set-array/-/set-array-1.1.2.tgz", + "integrity": "sha512-xnkseuNADM0gt2bs+BvhO0p78Mk762YnZdsuzFV018NoG1Sj1SCQvpSqa7XUaTam5vAGasABV9qXASMKnFMwMw==", + "dev": true + }, + "@jridgewell/sourcemap-codec": { + "version": "1.4.14", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.4.14.tgz", + "integrity": "sha512-XPSJHWmi394fuUuzDnGz1wiKqWfo1yXecHQMRf2l6hztTO+nPru658AyDngaBe7isIxEkRsPR3FZh+s7iVa4Uw==", + "dev": true + }, + "@jridgewell/trace-mapping": { + "version": "0.3.19", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.19.tgz", + "integrity": "sha512-kf37QtfW+Hwx/buWGMPcR60iF9ziHa6r/CZJIHbmcm4+0qrXiVdxegAH0F6yddEVQ7zdkjcGCgCzUu+BcbhQxw==", + "dev": true, + "requires": { + "@jridgewell/resolve-uri": "^3.1.0", + "@jridgewell/sourcemap-codec": "^1.4.14" + } + }, + "@nodelib/fs.scandir": { + "version": "2.1.5", + "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", + "integrity": "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==", + "dev": true, + "requires": { + "@nodelib/fs.stat": "2.0.5", + "run-parallel": "^1.1.9" + } + }, + "@nodelib/fs.stat": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz", + "integrity": "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==", + "dev": true + }, + "@nodelib/fs.walk": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/@nodelib/fs.walk/-/fs.walk-1.2.8.tgz", + "integrity": "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==", + "dev": true, + "requires": { + "@nodelib/fs.scandir": "2.1.5", + "fastq": "^1.6.0" + } + }, + "@pkgr/utils": { + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/@pkgr/utils/-/utils-2.4.2.tgz", + "integrity": "sha512-POgTXhjrTfbTV63DiFXav4lBHiICLKKwDeaKn9Nphwj7WH6m0hMMCaJkMyRWjgtPFyRKRVoMXXjczsTQRDEhYw==", + "dev": true, + "requires": { + "cross-spawn": "^7.0.3", + "fast-glob": "^3.3.0", + "is-glob": "^4.0.3", + "open": "^9.1.0", + "picocolors": "^1.0.0", + "tslib": "^2.6.0" + }, + "dependencies": { + "tslib": { + "version": "2.6.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.6.1.tgz", + "integrity": "sha512-t0hLfiEKfMUoqhG+U1oid7Pva4bbDPHYfJNiB7BiIjRkj1pyC++4N3huJfqY6aRH6VTB0rvtzQwjM4K6qpfOig==", + "dev": true + } + } + }, + "@sideway/address": { + "version": "4.1.4", + "resolved": "https://registry.npmjs.org/@sideway/address/-/address-4.1.4.tgz", + "integrity": "sha512-7vwq+rOHVWjyXxVlR76Agnvhy8I9rpzjosTESvmhNeXOXdZZB15Fl+TI9x1SiHZH5Jv2wTGduSxFDIaq0m3DUw==", + "requires": { + "@hapi/hoek": "^9.0.0" + } + }, + "@sideway/formula": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/@sideway/formula/-/formula-3.0.1.tgz", + "integrity": "sha512-/poHZJJVjx3L+zVD6g9KgHfYnb443oi7wLu/XKojDviHy6HOEOA6z1Trk5aR1dGcmPenJEgb2sK2I80LeS3MIg==" + }, + "@sideway/pinpoint": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/@sideway/pinpoint/-/pinpoint-2.0.0.tgz", + "integrity": "sha512-RNiOoTPkptFtSVzQevY/yWtZwf/RxyVnPy/OcA9HBM3MlGDnBEYL5B41H0MTn0Uec8Hi+2qUtTfG2WWZBmMejQ==" + }, + "@sinclair/typebox": { + "version": "0.24.38", + "resolved": "https://registry.npmjs.org/@sinclair/typebox/-/typebox-0.24.38.tgz", + "integrity": "sha512-IbYB6vdhLFmzGEyXXEdFAJKyq7S4/RsivkgxNzs/LzwYuUJHmeNQ0cHkjG/Yqm6VgUzzZDLMZAf0XgeeaZAocA==", + "dev": true + }, + "@sinonjs/commons": { + "version": "1.8.3", + "resolved": "https://registry.npmjs.org/@sinonjs/commons/-/commons-1.8.3.tgz", + "integrity": "sha512-xkNcLAn/wZaX14RPlwizcKicDk9G3F8m2nU3L7Ukm5zBgTwiT0wsoFAHx9Jq56fJA1z/7uKGtCRu16sOUCLIHQ==", + "dev": true, + "requires": { + "type-detect": "4.0.8" + } + }, + "@sinonjs/fake-timers": { + "version": "9.1.2", + "resolved": "https://registry.npmjs.org/@sinonjs/fake-timers/-/fake-timers-9.1.2.tgz", + "integrity": "sha512-BPS4ynJW/o92PUR4wgriz2Ud5gpST5vz6GQfMixEDK0Z8ZCUv2M7SkBLykH56T++Xs+8ln9zTGbOvNGIe02/jw==", + "dev": true, + "requires": { + "@sinonjs/commons": "^1.7.0" + } + }, + "@sinonjs/samsam": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/@sinonjs/samsam/-/samsam-7.0.1.tgz", + "integrity": "sha512-zsAk2Jkiq89mhZovB2LLOdTCxJF4hqqTToGP0ASWlhp4I1hqOjcfmZGafXntCN7MDC6yySH0mFHrYtHceOeLmw==", + "dev": true, + "requires": { + "@sinonjs/commons": "^2.0.0", + "lodash.get": "^4.4.2", + "type-detect": "^4.0.8" + }, + "dependencies": { + "@sinonjs/commons": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/@sinonjs/commons/-/commons-2.0.0.tgz", + "integrity": "sha512-uLa0j859mMrg2slwQYdO/AkrOfmH+X6LTVmNTS9CqexuE2IvVORIkSpJLqePAbEnKJ77aMmCwr1NUZ57120Xcg==", + "dev": true, + "requires": { + "type-detect": "4.0.8" + } + } + } + }, + "@sinonjs/text-encoding": { + "version": "0.7.2", + "resolved": "https://registry.npmjs.org/@sinonjs/text-encoding/-/text-encoding-0.7.2.tgz", + "integrity": "sha512-sXXKG+uL9IrKqViTtao2Ws6dy0znu9sOaP1di/jKGW1M6VssO8vlpXCQcpZ+jisQ1tTFAC5Jo/EOzFbggBagFQ==", + "dev": true + }, + "@smithy/abort-controller": { + "version": "2.0.10", + "resolved": "https://registry.npmjs.org/@smithy/abort-controller/-/abort-controller-2.0.10.tgz", + "integrity": "sha512-xn7PnFD3m4rQIG00h1lPuDVnC2QMtTFhzRLX3y56KkgFaCysS7vpNevNBgmNUtmJ4eVFc+66Zucwo2KDLdicOg==", + "requires": { + "@smithy/types": "^2.3.4", + "tslib": "^2.5.0" + }, + "dependencies": { + "tslib": { + "version": "2.6.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.6.1.tgz", + "integrity": "sha512-t0hLfiEKfMUoqhG+U1oid7Pva4bbDPHYfJNiB7BiIjRkj1pyC++4N3huJfqY6aRH6VTB0rvtzQwjM4K6qpfOig==" + } + } + }, + "@smithy/chunked-blob-reader": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/@smithy/chunked-blob-reader/-/chunked-blob-reader-2.0.0.tgz", + "integrity": "sha512-k+J4GHJsMSAIQPChGBrjEmGS+WbPonCXesoqP9fynIqjn7rdOThdH8FAeCmokP9mxTYKQAKoHCLPzNlm6gh7Wg==", + "requires": { + "tslib": "^2.5.0" + }, + "dependencies": { + "tslib": { + "version": "2.6.2", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.6.2.tgz", + "integrity": "sha512-AEYxH93jGFPn/a2iVAwW87VuUIkR1FVUKB77NwMF7nBTDkDrrT/Hpt/IrCJ0QXhW27jTBDcf5ZY7w6RiqTMw2Q==" + } + } + }, + "@smithy/chunked-blob-reader-native": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/@smithy/chunked-blob-reader-native/-/chunked-blob-reader-native-2.0.0.tgz", + "integrity": "sha512-HM8V2Rp1y8+1343tkZUKZllFhEQPNmpNdgFAncbTsxkZ18/gqjk23XXv3qGyXWp412f3o43ZZ1UZHVcHrpRnCQ==", + "requires": { + "@smithy/util-base64": "^2.0.0", + "tslib": "^2.5.0" + }, + "dependencies": { + "tslib": { + "version": "2.6.2", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.6.2.tgz", + "integrity": "sha512-AEYxH93jGFPn/a2iVAwW87VuUIkR1FVUKB77NwMF7nBTDkDrrT/Hpt/IrCJ0QXhW27jTBDcf5ZY7w6RiqTMw2Q==" + } + } + }, + "@smithy/config-resolver": { + "version": "2.0.11", + "resolved": "https://registry.npmjs.org/@smithy/config-resolver/-/config-resolver-2.0.11.tgz", + "integrity": "sha512-q97FnlUmbai1c4JlQJgLVBsvSxgV/7Nvg/JK76E1nRq/U5UM56Eqo3dn2fY7JibqgJLg4LPsGdwtIyqyOk35CQ==", + "requires": { + "@smithy/node-config-provider": "^2.0.13", + "@smithy/types": "^2.3.4", + "@smithy/util-config-provider": "^2.0.0", + "@smithy/util-middleware": "^2.0.3", + "tslib": "^2.5.0" + }, + "dependencies": { + "tslib": { + "version": "2.6.2", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.6.2.tgz", + "integrity": "sha512-AEYxH93jGFPn/a2iVAwW87VuUIkR1FVUKB77NwMF7nBTDkDrrT/Hpt/IrCJ0QXhW27jTBDcf5ZY7w6RiqTMw2Q==" + } + } + }, + "@smithy/credential-provider-imds": { + "version": "2.0.13", + "resolved": "https://registry.npmjs.org/@smithy/credential-provider-imds/-/credential-provider-imds-2.0.13.tgz", + "integrity": "sha512-/xe3wNoC4j+BeTemH9t2gSKLBfyZmk8LXB2pQm/TOEYi+QhBgT+PSolNDfNAhrR68eggNE17uOimsrnwSkCt4w==", + "requires": { + "@smithy/node-config-provider": "^2.0.13", + "@smithy/property-provider": "^2.0.11", + "@smithy/types": "^2.3.4", + "@smithy/url-parser": "^2.0.10", + "tslib": "^2.5.0" + }, + "dependencies": { + "tslib": { + "version": "2.6.2", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.6.2.tgz", + "integrity": "sha512-AEYxH93jGFPn/a2iVAwW87VuUIkR1FVUKB77NwMF7nBTDkDrrT/Hpt/IrCJ0QXhW27jTBDcf5ZY7w6RiqTMw2Q==" + } + } + }, + "@smithy/eventstream-codec": { + "version": "2.0.10", + "resolved": "https://registry.npmjs.org/@smithy/eventstream-codec/-/eventstream-codec-2.0.10.tgz", + "integrity": "sha512-3SSDgX2nIsFwif6m+I4+ar4KDcZX463Noes8ekBgQHitULiWvaDZX8XqPaRQSQ4bl1vbeVXHklJfv66MnVO+lw==", + "requires": { + "@aws-crypto/crc32": "3.0.0", + "@smithy/types": "^2.3.4", + "@smithy/util-hex-encoding": "^2.0.0", + "tslib": "^2.5.0" + }, + "dependencies": { + "tslib": { + "version": "2.6.2", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.6.2.tgz", + "integrity": "sha512-AEYxH93jGFPn/a2iVAwW87VuUIkR1FVUKB77NwMF7nBTDkDrrT/Hpt/IrCJ0QXhW27jTBDcf5ZY7w6RiqTMw2Q==" + } + } + }, + "@smithy/eventstream-serde-browser": { + "version": "2.0.10", + "resolved": "https://registry.npmjs.org/@smithy/eventstream-serde-browser/-/eventstream-serde-browser-2.0.10.tgz", + "integrity": "sha512-/NSUNrWedO9Se80jo/2WcPvqobqCM/0drZ03Kqn1GZpGwVTsdqNj7frVTCUJs/W/JEzOShdMv8ewoKIR7RWPmA==", + "requires": { + "@smithy/eventstream-serde-universal": "^2.0.10", + "@smithy/types": "^2.3.4", + "tslib": "^2.5.0" + }, + "dependencies": { + "tslib": { + "version": "2.6.2", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.6.2.tgz", + "integrity": "sha512-AEYxH93jGFPn/a2iVAwW87VuUIkR1FVUKB77NwMF7nBTDkDrrT/Hpt/IrCJ0QXhW27jTBDcf5ZY7w6RiqTMw2Q==" + } + } + }, + "@smithy/eventstream-serde-config-resolver": { + "version": "2.0.10", + "resolved": "https://registry.npmjs.org/@smithy/eventstream-serde-config-resolver/-/eventstream-serde-config-resolver-2.0.10.tgz", + "integrity": "sha512-ag1U0vsC5rhRm7okFzsS6YsvyTRe62jIgJ82+Wr4qoOASx7eCDWdjoqLnrdDY0S4UToF9hZAyo4Du/xrSSSk4g==", + "requires": { + "@smithy/types": "^2.3.4", + "tslib": "^2.5.0" + }, + "dependencies": { + "tslib": { + "version": "2.6.2", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.6.2.tgz", + "integrity": "sha512-AEYxH93jGFPn/a2iVAwW87VuUIkR1FVUKB77NwMF7nBTDkDrrT/Hpt/IrCJ0QXhW27jTBDcf5ZY7w6RiqTMw2Q==" + } + } + }, + "@smithy/eventstream-serde-node": { + "version": "2.0.10", + "resolved": "https://registry.npmjs.org/@smithy/eventstream-serde-node/-/eventstream-serde-node-2.0.10.tgz", + "integrity": "sha512-3+VeofxoVCa+dvqcuzEpnFve8EQJKaYR7UslDFpj6UTZfa7Hxr8o1/cbFkTftFo71PxzYVsR+bsD56EbAO432A==", + "requires": { + "@smithy/eventstream-serde-universal": "^2.0.10", + "@smithy/types": "^2.3.4", + "tslib": "^2.5.0" + }, + "dependencies": { + "tslib": { + "version": "2.6.2", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.6.2.tgz", + "integrity": "sha512-AEYxH93jGFPn/a2iVAwW87VuUIkR1FVUKB77NwMF7nBTDkDrrT/Hpt/IrCJ0QXhW27jTBDcf5ZY7w6RiqTMw2Q==" + } + } + }, + "@smithy/eventstream-serde-universal": { + "version": "2.0.10", + "resolved": "https://registry.npmjs.org/@smithy/eventstream-serde-universal/-/eventstream-serde-universal-2.0.10.tgz", + "integrity": "sha512-JhJJU1ULLsn5kxKfFe8zOF2tibjxlPIvIB71Kn20aa/OFs+lvXBR0hBGswpovyYyckXH3qU8VxuIOEuS+2G+3A==", + "requires": { + "@smithy/eventstream-codec": "^2.0.10", + "@smithy/types": "^2.3.4", + "tslib": "^2.5.0" + }, + "dependencies": { + "tslib": { + "version": "2.6.2", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.6.2.tgz", + "integrity": "sha512-AEYxH93jGFPn/a2iVAwW87VuUIkR1FVUKB77NwMF7nBTDkDrrT/Hpt/IrCJ0QXhW27jTBDcf5ZY7w6RiqTMw2Q==" + } + } + }, + "@smithy/fetch-http-handler": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/@smithy/fetch-http-handler/-/fetch-http-handler-2.2.1.tgz", + "integrity": "sha512-bXyM8PBAIKxVV++2ZSNBEposTDjFQ31XWOdHED+2hWMNvJHUoQqFbECg/uhcVOa6vHie2/UnzIZfXBSTpDBnEw==", + "requires": { + "@smithy/protocol-http": "^3.0.6", + "@smithy/querystring-builder": "^2.0.10", + "@smithy/types": "^2.3.4", + "@smithy/util-base64": "^2.0.0", + "tslib": "^2.5.0" + }, + "dependencies": { + "tslib": { + "version": "2.6.2", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.6.2.tgz", + "integrity": "sha512-AEYxH93jGFPn/a2iVAwW87VuUIkR1FVUKB77NwMF7nBTDkDrrT/Hpt/IrCJ0QXhW27jTBDcf5ZY7w6RiqTMw2Q==" + } + } + }, + "@smithy/hash-blob-browser": { + "version": "2.0.10", + "resolved": "https://registry.npmjs.org/@smithy/hash-blob-browser/-/hash-blob-browser-2.0.10.tgz", + "integrity": "sha512-U2+wIWWloOZ9DaRuz2sk9f7A6STRTlwdcv+q6abXDvS0TRDk8KGgUmfV5lCZy8yxFxZIA0hvHDNqcd25r4Hrew==", + "requires": { + "@smithy/chunked-blob-reader": "^2.0.0", + "@smithy/chunked-blob-reader-native": "^2.0.0", + "@smithy/types": "^2.3.4", + "tslib": "^2.5.0" + }, + "dependencies": { + "tslib": { + "version": "2.6.2", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.6.2.tgz", + "integrity": "sha512-AEYxH93jGFPn/a2iVAwW87VuUIkR1FVUKB77NwMF7nBTDkDrrT/Hpt/IrCJ0QXhW27jTBDcf5ZY7w6RiqTMw2Q==" + } + } + }, + "@smithy/hash-node": { + "version": "2.0.10", + "resolved": "https://registry.npmjs.org/@smithy/hash-node/-/hash-node-2.0.10.tgz", + "integrity": "sha512-jSTf6uzPk/Vf+8aQ7tVXeHfjxe9wRXSCqIZcBymSDTf7/YrVxniBdpyN74iI8ZUOx/Pyagc81OK5FROLaEjbXQ==", + "requires": { + "@smithy/types": "^2.3.4", + "@smithy/util-buffer-from": "^2.0.0", + "@smithy/util-utf8": "^2.0.0", + "tslib": "^2.5.0" + }, + "dependencies": { + "tslib": { + "version": "2.6.2", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.6.2.tgz", + "integrity": "sha512-AEYxH93jGFPn/a2iVAwW87VuUIkR1FVUKB77NwMF7nBTDkDrrT/Hpt/IrCJ0QXhW27jTBDcf5ZY7w6RiqTMw2Q==" + } + } + }, + "@smithy/hash-stream-node": { + "version": "2.0.10", + "resolved": "https://registry.npmjs.org/@smithy/hash-stream-node/-/hash-stream-node-2.0.10.tgz", + "integrity": "sha512-L58XEGrownZZSpF7Lp0gc0hy+eYKXuPgNz3pQgP5lPFGwBzHdldx2X6o3c6swD6RkcPvTRh0wTUVVGwUotbgnQ==", + "requires": { + "@smithy/types": "^2.3.4", + "@smithy/util-utf8": "^2.0.0", + "tslib": "^2.5.0" + }, + "dependencies": { + "tslib": { + "version": "2.6.2", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.6.2.tgz", + "integrity": "sha512-AEYxH93jGFPn/a2iVAwW87VuUIkR1FVUKB77NwMF7nBTDkDrrT/Hpt/IrCJ0QXhW27jTBDcf5ZY7w6RiqTMw2Q==" + } + } + }, + "@smithy/invalid-dependency": { + "version": "2.0.10", + "resolved": "https://registry.npmjs.org/@smithy/invalid-dependency/-/invalid-dependency-2.0.10.tgz", + "integrity": "sha512-zw9p/zsmJ2cFcW4KMz3CJoznlbRvEA6HG2mvEaX5eAca5dq4VGI2MwPDTfmteC/GsnURS4ogoMQ0p6aHM2SDVQ==", + "requires": { + "@smithy/types": "^2.3.4", + "tslib": "^2.5.0" + }, + "dependencies": { + "tslib": { + "version": "2.6.2", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.6.2.tgz", + "integrity": "sha512-AEYxH93jGFPn/a2iVAwW87VuUIkR1FVUKB77NwMF7nBTDkDrrT/Hpt/IrCJ0QXhW27jTBDcf5ZY7w6RiqTMw2Q==" + } + } + }, + "@smithy/is-array-buffer": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/@smithy/is-array-buffer/-/is-array-buffer-2.0.0.tgz", + "integrity": "sha512-z3PjFjMyZNI98JFRJi/U0nGoLWMSJlDjAW4QUX2WNZLas5C0CmVV6LJ01JI0k90l7FvpmixjWxPFmENSClQ7ug==", + "requires": { + "tslib": "^2.5.0" + }, + "dependencies": { + "tslib": { + "version": "2.6.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.6.1.tgz", + "integrity": "sha512-t0hLfiEKfMUoqhG+U1oid7Pva4bbDPHYfJNiB7BiIjRkj1pyC++4N3huJfqY6aRH6VTB0rvtzQwjM4K6qpfOig==" + } + } + }, + "@smithy/md5-js": { + "version": "2.0.10", + "resolved": "https://registry.npmjs.org/@smithy/md5-js/-/md5-js-2.0.10.tgz", + "integrity": "sha512-eA/Ova4/UdQUbMlrbBmnewmukH0zWU6C67HFFR/719vkFNepbnliGjmGksQ9vylz9eD4nfGkZZ5NKZMAcUuzjQ==", + "requires": { + "@smithy/types": "^2.3.4", + "@smithy/util-utf8": "^2.0.0", + "tslib": "^2.5.0" + }, + "dependencies": { + "tslib": { + "version": "2.6.2", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.6.2.tgz", + "integrity": "sha512-AEYxH93jGFPn/a2iVAwW87VuUIkR1FVUKB77NwMF7nBTDkDrrT/Hpt/IrCJ0QXhW27jTBDcf5ZY7w6RiqTMw2Q==" + } + } + }, + "@smithy/middleware-content-length": { + "version": "2.0.12", + "resolved": "https://registry.npmjs.org/@smithy/middleware-content-length/-/middleware-content-length-2.0.12.tgz", + "integrity": "sha512-QRhJTo5TjG7oF7np6yY4ZO9GDKFVzU/GtcqUqyEa96bLHE3yZHgNmsolOQ97pfxPHmFhH4vDP//PdpAIN3uI1Q==", + "requires": { + "@smithy/protocol-http": "^3.0.6", + "@smithy/types": "^2.3.4", + "tslib": "^2.5.0" + }, + "dependencies": { + "tslib": { + "version": "2.6.2", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.6.2.tgz", + "integrity": "sha512-AEYxH93jGFPn/a2iVAwW87VuUIkR1FVUKB77NwMF7nBTDkDrrT/Hpt/IrCJ0QXhW27jTBDcf5ZY7w6RiqTMw2Q==" + } + } + }, + "@smithy/middleware-endpoint": { + "version": "2.0.10", + "resolved": "https://registry.npmjs.org/@smithy/middleware-endpoint/-/middleware-endpoint-2.0.10.tgz", + "integrity": "sha512-O6m4puZc16xfenotZUHL4bRlMrwf4gTp+0I5l954M5KNd3dOK18P+FA/IIUgnXF/dX6hlCUcJkBp7nAzwrePKA==", + "requires": { + "@smithy/middleware-serde": "^2.0.10", + "@smithy/types": "^2.3.4", + "@smithy/url-parser": "^2.0.10", + "@smithy/util-middleware": "^2.0.3", + "tslib": "^2.5.0" + }, + "dependencies": { + "tslib": { + "version": "2.6.2", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.6.2.tgz", + "integrity": "sha512-AEYxH93jGFPn/a2iVAwW87VuUIkR1FVUKB77NwMF7nBTDkDrrT/Hpt/IrCJ0QXhW27jTBDcf5ZY7w6RiqTMw2Q==" + } + } + }, + "@smithy/middleware-retry": { + "version": "2.0.13", + "resolved": "https://registry.npmjs.org/@smithy/middleware-retry/-/middleware-retry-2.0.13.tgz", + "integrity": "sha512-zuOva8xgWC7KYG8rEXyWIcZv2GWszO83DCTU6IKcf/FKu6OBmSE+EYv3EUcCGY+GfiwCX0EyJExC9Lpq9b0w5Q==", + "requires": { + "@smithy/node-config-provider": "^2.0.13", + "@smithy/protocol-http": "^3.0.6", + "@smithy/service-error-classification": "^2.0.3", + "@smithy/types": "^2.3.4", + "@smithy/util-middleware": "^2.0.3", + "@smithy/util-retry": "^2.0.3", + "tslib": "^2.5.0", + "uuid": "^8.3.2" + }, + "dependencies": { + "tslib": { + "version": "2.6.2", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.6.2.tgz", + "integrity": "sha512-AEYxH93jGFPn/a2iVAwW87VuUIkR1FVUKB77NwMF7nBTDkDrrT/Hpt/IrCJ0QXhW27jTBDcf5ZY7w6RiqTMw2Q==" + } + } + }, + "@smithy/middleware-serde": { + "version": "2.0.10", + "resolved": "https://registry.npmjs.org/@smithy/middleware-serde/-/middleware-serde-2.0.10.tgz", + "integrity": "sha512-+A0AFqs768256H/BhVEsBF6HijFbVyAwYRVXY/izJFkTalVWJOp4JA0YdY0dpXQd+AlW0tzs+nMQCE1Ew+DcgQ==", + "requires": { + "@smithy/types": "^2.3.4", + "tslib": "^2.5.0" + }, + "dependencies": { + "tslib": { + "version": "2.6.2", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.6.2.tgz", + "integrity": "sha512-AEYxH93jGFPn/a2iVAwW87VuUIkR1FVUKB77NwMF7nBTDkDrrT/Hpt/IrCJ0QXhW27jTBDcf5ZY7w6RiqTMw2Q==" + } + } + }, + "@smithy/middleware-stack": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/@smithy/middleware-stack/-/middleware-stack-2.0.4.tgz", + "integrity": "sha512-MW0KNKfh8ZGLagMZnxcLJWPNXoKqW6XV/st5NnCBmmA2e2JhrUjU0AJ5Ca/yjTyNEKs3xH7AQDwp1YmmpEpmQQ==", + "requires": { + "@smithy/types": "^2.3.4", + "tslib": "^2.5.0" + }, + "dependencies": { + "tslib": { + "version": "2.6.2", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.6.2.tgz", + "integrity": "sha512-AEYxH93jGFPn/a2iVAwW87VuUIkR1FVUKB77NwMF7nBTDkDrrT/Hpt/IrCJ0QXhW27jTBDcf5ZY7w6RiqTMw2Q==" + } + } + }, + "@smithy/node-config-provider": { + "version": "2.0.13", + "resolved": "https://registry.npmjs.org/@smithy/node-config-provider/-/node-config-provider-2.0.13.tgz", + "integrity": "sha512-pPpLqYuJcOq1sj1EGu+DoZK47DUS4gepqSTNgRezmrjnzNlSU2/Dcc9Ebzs+WZ0Z5vXKazuE+k+NksFLo07/AA==", + "requires": { + "@smithy/property-provider": "^2.0.11", + "@smithy/shared-ini-file-loader": "^2.0.12", + "@smithy/types": "^2.3.4", + "tslib": "^2.5.0" + }, + "dependencies": { + "tslib": { + "version": "2.6.2", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.6.2.tgz", + "integrity": "sha512-AEYxH93jGFPn/a2iVAwW87VuUIkR1FVUKB77NwMF7nBTDkDrrT/Hpt/IrCJ0QXhW27jTBDcf5ZY7w6RiqTMw2Q==" + } + } + }, + "@smithy/node-http-handler": { + "version": "2.1.6", + "resolved": "https://registry.npmjs.org/@smithy/node-http-handler/-/node-http-handler-2.1.6.tgz", + "integrity": "sha512-NspvD3aCwiUNtoSTcVHz0RZz1tQ/SaRIe1KPF+r0mAdCZ9eWuhIeJT8ZNPYa1ITn7/Lgg64IyFjqPynZ8KnYQw==", + "requires": { + "@smithy/abort-controller": "^2.0.10", + "@smithy/protocol-http": "^3.0.6", + "@smithy/querystring-builder": "^2.0.10", + "@smithy/types": "^2.3.4", + "tslib": "^2.5.0" + }, + "dependencies": { + "tslib": { + "version": "2.6.2", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.6.2.tgz", + "integrity": "sha512-AEYxH93jGFPn/a2iVAwW87VuUIkR1FVUKB77NwMF7nBTDkDrrT/Hpt/IrCJ0QXhW27jTBDcf5ZY7w6RiqTMw2Q==" + } + } + }, + "@smithy/property-provider": { + "version": "2.0.11", + "resolved": "https://registry.npmjs.org/@smithy/property-provider/-/property-provider-2.0.11.tgz", + "integrity": "sha512-kzuOadu6XvrnlF1iXofpKXYmo4oe19st9/DE8f5gHNaFepb4eTkR8gD8BSdTnNnv7lxfv6uOwZPg4VS6hemX1w==", + "requires": { + "@smithy/types": "^2.3.4", + "tslib": "^2.5.0" + }, + "dependencies": { + "tslib": { + "version": "2.6.2", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.6.2.tgz", + "integrity": "sha512-AEYxH93jGFPn/a2iVAwW87VuUIkR1FVUKB77NwMF7nBTDkDrrT/Hpt/IrCJ0QXhW27jTBDcf5ZY7w6RiqTMw2Q==" + } + } + }, + "@smithy/protocol-http": { + "version": "3.0.6", + "resolved": "https://registry.npmjs.org/@smithy/protocol-http/-/protocol-http-3.0.6.tgz", + "integrity": "sha512-F0jAZzwznMmHaggiZgc7YoS08eGpmLvhVktY/Taz6+OAOHfyIqWSDNgFqYR+WHW9z5fp2XvY4mEUrQgYMQ71jw==", + "requires": { + "@smithy/types": "^2.3.4", + "tslib": "^2.5.0" + }, + "dependencies": { + "tslib": { + "version": "2.6.2", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.6.2.tgz", + "integrity": "sha512-AEYxH93jGFPn/a2iVAwW87VuUIkR1FVUKB77NwMF7nBTDkDrrT/Hpt/IrCJ0QXhW27jTBDcf5ZY7w6RiqTMw2Q==" + } + } + }, + "@smithy/querystring-builder": { + "version": "2.0.10", + "resolved": "https://registry.npmjs.org/@smithy/querystring-builder/-/querystring-builder-2.0.10.tgz", + "integrity": "sha512-uujJGp8jzrrU1UHme8sUKEbawQTcTmUWsh8rbGXYD/lMwNLQ+9jQ9dMDWbbH9Hpoa9RER1BeL/38WzGrbpob2w==", + "requires": { + "@smithy/types": "^2.3.4", + "@smithy/util-uri-escape": "^2.0.0", + "tslib": "^2.5.0" + }, + "dependencies": { + "tslib": { + "version": "2.6.2", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.6.2.tgz", + "integrity": "sha512-AEYxH93jGFPn/a2iVAwW87VuUIkR1FVUKB77NwMF7nBTDkDrrT/Hpt/IrCJ0QXhW27jTBDcf5ZY7w6RiqTMw2Q==" + } + } + }, + "@smithy/querystring-parser": { + "version": "2.0.10", + "resolved": "https://registry.npmjs.org/@smithy/querystring-parser/-/querystring-parser-2.0.10.tgz", + "integrity": "sha512-WSD4EU60Q8scacT5PIpx4Bahn6nWpt+MiYLcBkFt6fOj7AssrNeaNIU2Z0g40ftVmrwLcEOIKGX92ynbVDb3ZA==", + "requires": { + "@smithy/types": "^2.3.4", + "tslib": "^2.5.0" + }, + "dependencies": { + "tslib": { + "version": "2.6.2", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.6.2.tgz", + "integrity": "sha512-AEYxH93jGFPn/a2iVAwW87VuUIkR1FVUKB77NwMF7nBTDkDrrT/Hpt/IrCJ0QXhW27jTBDcf5ZY7w6RiqTMw2Q==" + } + } + }, + "@smithy/service-error-classification": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/@smithy/service-error-classification/-/service-error-classification-2.0.3.tgz", + "integrity": "sha512-b+m4QCHXb7oKAkM/jHwHrl5gpqhFoMTHF643L0/vAEkegrcUWyh1UjyoHttuHcP5FnHVVy4EtpPtLkEYD+xMFw==", + "requires": { + "@smithy/types": "^2.3.4" + } + }, + "@smithy/shared-ini-file-loader": { + "version": "2.0.12", + "resolved": "https://registry.npmjs.org/@smithy/shared-ini-file-loader/-/shared-ini-file-loader-2.0.12.tgz", + "integrity": "sha512-umi0wc4UBGYullAgYNUVfGLgVpxQyES47cnomTqzCKeKO5oudO4hyDNj+wzrOjqDFwK2nWYGVgS8Y0JgGietrw==", + "requires": { + "@smithy/types": "^2.3.4", + "tslib": "^2.5.0" + }, + "dependencies": { + "tslib": { + "version": "2.6.2", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.6.2.tgz", + "integrity": "sha512-AEYxH93jGFPn/a2iVAwW87VuUIkR1FVUKB77NwMF7nBTDkDrrT/Hpt/IrCJ0QXhW27jTBDcf5ZY7w6RiqTMw2Q==" + } + } + }, + "@smithy/signature-v4": { + "version": "2.0.10", + "resolved": "https://registry.npmjs.org/@smithy/signature-v4/-/signature-v4-2.0.10.tgz", + "integrity": "sha512-S6gcP4IXfO/VMswovrhxPpqvQvMal7ZRjM4NvblHSPpE5aNBYx67UkHFF3kg0hR3tJKqNpBGbxwq0gzpdHKLRA==", + "requires": { + "@smithy/eventstream-codec": "^2.0.10", + "@smithy/is-array-buffer": "^2.0.0", + "@smithy/types": "^2.3.4", + "@smithy/util-hex-encoding": "^2.0.0", + "@smithy/util-middleware": "^2.0.3", + "@smithy/util-uri-escape": "^2.0.0", + "@smithy/util-utf8": "^2.0.0", + "tslib": "^2.5.0" + }, + "dependencies": { + "tslib": { + "version": "2.6.2", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.6.2.tgz", + "integrity": "sha512-AEYxH93jGFPn/a2iVAwW87VuUIkR1FVUKB77NwMF7nBTDkDrrT/Hpt/IrCJ0QXhW27jTBDcf5ZY7w6RiqTMw2Q==" + } + } + }, + "@smithy/smithy-client": { + "version": "2.1.9", + "resolved": "https://registry.npmjs.org/@smithy/smithy-client/-/smithy-client-2.1.9.tgz", + "integrity": "sha512-HTicQSn/lOcXKJT+DKJ4YMu51S6PzbWsO8Z6Pwueo30mSoFKXg5P0BDkg2VCDqCVR0mtddM/F6hKhjW6YAV/yg==", + "requires": { + "@smithy/middleware-stack": "^2.0.4", + "@smithy/types": "^2.3.4", + "@smithy/util-stream": "^2.0.14", + "tslib": "^2.5.0" + }, + "dependencies": { + "tslib": { + "version": "2.6.2", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.6.2.tgz", + "integrity": "sha512-AEYxH93jGFPn/a2iVAwW87VuUIkR1FVUKB77NwMF7nBTDkDrrT/Hpt/IrCJ0QXhW27jTBDcf5ZY7w6RiqTMw2Q==" + } + } + }, + "@smithy/types": { + "version": "2.3.4", + "resolved": "https://registry.npmjs.org/@smithy/types/-/types-2.3.4.tgz", + "integrity": "sha512-D7xlM9FOMFyFw7YnMXn9dK2KuN6+JhnrZwVt1fWaIu8hCk5CigysweeIT/H/nCo4YV+s8/oqUdLfexbkPZtvqw==", + "requires": { + "tslib": "^2.5.0" + }, + "dependencies": { + "tslib": { + "version": "2.6.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.6.1.tgz", + "integrity": "sha512-t0hLfiEKfMUoqhG+U1oid7Pva4bbDPHYfJNiB7BiIjRkj1pyC++4N3huJfqY6aRH6VTB0rvtzQwjM4K6qpfOig==" + } + } + }, + "@smithy/url-parser": { + "version": "2.0.10", + "resolved": "https://registry.npmjs.org/@smithy/url-parser/-/url-parser-2.0.10.tgz", + "integrity": "sha512-4TXQFGjHcqru8aH5VRB4dSnOFKCYNX6SR1Do6fwxZ+ExT2onLsh2W77cHpks7ma26W5jv6rI1u7d0+KX9F0aOw==", + "requires": { + "@smithy/querystring-parser": "^2.0.10", + "@smithy/types": "^2.3.4", + "tslib": "^2.5.0" + }, + "dependencies": { + "tslib": { + "version": "2.6.2", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.6.2.tgz", + "integrity": "sha512-AEYxH93jGFPn/a2iVAwW87VuUIkR1FVUKB77NwMF7nBTDkDrrT/Hpt/IrCJ0QXhW27jTBDcf5ZY7w6RiqTMw2Q==" + } + } + }, + "@smithy/util-base64": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/@smithy/util-base64/-/util-base64-2.0.0.tgz", + "integrity": "sha512-Zb1E4xx+m5Lud8bbeYi5FkcMJMnn+1WUnJF3qD7rAdXpaL7UjkFQLdmW5fHadoKbdHpwH9vSR8EyTJFHJs++tA==", + "requires": { + "@smithy/util-buffer-from": "^2.0.0", + "tslib": "^2.5.0" + }, + "dependencies": { + "tslib": { + "version": "2.6.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.6.1.tgz", + "integrity": "sha512-t0hLfiEKfMUoqhG+U1oid7Pva4bbDPHYfJNiB7BiIjRkj1pyC++4N3huJfqY6aRH6VTB0rvtzQwjM4K6qpfOig==" + } + } + }, + "@smithy/util-body-length-browser": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/@smithy/util-body-length-browser/-/util-body-length-browser-2.0.0.tgz", + "integrity": "sha512-JdDuS4ircJt+FDnaQj88TzZY3+njZ6O+D3uakS32f2VNnDo3vyEuNdBOh/oFd8Df1zSZOuH1HEChk2AOYDezZg==", + "requires": { + "tslib": "^2.5.0" + }, + "dependencies": { + "tslib": { + "version": "2.6.2", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.6.2.tgz", + "integrity": "sha512-AEYxH93jGFPn/a2iVAwW87VuUIkR1FVUKB77NwMF7nBTDkDrrT/Hpt/IrCJ0QXhW27jTBDcf5ZY7w6RiqTMw2Q==" + } + } + }, + "@smithy/util-body-length-node": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/@smithy/util-body-length-node/-/util-body-length-node-2.1.0.tgz", + "integrity": "sha512-/li0/kj/y3fQ3vyzn36NTLGmUwAICb7Jbe/CsWCktW363gh1MOcpEcSO3mJ344Gv2dqz8YJCLQpb6hju/0qOWw==", + "requires": { + "tslib": "^2.5.0" + }, + "dependencies": { + "tslib": { + "version": "2.6.2", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.6.2.tgz", + "integrity": "sha512-AEYxH93jGFPn/a2iVAwW87VuUIkR1FVUKB77NwMF7nBTDkDrrT/Hpt/IrCJ0QXhW27jTBDcf5ZY7w6RiqTMw2Q==" + } + } + }, + "@smithy/util-buffer-from": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/@smithy/util-buffer-from/-/util-buffer-from-2.0.0.tgz", + "integrity": "sha512-/YNnLoHsR+4W4Vf2wL5lGv0ksg8Bmk3GEGxn2vEQt52AQaPSCuaO5PM5VM7lP1K9qHRKHwrPGktqVoAHKWHxzw==", + "requires": { + "@smithy/is-array-buffer": "^2.0.0", + "tslib": "^2.5.0" + }, + "dependencies": { + "tslib": { + "version": "2.6.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.6.1.tgz", + "integrity": "sha512-t0hLfiEKfMUoqhG+U1oid7Pva4bbDPHYfJNiB7BiIjRkj1pyC++4N3huJfqY6aRH6VTB0rvtzQwjM4K6qpfOig==" + } + } + }, + "@smithy/util-config-provider": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/@smithy/util-config-provider/-/util-config-provider-2.0.0.tgz", + "integrity": "sha512-xCQ6UapcIWKxXHEU4Mcs2s7LcFQRiU3XEluM2WcCjjBtQkUN71Tb+ydGmJFPxMUrW/GWMgQEEGipLym4XG0jZg==", + "requires": { + "tslib": "^2.5.0" + }, + "dependencies": { + "tslib": { + "version": "2.6.2", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.6.2.tgz", + "integrity": "sha512-AEYxH93jGFPn/a2iVAwW87VuUIkR1FVUKB77NwMF7nBTDkDrrT/Hpt/IrCJ0QXhW27jTBDcf5ZY7w6RiqTMw2Q==" + } + } + }, + "@smithy/util-defaults-mode-browser": { + "version": "2.0.13", + "resolved": "https://registry.npmjs.org/@smithy/util-defaults-mode-browser/-/util-defaults-mode-browser-2.0.13.tgz", + "integrity": "sha512-UmmOdUzaQjqdsl1EjbpEaQxM0VDFqTj6zDuI26/hXN7L/a1k1koTwkYpogHMvunDX3fjrQusg5gv1Td4UsGyog==", + "requires": { + "@smithy/property-provider": "^2.0.11", + "@smithy/smithy-client": "^2.1.9", + "@smithy/types": "^2.3.4", + "bowser": "^2.11.0", + "tslib": "^2.5.0" + }, + "dependencies": { + "tslib": { + "version": "2.6.2", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.6.2.tgz", + "integrity": "sha512-AEYxH93jGFPn/a2iVAwW87VuUIkR1FVUKB77NwMF7nBTDkDrrT/Hpt/IrCJ0QXhW27jTBDcf5ZY7w6RiqTMw2Q==" + } + } + }, + "@smithy/util-defaults-mode-node": { + "version": "2.0.15", + "resolved": "https://registry.npmjs.org/@smithy/util-defaults-mode-node/-/util-defaults-mode-node-2.0.15.tgz", + "integrity": "sha512-g6J7MHAibVPMTlXyH3mL+Iet4lMJKFVhsOhJmn+IKG81uy9m42CkRSDlwdQSJAcprLQBIaOPdFxNXQvrg2w1Uw==", + "requires": { + "@smithy/config-resolver": "^2.0.11", + "@smithy/credential-provider-imds": "^2.0.13", + "@smithy/node-config-provider": "^2.0.13", + "@smithy/property-provider": "^2.0.11", + "@smithy/smithy-client": "^2.1.9", + "@smithy/types": "^2.3.4", + "tslib": "^2.5.0" + }, + "dependencies": { + "tslib": { + "version": "2.6.2", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.6.2.tgz", + "integrity": "sha512-AEYxH93jGFPn/a2iVAwW87VuUIkR1FVUKB77NwMF7nBTDkDrrT/Hpt/IrCJ0QXhW27jTBDcf5ZY7w6RiqTMw2Q==" + } + } + }, + "@smithy/util-hex-encoding": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/@smithy/util-hex-encoding/-/util-hex-encoding-2.0.0.tgz", + "integrity": "sha512-c5xY+NUnFqG6d7HFh1IFfrm3mGl29lC+vF+geHv4ToiuJCBmIfzx6IeHLg+OgRdPFKDXIw6pvi+p3CsscaMcMA==", + "requires": { + "tslib": "^2.5.0" + }, + "dependencies": { + "tslib": { + "version": "2.6.2", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.6.2.tgz", + "integrity": "sha512-AEYxH93jGFPn/a2iVAwW87VuUIkR1FVUKB77NwMF7nBTDkDrrT/Hpt/IrCJ0QXhW27jTBDcf5ZY7w6RiqTMw2Q==" + } + } + }, + "@smithy/util-middleware": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/@smithy/util-middleware/-/util-middleware-2.0.3.tgz", + "integrity": "sha512-+FOCFYOxd2HO7v/0hkFSETKf7FYQWa08wh/x/4KUeoVBnLR4juw8Qi+TTqZI6E2h5LkzD9uOaxC9lAjrpVzaaA==", + "requires": { + "@smithy/types": "^2.3.4", + "tslib": "^2.5.0" + }, + "dependencies": { + "tslib": { + "version": "2.6.2", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.6.2.tgz", + "integrity": "sha512-AEYxH93jGFPn/a2iVAwW87VuUIkR1FVUKB77NwMF7nBTDkDrrT/Hpt/IrCJ0QXhW27jTBDcf5ZY7w6RiqTMw2Q==" + } + } + }, + "@smithy/util-retry": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/@smithy/util-retry/-/util-retry-2.0.3.tgz", + "integrity": "sha512-gw+czMnj82i+EaH7NL7XKkfX/ZKrCS2DIWwJFPKs76bMgkhf0y1C94Lybn7f8GkBI9lfIOUdPYtzm19zQOC8sw==", + "requires": { + "@smithy/service-error-classification": "^2.0.3", + "@smithy/types": "^2.3.4", + "tslib": "^2.5.0" + }, + "dependencies": { + "tslib": { + "version": "2.6.2", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.6.2.tgz", + "integrity": "sha512-AEYxH93jGFPn/a2iVAwW87VuUIkR1FVUKB77NwMF7nBTDkDrrT/Hpt/IrCJ0QXhW27jTBDcf5ZY7w6RiqTMw2Q==" + } + } + }, + "@smithy/util-stream": { + "version": "2.0.14", + "resolved": "https://registry.npmjs.org/@smithy/util-stream/-/util-stream-2.0.14.tgz", + "integrity": "sha512-XjvlDYe+9DieXhLf7p+EgkXwFtl34kHZcWfHnc5KaILbhyVfDLWuqKTFx6WwCFqb01iFIig8trGwExRIqqkBYg==", + "requires": { + "@smithy/fetch-http-handler": "^2.2.1", + "@smithy/node-http-handler": "^2.1.6", + "@smithy/types": "^2.3.4", + "@smithy/util-base64": "^2.0.0", + "@smithy/util-buffer-from": "^2.0.0", + "@smithy/util-hex-encoding": "^2.0.0", + "@smithy/util-utf8": "^2.0.0", + "tslib": "^2.5.0" + }, + "dependencies": { + "tslib": { + "version": "2.6.2", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.6.2.tgz", + "integrity": "sha512-AEYxH93jGFPn/a2iVAwW87VuUIkR1FVUKB77NwMF7nBTDkDrrT/Hpt/IrCJ0QXhW27jTBDcf5ZY7w6RiqTMw2Q==" + } + } + }, + "@smithy/util-uri-escape": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/@smithy/util-uri-escape/-/util-uri-escape-2.0.0.tgz", + "integrity": "sha512-ebkxsqinSdEooQduuk9CbKcI+wheijxEb3utGXkCoYQkJnwTnLbH1JXGimJtUkQwNQbsbuYwG2+aFVyZf5TLaw==", + "requires": { + "tslib": "^2.5.0" + }, + "dependencies": { + "tslib": { + "version": "2.6.2", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.6.2.tgz", + "integrity": "sha512-AEYxH93jGFPn/a2iVAwW87VuUIkR1FVUKB77NwMF7nBTDkDrrT/Hpt/IrCJ0QXhW27jTBDcf5ZY7w6RiqTMw2Q==" + } + } + }, + "@smithy/util-utf8": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/@smithy/util-utf8/-/util-utf8-2.0.0.tgz", + "integrity": "sha512-rctU1VkziY84n5OXe3bPNpKR001ZCME2JCaBBFgtiM2hfKbHFudc/BkMuPab8hRbLd0j3vbnBTTZ1igBf0wgiQ==", + "requires": { + "@smithy/util-buffer-from": "^2.0.0", + "tslib": "^2.5.0" + }, + "dependencies": { + "tslib": { + "version": "2.6.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.6.1.tgz", + "integrity": "sha512-t0hLfiEKfMUoqhG+U1oid7Pva4bbDPHYfJNiB7BiIjRkj1pyC++4N3huJfqY6aRH6VTB0rvtzQwjM4K6qpfOig==" + } + } + }, + "@smithy/util-waiter": { + "version": "2.0.10", + "resolved": "https://registry.npmjs.org/@smithy/util-waiter/-/util-waiter-2.0.10.tgz", + "integrity": "sha512-yQjwWVrwYw+/f3hFQccE3zZF7lk6N6xtNcA6jvhWFYhnyKAm6B2mX8Gzftl0TbgoPUpzCvKYlvhaEpVtRpVfVw==", + "requires": { + "@smithy/abort-controller": "^2.0.10", + "@smithy/types": "^2.3.4", + "tslib": "^2.5.0" + }, + "dependencies": { + "tslib": { + "version": "2.6.2", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.6.2.tgz", + "integrity": "sha512-AEYxH93jGFPn/a2iVAwW87VuUIkR1FVUKB77NwMF7nBTDkDrrT/Hpt/IrCJ0QXhW27jTBDcf5ZY7w6RiqTMw2Q==" + } + } + }, + "@types/babel__core": { + "version": "7.20.2", + "resolved": "https://registry.npmjs.org/@types/babel__core/-/babel__core-7.20.2.tgz", + "integrity": "sha512-pNpr1T1xLUc2l3xJKuPtsEky3ybxN3m4fJkknfIpTCTfIZCDW57oAg+EfCgIIp2rvCe0Wn++/FfodDS4YXxBwA==", + "dev": true, + "requires": { + "@babel/parser": "^7.20.7", + "@babel/types": "^7.20.7", + "@types/babel__generator": "*", + "@types/babel__template": "*", + "@types/babel__traverse": "*" + } + }, + "@types/babel__generator": { + "version": "7.6.5", + "resolved": "https://registry.npmjs.org/@types/babel__generator/-/babel__generator-7.6.5.tgz", + "integrity": "sha512-h9yIuWbJKdOPLJTbmSpPzkF67e659PbQDba7ifWm5BJ8xTv+sDmS7rFmywkWOvXedGTivCdeGSIIX8WLcRTz8w==", + "dev": true, + "requires": { + "@babel/types": "^7.0.0" + } + }, + "@types/babel__template": { + "version": "7.4.2", + "resolved": "https://registry.npmjs.org/@types/babel__template/-/babel__template-7.4.2.tgz", + "integrity": "sha512-/AVzPICMhMOMYoSx9MoKpGDKdBRsIXMNByh1PXSZoa+v6ZoLa8xxtsT/uLQ/NJm0XVAWl/BvId4MlDeXJaeIZQ==", + "dev": true, + "requires": { + "@babel/parser": "^7.1.0", + "@babel/types": "^7.0.0" + } + }, + "@types/babel__traverse": { + "version": "7.20.2", + "resolved": "https://registry.npmjs.org/@types/babel__traverse/-/babel__traverse-7.20.2.tgz", + "integrity": "sha512-ojlGK1Hsfce93J0+kn3H5R73elidKUaZonirN33GSmgTUMpzI/MIFfSpF3haANe3G1bEBS9/9/QEqwTzwqFsKw==", + "dev": true, + "requires": { + "@babel/types": "^7.20.7" + } + }, + "@types/color-name": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@types/color-name/-/color-name-1.1.1.tgz", + "integrity": "sha512-rr+OQyAjxze7GgWrSaJwydHStIhHq2lvY3BOC2Mj7KnzI7XK0Uw1TOOdI9lDoajEbSWLiYgoo4f1R51erQfhPQ==", + "dev": true + }, + "@types/graceful-fs": { + "version": "4.1.7", + "resolved": "https://registry.npmjs.org/@types/graceful-fs/-/graceful-fs-4.1.7.tgz", + "integrity": "sha512-MhzcwU8aUygZroVwL2jeYk6JisJrPl/oov/gsgGCue9mkgl9wjGbzReYQClxiUgFDnib9FuHqTndccKeZKxTRw==", + "dev": true, + "requires": { + "@types/node": "*" + } + }, + "@types/istanbul-lib-coverage": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/@types/istanbul-lib-coverage/-/istanbul-lib-coverage-2.0.4.tgz", + "integrity": "sha512-z/QT1XN4K4KYuslS23k62yDIDLwLFkzxOuMplDtObz0+y7VqJCaO2o+SPwHCvLFZh7xazvvoor2tA/hPz9ee7g==", + "dev": true + }, + "@types/istanbul-lib-report": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/@types/istanbul-lib-report/-/istanbul-lib-report-3.0.0.tgz", + "integrity": "sha512-plGgXAPfVKFoYfa9NpYDAkseG+g6Jr294RqeqcqDixSbU34MZVJRi/P+7Y8GDpzkEwLaGZZOpKIEmeVZNtKsrg==", + "dev": true, + "requires": { + "@types/istanbul-lib-coverage": "*" + } + }, + "@types/istanbul-reports": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/@types/istanbul-reports/-/istanbul-reports-3.0.1.tgz", + "integrity": "sha512-c3mAZEuK0lvBp8tmuL74XRKn1+y2dcwOUpH7x4WrF6gk1GIgiluDRgMYQtw2OFcBvAJWlt6ASU3tSqxp0Uu0Aw==", + "dev": true, + "requires": { + "@types/istanbul-lib-report": "*" + } + }, + "@types/jest": { + "version": "28.1.8", + "resolved": "https://registry.npmjs.org/@types/jest/-/jest-28.1.8.tgz", + "integrity": "sha512-8TJkV++s7B6XqnDrzR1m/TT0A0h948Pnl/097veySPN67VRAgQ4gZ7n2KfJo2rVq6njQjdxU3GCCyDvAeuHoiw==", + "dev": true, + "requires": { + "expect": "^28.0.0", + "pretty-format": "^28.0.0" + } + }, + "@types/json5": { + "version": "0.0.29", + "resolved": "https://registry.npmjs.org/@types/json5/-/json5-0.0.29.tgz", + "integrity": "sha1-7ihweulOEdK4J7y+UnC86n8+ce4=", + "dev": true + }, + "@types/node": { + "version": "14.0.23", + "resolved": "https://registry.npmjs.org/@types/node/-/node-14.0.23.tgz", + "integrity": "sha512-Z4U8yDAl5TFkmYsZdFPdjeMa57NOvnaf1tljHzhouaPEp7LCj2JKkejpI1ODviIAQuW4CcQmxkQ77rnLsOOoKw==", + "dev": true + }, + "@types/sinon": { + "version": "10.0.10", + "resolved": "https://registry.npmjs.org/@types/sinon/-/sinon-10.0.10.tgz", + "integrity": "sha512-US5E539UfeL2DiWALzCyk0c4zKh6sCv86V/0lpda/afMJJ0oEm2SrKgedH5optvFWstnJ8e1MNYhLmPhAy4rvQ==", + "dev": true, + "requires": { + "@sinonjs/fake-timers": "^7.1.0" + }, + "dependencies": { + "@sinonjs/fake-timers": { + "version": "7.1.2", + "resolved": "https://registry.npmjs.org/@sinonjs/fake-timers/-/fake-timers-7.1.2.tgz", + "integrity": "sha512-iQADsW4LBMISqZ6Ci1dupJL9pprqwcVFTcOsEmQOEhW+KLCVn/Y4Jrvg2k19fIHCp+iFprriYPTdRcQR8NbUPg==", + "dev": true, + "requires": { + "@sinonjs/commons": "^1.7.0" + } + } + } + }, + "@types/stack-utils": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/@types/stack-utils/-/stack-utils-2.0.1.tgz", + "integrity": "sha512-Hl219/BT5fLAaz6NDkSuhzasy49dwQS/DSdu4MdggFB8zcXv7vflBI3xp7FEmkmdDkBUI2bPUNeMttp2knYdxw==", + "dev": true + }, + "@types/yargs": { + "version": "17.0.12", + "resolved": "https://registry.npmjs.org/@types/yargs/-/yargs-17.0.12.tgz", + "integrity": "sha512-Nz4MPhecOFArtm81gFQvQqdV7XYCrWKx5uUt6GNHredFHn1i2mtWqXTON7EPXMtNi1qjtjEM/VCHDhcHsAMLXQ==", + "dev": true, + "requires": { + "@types/yargs-parser": "*" + } + }, + "@types/yargs-parser": { + "version": "21.0.0", + "resolved": "https://registry.npmjs.org/@types/yargs-parser/-/yargs-parser-21.0.0.tgz", + "integrity": "sha512-iO9ZQHkZxHn4mSakYV0vFHAVDyEOIJQrV2uZ06HxEPcx+mt8swXoZHIbaaJ2crJYFfErySgktuTZ3BeLz+XmFA==", + "dev": true + }, + "abbrev": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/abbrev/-/abbrev-1.1.1.tgz", + "integrity": "sha512-nne9/IiQ/hzIhY6pdDnbBtz7DjPTKrY00P/zvPSm5pOFkl6xuGrGnXn/VtTNNfNtAfZ9/1RtehkszU9qcTii0Q==", + "dev": true + }, + "accepts": { + "version": "1.3.8", + "resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.8.tgz", + "integrity": "sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw==", + "requires": { + "mime-types": "~2.1.34", + "negotiator": "0.6.3" + } + }, + "acorn": { + "version": "7.3.1", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-7.3.1.tgz", + "integrity": "sha512-tLc0wSnatxAQHVHUapaHdz72pi9KUyHjq5KyHjGg9Y8Ifdc79pTh2XvI6I1/chZbnM7QtNKzh66ooDogPZSleA==", + "dev": true + }, + "acorn-jsx": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.3.2.tgz", + "integrity": "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==", + "dev": true + }, + "ajv": { + "version": "6.12.6", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", + "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", + "dev": true, + "requires": { + "fast-deep-equal": "^3.1.1", + "fast-json-stable-stringify": "^2.0.0", + "json-schema-traverse": "^0.4.1", + "uri-js": "^4.2.2" + } + }, + "ajv-formats": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/ajv-formats/-/ajv-formats-2.1.1.tgz", + "integrity": "sha512-Wx0Kx52hxE7C18hkMEggYlEifqWZtYaRgouJor+WMdPnQyEK13vgEWyVNup7SoeeoLMsr4kf5h6dOW11I15MUA==", + "requires": { + "ajv": "^8.0.0" + }, + "dependencies": { + "ajv": { + "version": "8.12.0", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.12.0.tgz", + "integrity": "sha512-sRu1kpcO9yLtYxBKvqfTeh9KzZEwO3STyX1HT+4CaDzC6HpTGYhIhPIzj9XuKU7KYDwnaeh5hcOwjy1QuJzBPA==", + "requires": { + "fast-deep-equal": "^3.1.1", + "json-schema-traverse": "^1.0.0", + "require-from-string": "^2.0.2", + "uri-js": "^4.2.2" + } + }, + "json-schema-traverse": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", + "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==" + } + } + }, + "ansi-escapes": { + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/ansi-escapes/-/ansi-escapes-4.3.1.tgz", + "integrity": "sha512-JWF7ocqNrp8u9oqpgV+wH5ftbt+cfvv+PTjOvKLT3AdYly/LmORARfEVT1iyjwN+4MqE5UmVKoAdIBqeoCHgLA==", + "dev": true, + "requires": { + "type-fest": "^0.11.0" + }, + "dependencies": { + "type-fest": { + "version": "0.11.0", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.11.0.tgz", + "integrity": "sha512-OdjXJxnCN1AvyLSzeKIgXTXxV+99ZuXl3Hpo9XpJAv9MBcHrrJOQ5kV7ypXOuQie+AmWG25hLbiKdwYTifzcfQ==", + "dev": true + } + } + }, + "ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "dev": true + }, + "ansi-styles": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz", + "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==", + "requires": { + "color-convert": "^1.9.0" + } + }, + "anymatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.2.tgz", + "integrity": "sha512-P43ePfOAIupkguHUycrc4qJ9kz8ZiuOUijaETwX7THt0Y/GNK7v0aa8rY816xWjZ7rJdA5XdMcpVFTKMq+RvWg==", + "dev": true, + "requires": { + "normalize-path": "^3.0.0", + "picomatch": "^2.0.4" + } + }, + "api-problem": { + "version": "9.0.2", + "resolved": "https://registry.npmjs.org/api-problem/-/api-problem-9.0.2.tgz", + "integrity": "sha512-Xr4TyFkyvTEkgL8zUhyoqeK2Oxx2GQaFIPNiuhVRfop34gNl5r5gk1jYMsQhtPWSpwavROVweNIyL677ibE4rw==" + }, + "argparse": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", + "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==" + }, + "array-flatten": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-1.1.1.tgz", + "integrity": "sha1-ml9pkFGx5wczKPKgCJaLZOopVdI=" + }, + "array-includes": { + "version": "3.1.4", + "resolved": "https://registry.npmjs.org/array-includes/-/array-includes-3.1.4.tgz", + "integrity": "sha512-ZTNSQkmWumEbiHO2GF4GmWxYVTiQyJy2XOTa15sdQSrvKn7l+180egQMqlrMOUMCyLMD7pmyQe4mMDUT6Behrw==", + "dev": true, + "requires": { + "call-bind": "^1.0.2", + "define-properties": "^1.1.3", + "es-abstract": "^1.19.1", + "get-intrinsic": "^1.1.1", + "is-string": "^1.0.7" + } + }, + "array.prototype.flat": { + "version": "1.2.5", + "resolved": "https://registry.npmjs.org/array.prototype.flat/-/array.prototype.flat-1.2.5.tgz", + "integrity": "sha512-KaYU+S+ndVqyUnignHftkwc58o3uVU1jzczILJ1tN2YaIZpFIKBiP/x/j97E5MVPsaCloPbqWLB/8qCTVvT2qg==", + "dev": true, + "requires": { + "call-bind": "^1.0.2", + "define-properties": "^1.1.3", + "es-abstract": "^1.19.0" + } + }, + "array.prototype.flatmap": { + "version": "1.2.5", + "resolved": "https://registry.npmjs.org/array.prototype.flatmap/-/array.prototype.flatmap-1.2.5.tgz", + "integrity": "sha512-08u6rVyi1Lj7oqWbS9nUxliETrtIROT4XGTA4D/LWGten6E3ocm7cy9SIrmNHOL5XVbVuckUp3X6Xyg8/zpvHA==", + "dev": true, + "requires": { + "call-bind": "^1.0.0", + "define-properties": "^1.1.3", + "es-abstract": "^1.19.0" + } + }, + "asap": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/asap/-/asap-2.0.6.tgz", + "integrity": "sha512-BSHWgDSAiKs50o2Re8ppvp3seVHXSRM44cdSsT9FfNEUUZLOGWVCsiWaRPWM1Znn+mqZ1OfVZ3z3DWEzSp7hRA==", + "dev": true + }, + "astral-regex": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/astral-regex/-/astral-regex-1.0.0.tgz", + "integrity": "sha512-+Ryf6g3BKoRc7jfp7ad8tM4TtMiaWvbF/1/sQcZPkkS7ag3D5nMBCe2UfOTONtAkaG0tO0ij3C5Lwmf1EiyjHg==", + "dev": true + }, + "async": { + "version": "3.2.3", + "resolved": "https://registry.npmjs.org/async/-/async-3.2.3.tgz", + "integrity": "sha512-spZRyzKL5l5BZQrr/6m/SqFdBN0q3OCI0f9rjfBzCMBIP4p75P620rR3gTmaksNOhmzgdxcaxdNfMy6anrbM0g==" + }, + "asynckit": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", + "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==", + "dev": true + }, + "aws-sdk-client-mock": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/aws-sdk-client-mock/-/aws-sdk-client-mock-3.0.0.tgz", + "integrity": "sha512-4mBiWhuLYLZe1+K/iB8eYy5SAZyW2se+Keyh5u9QouMt6/qJ5SRZhss68xvUX5g3ApzROJ06QPRziYHP6buuvQ==", + "dev": true, + "requires": { + "@types/sinon": "^10.0.10", + "sinon": "^14.0.2", + "tslib": "^2.1.0" + }, + "dependencies": { + "tslib": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.3.1.tgz", + "integrity": "sha512-77EbyPPpMz+FRFRuAFlWMtmgUWGe9UOG2Z25NqCwiIjRhOf5iKGuzSe5P2w1laq+FkRy4p+PCuVkJSGkzTEKVw==", + "dev": true + } + } + }, + "aws-sdk-client-mock-jest": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/aws-sdk-client-mock-jest/-/aws-sdk-client-mock-jest-3.0.0.tgz", + "integrity": "sha512-oV1rBQZc4UumLbzZAhi8UAehUq+k75hkQYGLrVIP0iJj85Z9xw+EaSsmJke/KQ8Z3vng+Xv1xbounsxpvZpunQ==", + "dev": true, + "requires": { + "@types/jest": "^28.1.3", + "tslib": "^2.1.0" + }, + "dependencies": { + "tslib": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.4.1.tgz", + "integrity": "sha512-tGyy4dAjRIEwI7BzsB0lynWgOpfqjUdq91XXAlIWD2OwKBH7oCl/GZG/HT4BOHrTlPMOASlMQ7veyTqpmRcrNA==", + "dev": true + } + } + }, + "babel-eslint": { + "version": "10.1.0", + "resolved": "https://registry.npmjs.org/babel-eslint/-/babel-eslint-10.1.0.tgz", + "integrity": "sha512-ifWaTHQ0ce+448CYop8AdrQiBsGrnC+bMgfyKFdi6EsPLTAWG+QfyDeM6OH+FmWnKvEq5NnBMLvlBUPKQZoDSg==", + "dev": true, + "requires": { + "@babel/code-frame": "^7.0.0", + "@babel/parser": "^7.7.0", + "@babel/traverse": "^7.7.0", + "@babel/types": "^7.7.0", + "eslint-visitor-keys": "^1.0.0", + "resolve": "^1.12.0" + } + }, + "babel-jest": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/babel-jest/-/babel-jest-29.7.0.tgz", + "integrity": "sha512-BrvGY3xZSwEcCzKvKsCi2GgHqDqsYkOP4/by5xCgIwGXQxIEh+8ew3gmrE1y7XRR6LHZIj6yLYnUi/mm2KXKBg==", + "dev": true, + "requires": { + "@jest/transform": "^29.7.0", + "@types/babel__core": "^7.1.14", + "babel-plugin-istanbul": "^6.1.1", + "babel-preset-jest": "^29.6.3", + "chalk": "^4.0.0", + "graceful-fs": "^4.2.9", + "slash": "^3.0.0" + } + }, + "babel-plugin-istanbul": { + "version": "6.1.1", + "resolved": "https://registry.npmjs.org/babel-plugin-istanbul/-/babel-plugin-istanbul-6.1.1.tgz", + "integrity": "sha512-Y1IQok9821cC9onCx5otgFfRm7Lm+I+wwxOx738M/WLPZ9Q42m4IG5W0FNX8WLL2gYMZo3JkuXIH2DOpWM+qwA==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.0.0", + "@istanbuljs/load-nyc-config": "^1.0.0", + "@istanbuljs/schema": "^0.1.2", + "istanbul-lib-instrument": "^5.0.4", + "test-exclude": "^6.0.0" + }, + "dependencies": { + "istanbul-lib-instrument": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/istanbul-lib-instrument/-/istanbul-lib-instrument-5.2.1.tgz", + "integrity": "sha512-pzqtp31nLv/XFOzXGuvhCb8qhjmTVo5vjVk19XE4CRlSWz0KoeJ3bw9XsA7nOp9YBf4qHjwBxkDzKcME/J29Yg==", + "dev": true, + "requires": { + "@babel/core": "^7.12.3", + "@babel/parser": "^7.14.7", + "@istanbuljs/schema": "^0.1.2", + "istanbul-lib-coverage": "^3.2.0", + "semver": "^6.3.0" + } + } + } + }, + "babel-plugin-jest-hoist": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/babel-plugin-jest-hoist/-/babel-plugin-jest-hoist-29.6.3.tgz", + "integrity": "sha512-ESAc/RJvGTFEzRwOTT4+lNDk/GNHMkKbNzsvT0qKRfDyyYTskxB5rnU2njIDYVxXCBHHEI1c0YwHob3WaYujOg==", + "dev": true, + "requires": { + "@babel/template": "^7.3.3", + "@babel/types": "^7.3.3", + "@types/babel__core": "^7.1.14", + "@types/babel__traverse": "^7.0.6" + } + }, + "babel-preset-current-node-syntax": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/babel-preset-current-node-syntax/-/babel-preset-current-node-syntax-1.0.1.tgz", + "integrity": "sha512-M7LQ0bxarkxQoN+vz5aJPsLBn77n8QgTFmo8WK0/44auK2xlCXrYcUxHFxgU7qW5Yzw/CjmLRK2uJzaCd7LvqQ==", + "dev": true, + "requires": { + "@babel/plugin-syntax-async-generators": "^7.8.4", + "@babel/plugin-syntax-bigint": "^7.8.3", + "@babel/plugin-syntax-class-properties": "^7.8.3", + "@babel/plugin-syntax-import-meta": "^7.8.3", + "@babel/plugin-syntax-json-strings": "^7.8.3", + "@babel/plugin-syntax-logical-assignment-operators": "^7.8.3", + "@babel/plugin-syntax-nullish-coalescing-operator": "^7.8.3", + "@babel/plugin-syntax-numeric-separator": "^7.8.3", + "@babel/plugin-syntax-object-rest-spread": "^7.8.3", + "@babel/plugin-syntax-optional-catch-binding": "^7.8.3", + "@babel/plugin-syntax-optional-chaining": "^7.8.3", + "@babel/plugin-syntax-top-level-await": "^7.8.3" + } + }, + "babel-preset-jest": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/babel-preset-jest/-/babel-preset-jest-29.6.3.tgz", + "integrity": "sha512-0B3bhxR6snWXJZtR/RliHTDPRgn1sNHOR0yVtq/IiQFyuOVjFS+wuio/R4gSNkyYmKmJB4wGZv2NZanmKmTnNA==", + "dev": true, + "requires": { + "babel-plugin-jest-hoist": "^29.6.3", + "babel-preset-current-node-syntax": "^1.0.0" + } + }, + "balanced-match": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.0.tgz", + "integrity": "sha1-ibTRmasr7kneFk6gK4nORi1xt2c=", + "dev": true + }, + "base64-js": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz", + "integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==" + }, + "basic-auth": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/basic-auth/-/basic-auth-2.0.1.tgz", + "integrity": "sha512-NF+epuEdnUYVlGuhaxbbq+dvJttwLnGY+YixlXlME5KpQ5W3CnXA5cVTneY3SPbPDRkcjMbifrwmFYcClgOZeg==", + "requires": { + "safe-buffer": "5.1.2" + } + }, + "big-integer": { + "version": "1.6.51", + "resolved": "https://registry.npmjs.org/big-integer/-/big-integer-1.6.51.tgz", + "integrity": "sha512-GPEid2Y9QU1Exl1rpO9B2IPJGHPSupF5GnVIP0blYvNOMer2bTvSWs1jGOUg04hTmu67nmLsQ9TBo1puaotBHg==", + "dev": true + }, + "binary-extensions": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.2.0.tgz", + "integrity": "sha512-jDctJ/IVQbZoJykoeHbhXpOlNBqGNcwXJKJog42E5HDPUwQTSdjCHdihjj0DlnheQ7blbT6dHOafNAiS8ooQKA==", + "dev": true + }, + "body-parser": { + "version": "1.20.1", + "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.1.tgz", + "integrity": "sha512-jWi7abTbYwajOytWCQc37VulmWiRae5RyTpaCyDcS5/lMdtwSz5lOpDE67srw/HYe35f1z3fDQw+3txg7gNtWw==", + "requires": { + "bytes": "3.1.2", + "content-type": "~1.0.4", + "debug": "2.6.9", + "depd": "2.0.0", + "destroy": "1.2.0", + "http-errors": "2.0.0", + "iconv-lite": "0.4.24", + "on-finished": "2.4.1", + "qs": "6.11.0", + "raw-body": "2.5.1", + "type-is": "~1.6.18", + "unpipe": "1.0.0" + }, + "dependencies": { + "debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "requires": { + "ms": "2.0.0" + } + } + } + }, + "bowser": { + "version": "2.11.0", + "resolved": "https://registry.npmjs.org/bowser/-/bowser-2.11.0.tgz", + "integrity": "sha512-AlcaJBi/pqqJBIQ8U9Mcpc9i8Aqxn88Skv5d+xBX006BY5u8N3mGLHa5Lgppa7L/HfwgwLgZ6NYs+Ag6uUmJRA==" + }, + "bplist-parser": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/bplist-parser/-/bplist-parser-0.2.0.tgz", + "integrity": "sha512-z0M+byMThzQmD9NILRniCUXYsYpjwnlO8N5uCFaCqIOpqRsJCrQL9NK3JsD67CN5a08nF5oIL2bD6loTdHOuKw==", + "dev": true, + "requires": { + "big-integer": "^1.6.44" + } + }, + "brace-expansion": { + "version": "1.1.11", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", + "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "dev": true, + "requires": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "braces": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.2.tgz", + "integrity": "sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A==", + "dev": true, + "requires": { + "fill-range": "^7.0.1" + } + }, + "browserslist": { + "version": "4.22.1", + "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.22.1.tgz", + "integrity": "sha512-FEVc202+2iuClEhZhrWy6ZiAcRLvNMyYcxZ8raemul1DYVOVdFsbqckWLdsixQZCpJlwe77Z3UTalE7jsjnKfQ==", + "dev": true, + "requires": { + "caniuse-lite": "^1.0.30001541", + "electron-to-chromium": "^1.4.535", + "node-releases": "^2.0.13", + "update-browserslist-db": "^1.0.13" + } + }, + "bser": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/bser/-/bser-2.1.1.tgz", + "integrity": "sha512-gQxTNE/GAfIIrmHLUE3oJyp5FO6HRBfhjnw4/wMmA63ZGDJnWBmgY/lyQBpnDUkGmAhbSe39tx2d/iTOAfglwQ==", + "dev": true, + "requires": { + "node-int64": "^0.4.0" + } + }, + "buffer": { + "version": "5.6.0", + "resolved": "https://registry.npmjs.org/buffer/-/buffer-5.6.0.tgz", + "integrity": "sha512-/gDYp/UtU0eA1ys8bOs9J6a+E/KWIY+DZ+Q2WESNUA0jFRsJOc0SNUO6xJ5SGA1xueg3NL65W6s+NY5l9cunuw==", + "requires": { + "base64-js": "^1.0.2", + "ieee754": "^1.1.4" + } + }, + "buffer-equal-constant-time": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/buffer-equal-constant-time/-/buffer-equal-constant-time-1.0.1.tgz", + "integrity": "sha1-+OcRMvf/5uAaXJaXpMbz5I1cyBk=" + }, + "buffer-from": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz", + "integrity": "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==", + "dev": true + }, + "buffer-writer": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/buffer-writer/-/buffer-writer-2.0.0.tgz", + "integrity": "sha512-a7ZpuTZU1TRtnwyCNW3I5dc0wWNC3VR9S++Ewyk2HHZdrO3CQJqSpd+95Us590V6AL7JqUAH2IwZ/398PmNFgw==" + }, + "bundle-name": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/bundle-name/-/bundle-name-3.0.0.tgz", + "integrity": "sha512-PKA4BeSvBpQKQ8iPOGCSiell+N8P+Tf1DlwqmYhpe2gAhKPHn8EYOxVT+ShuGmhg8lN8XiSlS80yiExKXrURlw==", + "dev": true, + "requires": { + "run-applescript": "^5.0.0" + } + }, + "bytes": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz", + "integrity": "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==" + }, + "call-bind": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.2.tgz", + "integrity": "sha512-7O+FbCihrB5WGbFYesctwmTKae6rOiIzmz1icreWJ+0aA7LJfuqhEso2T9ncpcFtzMQtzXf2QGGueWJGTYsqrA==", + "requires": { + "function-bind": "^1.1.1", + "get-intrinsic": "^1.0.2" + } + }, + "callsites": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz", + "integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==", + "dev": true + }, + "camelcase": { + "version": "5.3.1", + "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-5.3.1.tgz", + "integrity": "sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg==", + "dev": true + }, + "caniuse-lite": { + "version": "1.0.30001543", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001543.tgz", + "integrity": "sha512-qxdO8KPWPQ+Zk6bvNpPeQIOH47qZSYdFZd6dXQzb2KzhnSXju4Kd7H1PkSJx6NICSMgo/IhRZRhhfPTHYpJUCA==", + "dev": true + }, + "chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, + "requires": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "dependencies": { + "ansi-styles": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.2.1.tgz", + "integrity": "sha512-9VGjrMsG1vePxcSweQsN20KY/c4zN0h9fLjqAbwbPfahM3t+NL+M9HC8xeXG2I8pX5NoamTGNuomEUFI7fcUjA==", + "dev": true, + "requires": { + "@types/color-name": "^1.1.1", + "color-convert": "^2.0.1" + } + }, + "color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "requires": { + "color-name": "~1.1.4" + } + }, + "color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true + }, + "has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true + }, + "supports-color": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.1.0.tgz", + "integrity": "sha512-oRSIpR8pxT1Wr2FquTNnGet79b3BWljqOuoW/h4oBhxJ/HUbX5nX6JSruTkvXDCFMwDPvsaTTbvMLKZWSy0R5g==", + "dev": true, + "requires": { + "has-flag": "^4.0.0" + } + } + } + }, + "char-regex": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/char-regex/-/char-regex-1.0.2.tgz", + "integrity": "sha512-kWWXztvZ5SBQV+eRgKFeh8q5sLuZY2+8WUIzlxWVTg+oGwY14qylx1KbKzHd8P6ZYkAg0xyIDU9JMHhyJMZ1jw==", + "dev": true + }, + "chardet": { + "version": "0.7.0", + "resolved": "https://registry.npmjs.org/chardet/-/chardet-0.7.0.tgz", + "integrity": "sha512-mT8iDcrh03qDGRRmoA2hmBJnxpllMR+0/0qlzjqZES6NdiWDcZkCNAk4rPFZ9Q85r27unkiNNg8ZOiwZXBHwcA==", + "dev": true + }, + "chokidar": { + "version": "3.5.3", + "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.5.3.tgz", + "integrity": "sha512-Dr3sfKRP6oTcjf2JmUmFJfeVMvXBdegxB0iVQ5eb2V10uFJUCAS8OByZdVAyVb8xXNz3GjjTgj9kLWsZTqE6kw==", + "dev": true, + "requires": { + "anymatch": "~3.1.2", + "braces": "~3.0.2", + "fsevents": "~2.3.2", + "glob-parent": "~5.1.2", + "is-binary-path": "~2.1.0", + "is-glob": "~4.0.1", + "normalize-path": "~3.0.0", + "readdirp": "~3.6.0" + } + }, + "ci-info": { + "version": "3.3.2", + "resolved": "https://registry.npmjs.org/ci-info/-/ci-info-3.3.2.tgz", + "integrity": "sha512-xmDt/QIAdeZ9+nfdPsaBCpMvHNLFiLdjj59qjqn+6iPe6YmHGQ35sBnQ8uslRBXFmXkiZQOJRjvQeoGppoTjjg==", + "dev": true + }, + "cjs-module-lexer": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/cjs-module-lexer/-/cjs-module-lexer-1.2.3.tgz", + "integrity": "sha512-0TNiGstbQmCFwt4akjjBg5pLRTSyj/PkWQ1ZoO2zntmg9yLqSRxwEa4iCfQLGjqhiqBfOJa7W/E8wfGrTDmlZQ==", + "dev": true + }, + "cli-cursor": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/cli-cursor/-/cli-cursor-3.1.0.tgz", + "integrity": "sha512-I/zHAwsKf9FqGoXM4WWRACob9+SNukZTd94DWF57E4toouRulbCxcUh6RKUEOQlYTHJnzkPMySvPNaaSLNfLZw==", + "dev": true, + "requires": { + "restore-cursor": "^3.1.0" + } + }, + "cli-width": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/cli-width/-/cli-width-3.0.0.tgz", + "integrity": "sha512-FxqpkPPwu1HjuN93Omfm4h8uIanXofW0RxVEW3k5RKx+mJJYSthzNhp32Kzxxy3YAEZ/Dc/EWN1vZRY0+kOhbw==", + "dev": true + }, + "cliui": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-8.0.1.tgz", + "integrity": "sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==", + "dev": true, + "requires": { + "string-width": "^4.2.0", + "strip-ansi": "^6.0.1", + "wrap-ansi": "^7.0.0" + } + }, + "co": { + "version": "4.6.0", + "resolved": "https://registry.npmjs.org/co/-/co-4.6.0.tgz", + "integrity": "sha512-QVb0dM5HvG+uaxitm8wONl7jltx8dqhfU33DcqtOZcLSVIKSDDLDi7+0LbAKiyI8hD9u42m2YxXSkMGWThaecQ==", + "dev": true + }, + "collect-v8-coverage": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/collect-v8-coverage/-/collect-v8-coverage-1.0.2.tgz", + "integrity": "sha512-lHl4d5/ONEbLlJvaJNtsF/Lz+WvB07u2ycqTYbdrq7UypDXailES4valYb2eWiJFxZlVmpGekfqoxQhzyFdT4Q==", + "dev": true + }, + "color": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/color/-/color-3.0.0.tgz", + "integrity": "sha512-jCpd5+s0s0t7p3pHQKpnJ0TpQKKdleP71LWcA0aqiljpiuAkOSUFN/dyH8ZwF0hRmFlrIuRhufds1QyEP9EB+w==", + "requires": { + "color-convert": "^1.9.1", + "color-string": "^1.5.2" + } + }, + "color-convert": { + "version": "1.9.3", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz", + "integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==", + "requires": { + "color-name": "1.1.3" + } + }, + "color-name": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz", + "integrity": "sha1-p9BVi9icQveV3UIyj3QIMcpTvCU=" + }, + "color-string": { + "version": "1.5.5", + "resolved": "https://registry.npmjs.org/color-string/-/color-string-1.5.5.tgz", + "integrity": "sha512-jgIoum0OfQfq9Whcfc2z/VhCNcmQjWbey6qBX0vqt7YICflUmBCh9E9CiQD5GSJ+Uehixm3NUwHVhqUAWRivZg==", + "requires": { + "color-name": "^1.0.0", + "simple-swizzle": "^0.2.2" + } + }, + "colorette": { + "version": "2.0.19", + "resolved": "https://registry.npmjs.org/colorette/-/colorette-2.0.19.tgz", + "integrity": "sha512-3tlv/dIP7FWvj3BsbHrGLJ6l/oKh1O3TcgBqMn+yyCagOxc23fyzDS6HypQbgxWbkpDnf52p1LuR4eWDQ/K9WQ==" + }, + "colorspace": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/colorspace/-/colorspace-1.1.2.tgz", + "integrity": "sha512-vt+OoIP2d76xLhjwbBaucYlNSpPsrJWPlBTtwCpQKIu6/CSMutyzX93O/Do0qzpH3YoHEes8YEFXyZ797rEhzQ==", + "requires": { + "color": "3.0.x", + "text-hex": "1.0.x" + } + }, + "combined-stream": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", + "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", + "dev": true, + "requires": { + "delayed-stream": "~1.0.0" + } + }, + "commander": { + "version": "10.0.1", + "resolved": "https://registry.npmjs.org/commander/-/commander-10.0.1.tgz", + "integrity": "sha512-y4Mg2tXshplEbSGzx7amzPwKKOCGuoSRP/CjEdwwk0FOGlUbq6lKuoyDZTNZkmxHdJtp54hdfY/JUrdL7Xfdug==" + }, + "component-emitter": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/component-emitter/-/component-emitter-1.3.0.tgz", + "integrity": "sha512-Rd3se6QB+sO1TwqZjscQrurpEPIfO0/yYnSin6Q/rD3mOutHvUrCAhJub3r90uNb+SESBuE0QYoB90YdfatsRg==", + "dev": true + }, + "compressible": { + "version": "2.0.18", + "resolved": "https://registry.npmjs.org/compressible/-/compressible-2.0.18.tgz", + "integrity": "sha512-AF3r7P5dWxL8MxyITRMlORQNaOA2IkAFaTr4k7BUumjPtRpGDTZpl0Pb1XCO6JeDCBdp126Cgs9sMxqSjgYyRg==", + "requires": { + "mime-db": ">= 1.43.0 < 2" + } + }, + "compression": { + "version": "1.7.4", + "resolved": "https://registry.npmjs.org/compression/-/compression-1.7.4.tgz", + "integrity": "sha512-jaSIDzP9pZVS4ZfQ+TzvtiWhdpFhE2RDHz8QJkpX9SIpLq88VueF5jJw6t+6CUQcAoA6t+x89MLrWAqpfDE8iQ==", + "requires": { + "accepts": "~1.3.5", + "bytes": "3.0.0", + "compressible": "~2.0.16", + "debug": "2.6.9", + "on-headers": "~1.0.2", + "safe-buffer": "5.1.2", + "vary": "~1.1.2" + }, + "dependencies": { + "bytes": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.0.0.tgz", + "integrity": "sha1-0ygVQE1olpn4Wk6k+odV3ROpYEg=" + }, + "debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "requires": { + "ms": "2.0.0" + } + } + } + }, + "concat-map": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", + "integrity": "sha1-2Klr13/Wjfd5OnMDajug1UBdR3s=", + "dev": true + }, + "config": { + "version": "3.3.9", + "resolved": "https://registry.npmjs.org/config/-/config-3.3.9.tgz", + "integrity": "sha512-G17nfe+cY7kR0wVpc49NCYvNtelm/pPy8czHoFkAgtV1lkmcp7DHtWCdDu+C9Z7gb2WVqa9Tm3uF9aKaPbCfhg==", + "requires": { + "json5": "^2.2.3" + } + }, + "content-disposition": { + "version": "0.5.4", + "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.4.tgz", + "integrity": "sha512-FveZTNuGw04cxlAiWbzi6zTAL/lhehaWbTtgluJh4/E95DqMwTmha3KZN1aAWA8cFIhHzMZUvLevkw5Rqk+tSQ==", + "requires": { + "safe-buffer": "5.2.1" + }, + "dependencies": { + "safe-buffer": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", + "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==" + } + } + }, + "content-type": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.4.tgz", + "integrity": "sha512-hIP3EEPs8tB9AT1L+NUqtwOAps4mk2Zob89MWXMHjHWg9milF/j4osnnQLXBCBFBk/tvIG/tUc9mOUJiPBhPXA==" + }, + "convert-source-map": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz", + "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==", + "dev": true + }, + "cookie": { + "version": "0.5.0", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.5.0.tgz", + "integrity": "sha512-YZ3GUyn/o8gfKJlnlX7g7xq4gyO6OSuhGPKaaGssGB2qgDUS0gPgtTvoyZLTt9Ab6dC4hfc9dV5arkvc/OCmrw==" + }, + "cookie-signature": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.6.tgz", + "integrity": "sha1-4wOogrNCzD7oylE6eZmXNNqzriw=" + }, + "cookiejar": { + "version": "2.1.4", + "resolved": "https://registry.npmjs.org/cookiejar/-/cookiejar-2.1.4.tgz", + "integrity": "sha512-LDx6oHrK+PhzLKJU9j5S7/Y3jM/mUHvD/DeI1WQmJn652iPC5Y4TBzC9l+5OMOXlyTTA+SmVUPm0HQUwpD5Jqw==", + "dev": true + }, + "cors": { + "version": "2.8.5", + "resolved": "https://registry.npmjs.org/cors/-/cors-2.8.5.tgz", + "integrity": "sha512-KIHbLJqu73RGr/hnbrO9uBeixNGuvSQjul/jdFvS/KFSIH1hWVd1ng7zOHx+YrEfInLG7q4n6GHQ9cDtxv/P6g==", + "requires": { + "object-assign": "^4", + "vary": "^1" + } + }, + "create-jest": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/create-jest/-/create-jest-29.7.0.tgz", + "integrity": "sha512-Adz2bdH0Vq3F53KEMJOoftQFutWCukm6J24wbPWRO4k1kMY7gS7ds/uoJkNuV8wDCtWWnuwGcJwpWcih+zEW1Q==", + "dev": true, + "requires": { + "@jest/types": "^29.6.3", + "chalk": "^4.0.0", + "exit": "^0.1.2", + "graceful-fs": "^4.2.9", + "jest-config": "^29.7.0", + "jest-util": "^29.7.0", + "prompts": "^2.0.1" + }, + "dependencies": { + "@jest/schemas": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/@jest/schemas/-/schemas-29.6.3.tgz", + "integrity": "sha512-mo5j5X+jIZmJQveBKeS/clAueipV7KgiX1vMgCxam1RNYiqE1w62n0/tJJnHtjW8ZHcQco5gY85jA3mi0L+nSA==", + "dev": true, + "requires": { + "@sinclair/typebox": "^0.27.8" + } + }, + "@jest/types": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/@jest/types/-/types-29.6.3.tgz", + "integrity": "sha512-u3UPsIilWKOM3F9CXtrG8LEJmNxwoCQC/XVj4IKYXvvpx7QIi/Kg1LI5uDmDpKlac62NUtX7eLjRh+jVZcLOzw==", + "dev": true, + "requires": { + "@jest/schemas": "^29.6.3", + "@types/istanbul-lib-coverage": "^2.0.0", + "@types/istanbul-reports": "^3.0.0", + "@types/node": "*", + "@types/yargs": "^17.0.8", + "chalk": "^4.0.0" + } + }, + "@sinclair/typebox": { + "version": "0.27.8", + "resolved": "https://registry.npmjs.org/@sinclair/typebox/-/typebox-0.27.8.tgz", + "integrity": "sha512-+Fj43pSMwJs4KRrH/938Uf+uAELIgVBmQzg/q1YG10djyfA3TnrU8N8XzqCh/okZdszqBQTZf96idMfE5lnwTA==", + "dev": true + }, + "jest-util": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-util/-/jest-util-29.7.0.tgz", + "integrity": "sha512-z6EbKajIpqGKU56y5KBUgy1dt1ihhQJgWzUlZHArA/+X2ad7Cb5iF+AK1EWVL/Bo7Rz9uurpqw6SiBCefUbCGA==", + "dev": true, + "requires": { + "@jest/types": "^29.6.3", + "@types/node": "*", + "chalk": "^4.0.0", + "ci-info": "^3.2.0", + "graceful-fs": "^4.2.9", + "picomatch": "^2.2.3" + } + } + } + }, + "cross-spawn": { + "version": "7.0.3", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.3.tgz", + "integrity": "sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w==", + "dev": true, + "requires": { + "path-key": "^3.1.0", + "shebang-command": "^2.0.0", + "which": "^2.0.1" + } + }, + "date-fns": { + "version": "2.30.0", + "resolved": "https://registry.npmjs.org/date-fns/-/date-fns-2.30.0.tgz", + "integrity": "sha512-fnULvOpxnC5/Vg3NCiWelDsLiUc9bRwAPs/+LfTLNvetFCtCTN+yQz15C/fs4AwX1R9K5GLtLfn8QW+dWisaAw==", + "requires": { + "@babel/runtime": "^7.21.0" + } + }, + "db-errors": { + "version": "0.2.3", + "resolved": "https://registry.npmjs.org/db-errors/-/db-errors-0.2.3.tgz", + "integrity": "sha512-OOgqgDuCavHXjYSJoV2yGhv6SeG8nk42aoCSoyXLZUH7VwFG27rxbavU1z+VrZbZjphw5UkDQwUlD21MwZpUng==" + }, + "debug": { + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.1.tgz", + "integrity": "sha512-doEwdvm4PCeK4K3RQN2ZC2BYUBaxwLARCqZmMjtF8a51J2Rb0xpVloFRnCODwqjpwnAoao4pelN8l3RJdv3gRQ==", + "dev": true, + "requires": { + "ms": "2.1.2" + }, + "dependencies": { + "ms": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", + "dev": true + } + } + }, + "dedent": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/dedent/-/dedent-1.5.1.tgz", + "integrity": "sha512-+LxW+KLWxu3HW3M2w2ympwtqPrqYRzU8fqi6Fhd18fBALe15blJPI/I4+UHveMVG6lJqB4JNd4UG0S5cnVHwIg==", + "dev": true + }, + "deep-is": { + "version": "0.1.3", + "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.3.tgz", + "integrity": "sha1-s2nW+128E+7PUk+RsHD+7cNXzzQ=", + "dev": true + }, + "deepmerge": { + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/deepmerge/-/deepmerge-4.3.1.tgz", + "integrity": "sha512-3sUqbMEc77XqpdNO7FRyRog+eW3ph+GYCbj+rK+uYyRMuwsVy0rMiVtPn+QJlKFvWP/1PYpapqYn0Me2knFn+A==", + "dev": true + }, + "default-browser": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/default-browser/-/default-browser-4.0.0.tgz", + "integrity": "sha512-wX5pXO1+BrhMkSbROFsyxUm0i/cJEScyNhA4PPxc41ICuv05ZZB/MX28s8aZx6xjmatvebIapF6hLEKEcpneUA==", + "dev": true, + "requires": { + "bundle-name": "^3.0.0", + "default-browser-id": "^3.0.0", + "execa": "^7.1.1", + "titleize": "^3.0.0" + }, + "dependencies": { + "execa": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/execa/-/execa-7.2.0.tgz", + "integrity": "sha512-UduyVP7TLB5IcAQl+OzLyLcS/l32W/GLg+AhHJ+ow40FOk2U3SAllPwR44v4vmdFwIWqpdwxxpQbF1n5ta9seA==", + "dev": true, + "requires": { + "cross-spawn": "^7.0.3", + "get-stream": "^6.0.1", + "human-signals": "^4.3.0", + "is-stream": "^3.0.0", + "merge-stream": "^2.0.0", + "npm-run-path": "^5.1.0", + "onetime": "^6.0.0", + "signal-exit": "^3.0.7", + "strip-final-newline": "^3.0.0" + } + }, + "get-stream": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-6.0.1.tgz", + "integrity": "sha512-ts6Wi+2j3jQjqi70w5AlN8DFnkSwC+MqmxEzdEALB2qXZYV3X/b1CTfgPLGJNMeAWxdPfU8FO1ms3NUfaHCPYg==", + "dev": true + }, + "human-signals": { + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/human-signals/-/human-signals-4.3.1.tgz", + "integrity": "sha512-nZXjEF2nbo7lIw3mgYjItAfgQXog3OjJogSbKa2CQIIvSGWcKgeJnQlNXip6NglNzYH45nSRiEVimMvYL8DDqQ==", + "dev": true + }, + "is-stream": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-3.0.0.tgz", + "integrity": "sha512-LnQR4bZ9IADDRSkvpqMGvt/tEJWclzklNgSw48V5EAaAeDd6qGvN8ei6k5p0tvxSR171VmGyHuTiAOfxAbr8kA==", + "dev": true + }, + "mimic-fn": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-4.0.0.tgz", + "integrity": "sha512-vqiC06CuhBTUdZH+RYl8sFrL096vA45Ok5ISO6sE/Mr1jRbGH4Csnhi8f3wKVl7x8mO4Au7Ir9D3Oyv1VYMFJw==", + "dev": true + }, + "npm-run-path": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-5.1.0.tgz", + "integrity": "sha512-sJOdmRGrY2sjNTRMbSvluQqg+8X7ZK61yvzBEIDhz4f8z1TZFYABsqjjCBd/0PUNE9M6QDgHJXQkGUEm7Q+l9Q==", + "dev": true, + "requires": { + "path-key": "^4.0.0" + } + }, + "onetime": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/onetime/-/onetime-6.0.0.tgz", + "integrity": "sha512-1FlR+gjXK7X+AsAHso35MnyN5KqGwJRi/31ft6x0M194ht7S+rWAvd7PHss9xSKMzE0asv1pyIHaJYq+BbacAQ==", + "dev": true, + "requires": { + "mimic-fn": "^4.0.0" + } + }, + "path-key": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-4.0.0.tgz", + "integrity": "sha512-haREypq7xkM7ErfgIyA0z+Bj4AGKlMSdlQE2jvJo6huWD1EdkKYV+G/T4nq0YEF2vgTT8kqMFKo1uHn950r4SQ==", + "dev": true + }, + "strip-final-newline": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/strip-final-newline/-/strip-final-newline-3.0.0.tgz", + "integrity": "sha512-dOESqjYr96iWYylGObzd39EuNTa5VJxyvVAEm5Jnh7KGo75V43Hk1odPQkNDyXNmUR6k+gEiDVXnjB8HJ3crXw==", + "dev": true + } + } + }, + "default-browser-id": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/default-browser-id/-/default-browser-id-3.0.0.tgz", + "integrity": "sha512-OZ1y3y0SqSICtE8DE4S8YOE9UZOJ8wO16fKWVP5J1Qz42kV9jcnMVFrEE/noXb/ss3Q4pZIH79kxofzyNNtUNA==", + "dev": true, + "requires": { + "bplist-parser": "^0.2.0", + "untildify": "^4.0.0" + } + }, + "define-lazy-prop": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/define-lazy-prop/-/define-lazy-prop-3.0.0.tgz", + "integrity": "sha512-N+MeXYoqr3pOgn8xfyRPREN7gHakLYjhsHhWGT3fWAiL4IkAt0iDw14QiiEm2bE30c5XX5q0FtAA3CK5f9/BUg==", + "dev": true + }, + "define-properties": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/define-properties/-/define-properties-1.1.3.tgz", + "integrity": "sha512-3MqfYKj2lLzdMSf8ZIZE/V+Zuy+BgD6f164e8K2w7dgnpKArBDerGYpM46IYYcjnkdPNMjPk9A6VFB8+3SKlXQ==", + "dev": true, + "requires": { + "object-keys": "^1.0.12" + } + }, + "delayed-stream": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", + "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==", + "dev": true + }, + "depd": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz", + "integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==" + }, + "destroy": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/destroy/-/destroy-1.2.0.tgz", + "integrity": "sha512-2sJGJTaXIIaR1w4iJSNoN0hnMY7Gpc/n8D4qSCJw8QqFWXf7cuAgnEHxBpweaVcPevC2l3KpjYCx3NypQQgaJg==" + }, + "detect-newline": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/detect-newline/-/detect-newline-3.1.0.tgz", + "integrity": "sha512-TLz+x/vEXm/Y7P7wn1EJFNLxYpUD4TgMosxY6fAVJUnJMbupHBOncxyWUG9OpTaH9EBD7uFI5LfEgmMOc54DsA==", + "dev": true + }, + "dezalgo": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/dezalgo/-/dezalgo-1.0.4.tgz", + "integrity": "sha512-rXSP0bf+5n0Qonsb+SVVfNfIsimO4HEtmnIpPHY8Q1UCzKlQrDMfdobr8nJOOsRgWCyMRqeSBQzmWUMq7zvVig==", + "dev": true, + "requires": { + "asap": "^2.0.0", + "wrappy": "1" + } + }, + "diff": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/diff/-/diff-5.1.0.tgz", + "integrity": "sha512-D+mk+qE8VC/PAUrlAU34N+VfXev0ghe5ywmpqrawphmVZc1bEfn56uo9qpyGp1p4xpzOHkSW4ztBd6L7Xx4ACw==", + "dev": true + }, + "diff-sequences": { + "version": "28.1.1", + "resolved": "https://registry.npmjs.org/diff-sequences/-/diff-sequences-28.1.1.tgz", + "integrity": "sha512-FU0iFaH/E23a+a718l8Qa/19bF9p06kgE0KipMOMadwa3SjnaElKzPaUC0vnibs6/B/9ni97s61mcejk8W1fQw==", + "dev": true + }, + "doctrine": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/doctrine/-/doctrine-3.0.0.tgz", + "integrity": "sha512-yS+Q5i3hBf7GBkd4KG8a7eBNNWNGLTaEwwYWUijIYM7zrlYDM0BFXHjjPWlWZ1Rg7UaddZeIDmi9jF3HmqiQ2w==", + "dev": true, + "requires": { + "esutils": "^2.0.2" + } + }, + "ecdsa-sig-formatter": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/ecdsa-sig-formatter/-/ecdsa-sig-formatter-1.0.11.tgz", + "integrity": "sha512-nagl3RYrbNv6kQkeJIpt6NJZy8twLB/2vtz6yN9Z4vRKHN4/QZJIEbqohALSgwKdnksuY3k5Addp5lg8sVoVcQ==", + "requires": { + "safe-buffer": "^5.0.1" + } + }, + "ee-first": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", + "integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==" + }, + "electron-to-chromium": { + "version": "1.4.540", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.4.540.tgz", + "integrity": "sha512-aoCqgU6r9+o9/S7wkcSbmPRFi7OWZWiXS9rtjEd+Ouyu/Xyw5RSq2XN8s5Qp8IaFOLiRrhQCphCIjAxgG3eCAg==", + "dev": true + }, + "emittery": { + "version": "0.13.1", + "resolved": "https://registry.npmjs.org/emittery/-/emittery-0.13.1.tgz", + "integrity": "sha512-DeWwawk6r5yR9jFgnDKYt4sLS0LmHJJi3ZOnb5/JdbYwj3nW+FxQnHIjhBKz8YLC7oRNPVM9NQ47I3CVx34eqQ==", + "dev": true + }, + "emoji-regex": { + "version": "7.0.3", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-7.0.3.tgz", + "integrity": "sha512-CwBLREIQ7LvYFB0WyRvwhq5N5qPhc6PMjD6bYggFlI5YyDgl+0vxq5VHbMOFqLg7hfWzmu8T5Z1QofhmTIhItA==", + "dev": true + }, + "enabled": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/enabled/-/enabled-2.0.0.tgz", + "integrity": "sha512-AKrN98kuwOzMIdAizXGI86UFBoo26CL21UM763y1h/GMSJ4/OHU9k2YlsmBpyScFo/wbLzWQJBMCW4+IO3/+OQ==" + }, + "encodeurl": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-1.0.2.tgz", + "integrity": "sha512-TPJXq8JqFaVYm2CWmPvnP2Iyo4ZSM7/QKcSmuMLDObfpH5fi7RUGmd/rTDf+rut/saiDiQEeVTNgAmJEdAOx0w==" + }, + "error-ex": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/error-ex/-/error-ex-1.3.2.tgz", + "integrity": "sha512-7dFHNmqeFSEt2ZBsCriorKnn3Z2pj+fd9kmI6QoWw4//DL+icEBfc0U7qJCisqrTsKTjw4fNFy2pW9OqStD84g==", + "dev": true, + "requires": { + "is-arrayish": "^0.2.1" + } + }, + "es-abstract": { + "version": "1.19.1", + "resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.19.1.tgz", + "integrity": "sha512-2vJ6tjA/UfqLm2MPs7jxVybLoB8i1t1Jd9R3kISld20sIxPcTbLuggQOUxeWeAvIUkduv/CfMjuh4WmiXr2v9w==", + "dev": true, + "requires": { + "call-bind": "^1.0.2", + "es-to-primitive": "^1.2.1", + "function-bind": "^1.1.1", + "get-intrinsic": "^1.1.1", + "get-symbol-description": "^1.0.0", + "has": "^1.0.3", + "has-symbols": "^1.0.2", + "internal-slot": "^1.0.3", + "is-callable": "^1.2.4", + "is-negative-zero": "^2.0.1", + "is-regex": "^1.1.4", + "is-shared-array-buffer": "^1.0.1", + "is-string": "^1.0.7", + "is-weakref": "^1.0.1", + "object-inspect": "^1.11.0", + "object-keys": "^1.1.1", + "object.assign": "^4.1.2", + "string.prototype.trimend": "^1.0.4", + "string.prototype.trimstart": "^1.0.4", + "unbox-primitive": "^1.0.1" + } + }, + "es-to-primitive": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/es-to-primitive/-/es-to-primitive-1.2.1.tgz", + "integrity": "sha512-QCOllgZJtaUo9miYBcLChTUaHNjJF3PYs1VidD7AwiEj1kYxKeQTctLAezAOH5ZKRH0g2IgPn6KwB4IT8iRpvA==", + "dev": true, + "requires": { + "is-callable": "^1.1.4", + "is-date-object": "^1.0.1", + "is-symbol": "^1.0.2" + } + }, + "escalade": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.1.1.tgz", + "integrity": "sha512-k0er2gUkLf8O0zKJiAhmkTnJlTvINGv7ygDNPbeIsX/TJjGJZHuh9B2UxbsaEkmlEo9MfhrSzmhIlhRlI2GXnw==" + }, + "escape-html": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz", + "integrity": "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==" + }, + "escape-string-regexp": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", + "integrity": "sha1-G2HAViGQqN/2rjuyzwIAyhMLhtQ=" + }, + "eslint": { + "version": "8.50.0", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-8.50.0.tgz", + "integrity": "sha512-FOnOGSuFuFLv/Sa+FDVRZl4GGVAAFFi8LecRsI5a1tMO5HIE8nCm4ivAlzt4dT3ol/PaaGC0rJEEXQmHJBGoOg==", + "dev": true, + "requires": { + "@eslint-community/eslint-utils": "^4.2.0", + "@eslint-community/regexpp": "^4.6.1", + "@eslint/eslintrc": "^2.1.2", + "@eslint/js": "8.50.0", + "@humanwhocodes/config-array": "^0.11.11", + "@humanwhocodes/module-importer": "^1.0.1", + "@nodelib/fs.walk": "^1.2.8", + "ajv": "^6.12.4", + "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.2.2", + "eslint-visitor-keys": "^3.4.3", + "espree": "^9.6.1", + "esquery": "^1.4.2", + "esutils": "^2.0.2", + "fast-deep-equal": "^3.1.3", + "file-entry-cache": "^6.0.1", + "find-up": "^5.0.0", + "glob-parent": "^6.0.2", + "globals": "^13.19.0", + "graphemer": "^1.4.0", + "ignore": "^5.2.0", + "imurmurhash": "^0.1.4", + "is-glob": "^4.0.0", + "is-path-inside": "^3.0.3", + "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.3", + "strip-ansi": "^6.0.1", + "text-table": "^0.2.0" + }, + "dependencies": { + "debug": { + "version": "4.3.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.3.tgz", + "integrity": "sha512-/zxw5+vh1Tfv+4Qn7a5nsbcJKPaSvCDhojn6FEl9vupwK2VCSDtEiEtqr8DFtzYFOdz63LBkxec7DYuc2jon6Q==", + "dev": true, + "requires": { + "ms": "2.1.2" + } + }, + "escape-string-regexp": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", + "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==", + "dev": true + }, + "eslint-scope": { + "version": "7.2.2", + "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-7.2.2.tgz", + "integrity": "sha512-dOt21O7lTMhDM+X9mB4GX+DZrZtCUJPL/wlcTqxyrx5IvO0IYtILdtrQGQp+8n5S0gwSVmOf9NQrjMOgfQZlIg==", + "dev": true, + "requires": { + "esrecurse": "^4.3.0", + "estraverse": "^5.2.0" + } + }, + "eslint-visitor-keys": { + "version": "3.4.3", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.4.3.tgz", + "integrity": "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==", + "dev": true + }, + "estraverse": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz", + "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==", + "dev": true + }, + "file-entry-cache": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-6.0.1.tgz", + "integrity": "sha512-7Gps/XWymbLk2QLYK4NzpMOrYjMhdIxXuIvy2QBsLE6ljuodKvdkWs/cpyJJ3CVIVpH0Oi1Hvg1ovbMzLdFBBg==", + "dev": true, + "requires": { + "flat-cache": "^3.0.4" + } + }, + "find-up": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz", + "integrity": "sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==", + "dev": true, + "requires": { + "locate-path": "^6.0.0", + "path-exists": "^4.0.0" + } + }, + "flat-cache": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-3.0.4.tgz", + "integrity": "sha512-dm9s5Pw7Jc0GvMYbshN6zchCA9RgQlzzEZX3vylR9IqFfS8XciblUXOKfW6SiuJ0e13eDYZoZV5wdrev7P3Nwg==", + "dev": true, + "requires": { + "flatted": "^3.1.0", + "rimraf": "^3.0.2" + } + }, + "flatted": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.1.1.tgz", + "integrity": "sha512-zAoAQiudy+r5SvnSw3KJy5os/oRJYHzrzja/tBDqrZtNhUw8bt6y8OBzMWcjWr+8liV8Eb6yOhw8WZ7VFZ5ZzA==", + "dev": true + }, + "glob-parent": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz", + "integrity": "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==", + "dev": true, + "requires": { + "is-glob": "^4.0.3" + } + }, + "globals": { + "version": "13.20.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-13.20.0.tgz", + "integrity": "sha512-Qg5QtVkCy/kv3FUSlu4ukeZDVf9ee0iXLAUYX13gbR17bnejFTzr4iS9bY7kwCf1NztRNm1t91fjOiyx4CSwPQ==", + "dev": true, + "requires": { + "type-fest": "^0.20.2" + } + }, + "locate-path": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz", + "integrity": "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==", + "dev": true, + "requires": { + "p-locate": "^5.0.0" + } + }, + "ms": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", + "dev": true + }, + "p-limit": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", + "integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==", + "dev": true, + "requires": { + "yocto-queue": "^0.1.0" + } + }, + "p-locate": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-5.0.0.tgz", + "integrity": "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==", + "dev": true, + "requires": { + "p-limit": "^3.0.2" + } + }, + "path-exists": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", + "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", + "dev": true + }, + "type-fest": { + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.20.2.tgz", + "integrity": "sha512-Ne+eE4r0/iWnpAxD852z3A+N0Bt5RN//NjJwRd2VFHEmrywxf5vsZlh4R6lixl6B+wz/8d+maTSAkN1FIkI3LQ==", + "dev": true + } + } + }, + "eslint-config-esnext": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/eslint-config-esnext/-/eslint-config-esnext-4.1.0.tgz", + "integrity": "sha512-GhfVEXdqYKEIIj7j+Fw2SQdL9qyZMekgXfq6PyXM66cQw0B435ddjz3P3kxOBVihMRJ0xGYjosaveQz5Y6z0uA==", + "dev": true, + "requires": { + "babel-eslint": "^10.0.1", + "eslint": "^6.8.0", + "eslint-plugin-babel": "^5.2.1", + "eslint-plugin-import": "^2.14.0" + }, + "dependencies": { + "ansi-regex": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-4.1.1.tgz", + "integrity": "sha512-ILlv4k/3f6vfQ4OoP2AGvirOktlQ98ZEL1k9FaQjxa3L1abBgbuTDAdPOpvbGncC0BTVQrl+OM8xZGK6tWXt7g==", + "dev": true + }, + "argparse": { + "version": "1.0.10", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-1.0.10.tgz", + "integrity": "sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==", + "dev": true, + "requires": { + "sprintf-js": "~1.0.2" + } + }, + "chalk": { + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", + "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==", + "dev": true, + "requires": { + "ansi-styles": "^3.2.1", + "escape-string-regexp": "^1.0.5", + "supports-color": "^5.3.0" + } + }, + "cross-spawn": { + "version": "6.0.5", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-6.0.5.tgz", + "integrity": "sha512-eTVLrBSt7fjbDygz805pMnstIs2VTBNkRm0qxZd+M7A5XDdxVRWO5MxGBXZhjY4cqLYLdtrGqRf8mBPmzwSpWQ==", + "dev": true, + "requires": { + "nice-try": "^1.0.4", + "path-key": "^2.0.1", + "semver": "^5.5.0", + "shebang-command": "^1.2.0", + "which": "^1.2.9" + }, + "dependencies": { + "semver": { + "version": "5.7.2", + "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.2.tgz", + "integrity": "sha512-cBznnQ9KjJqU67B52RMC65CMarK2600WFnbkcaiwWq3xy/5haFJlshgnpjovMVJ+Hff49d8GEn0b87C5pDQ10g==", + "dev": true + } + } + }, + "eslint": { + "version": "6.8.0", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-6.8.0.tgz", + "integrity": "sha512-K+Iayyo2LtyYhDSYwz5D5QdWw0hCacNzyq1Y821Xna2xSJj7cijoLLYmLxTQgcgZ9mC61nryMy9S7GRbYpI5Ig==", + "dev": true, + "requires": { + "@babel/code-frame": "^7.0.0", + "ajv": "^6.10.0", + "chalk": "^2.1.0", + "cross-spawn": "^6.0.5", + "debug": "^4.0.1", + "doctrine": "^3.0.0", + "eslint-scope": "^5.0.0", + "eslint-utils": "^1.4.3", + "eslint-visitor-keys": "^1.1.0", + "espree": "^6.1.2", + "esquery": "^1.0.1", + "esutils": "^2.0.2", + "file-entry-cache": "^5.0.1", + "functional-red-black-tree": "^1.0.1", + "glob-parent": "^5.0.0", + "globals": "^12.1.0", + "ignore": "^4.0.6", + "import-fresh": "^3.0.0", + "imurmurhash": "^0.1.4", + "inquirer": "^7.0.0", + "is-glob": "^4.0.0", + "js-yaml": "^3.13.1", + "json-stable-stringify-without-jsonify": "^1.0.1", + "levn": "^0.3.0", + "lodash": "^4.17.14", + "minimatch": "^3.0.4", + "mkdirp": "^0.5.1", + "natural-compare": "^1.4.0", + "optionator": "^0.8.3", + "progress": "^2.0.0", + "regexpp": "^2.0.1", + "semver": "^6.1.2", + "strip-ansi": "^5.2.0", + "strip-json-comments": "^3.0.1", + "table": "^5.2.3", + "text-table": "^0.2.0", + "v8-compile-cache": "^2.0.3" + }, + "dependencies": { + "js-yaml": { + "version": "3.14.1", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.14.1.tgz", + "integrity": "sha512-okMH7OXXJ7YrN9Ok3/SXrnu4iX9yOk+25nqX4imS2npuvTYDmo/QEZoqwZkYaIDk3jVvBOTOIEgEhaLOynBS9g==", + "dev": true, + "requires": { + "argparse": "^1.0.7", + "esprima": "^4.0.0" + } + } + } + }, + "eslint-plugin-import": { + "version": "2.25.3", + "resolved": "https://registry.npmjs.org/eslint-plugin-import/-/eslint-plugin-import-2.25.3.tgz", + "integrity": "sha512-RzAVbby+72IB3iOEL8clzPLzL3wpDrlwjsTBAQXgyp5SeTqqY+0bFubwuo+y/HLhNZcXV4XqTBO4LGsfyHIDXg==", + "dev": true, + "requires": { + "array-includes": "^3.1.4", + "array.prototype.flat": "^1.2.5", + "debug": "^2.6.9", + "doctrine": "^2.1.0", + "eslint-import-resolver-node": "^0.3.6", + "eslint-module-utils": "^2.7.1", + "has": "^1.0.3", + "is-core-module": "^2.8.0", + "is-glob": "^4.0.3", + "minimatch": "^3.0.4", + "object.values": "^1.1.5", + "resolve": "^1.20.0", + "tsconfig-paths": "^3.11.0" + }, + "dependencies": { + "debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "dev": true, + "requires": { + "ms": "2.0.0" + } + }, + "doctrine": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/doctrine/-/doctrine-2.1.0.tgz", + "integrity": "sha512-35mSku4ZXK0vfCuHEDAwt55dg2jNajHZ1odvF+8SSr82EsZY4QmXfuWso8oEd8zRhVObSN18aM0CjSdoBX7zIw==", + "dev": true, + "requires": { + "esutils": "^2.0.2" + } + } + } + }, + "eslint-utils": { + "version": "1.4.3", + "resolved": "https://registry.npmjs.org/eslint-utils/-/eslint-utils-1.4.3.tgz", + "integrity": "sha512-fbBN5W2xdY45KulGXmLHZ3c3FHfVYmKg0IrAKGOkT/464PQsx2UeIzfz1RmEci+KLm1bBaAzZAh8+/E+XAeZ8Q==", + "dev": true, + "requires": { + "eslint-visitor-keys": "^1.1.0" + } + }, + "espree": { + "version": "6.2.1", + "resolved": "https://registry.npmjs.org/espree/-/espree-6.2.1.tgz", + "integrity": "sha512-ysCxRQY3WaXJz9tdbWOwuWr5Y/XrPTGX9Kiz3yoUXwW0VZ4w30HTkQLaGx/+ttFjF8i+ACbArnB4ce68a9m5hw==", + "dev": true, + "requires": { + "acorn": "^7.1.1", + "acorn-jsx": "^5.2.0", + "eslint-visitor-keys": "^1.1.0" + } + }, + "ignore": { + "version": "4.0.6", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-4.0.6.tgz", + "integrity": "sha512-cyFDKrqc/YdcWFniJhzI42+AzS+gNwmUzOSFcRCQYwySuBBBy/KjuxWLZ/FHEH6Moq1NizMOBWyTcv8O4OZIMg==", + "dev": true + }, + "levn": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/levn/-/levn-0.3.0.tgz", + "integrity": "sha1-OwmSTt+fCDwEkP3UwLxEIeBHZO4=", + "dev": true, + "requires": { + "prelude-ls": "~1.1.2", + "type-check": "~0.3.2" + } + }, + "mkdirp": { + "version": "0.5.5", + "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.5.tgz", + "integrity": "sha512-NKmAlESf6jMGym1++R0Ra7wvhV+wFW63FaSOFPwRahvea0gMUcGUhVeAg/0BC0wiv9ih5NYPB1Wn1UEI1/L+xQ==", + "dev": true, + "requires": { + "minimist": "^1.2.5" + } + }, + "optionator": { + "version": "0.8.3", + "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.8.3.tgz", + "integrity": "sha512-+IW9pACdk3XWmmTXG8m3upGUJst5XRGzxMRjXzAuJ1XnIFNvfhjjIuYkDvysnPQ7qzqVzLt78BCruntqRhWQbA==", + "dev": true, + "requires": { + "deep-is": "~0.1.3", + "fast-levenshtein": "~2.0.6", + "levn": "~0.3.0", + "prelude-ls": "~1.1.2", + "type-check": "~0.3.2", + "word-wrap": "~1.2.3" + } + }, + "path-key": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-2.0.1.tgz", + "integrity": "sha1-QRyttXTFoUDTpLGRDUDYDMn0C0A=", + "dev": true + }, + "prelude-ls": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.1.2.tgz", + "integrity": "sha1-IZMqVJ9eUv/ZqCf1cOBL5iqX2lQ=", + "dev": true + }, + "regexpp": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/regexpp/-/regexpp-2.0.1.tgz", + "integrity": "sha512-lv0M6+TkDVniA3aD1Eg0DVpfU/booSu7Eev3TDO/mZKHBfVjgCGTV4t4buppESEYDtkArYFOxTJWv6S5C+iaNw==", + "dev": true + }, + "shebang-command": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-1.2.0.tgz", + "integrity": "sha1-RKrGW2lbAzmJaMOfNj/uXer98eo=", + "dev": true, + "requires": { + "shebang-regex": "^1.0.0" + } + }, + "shebang-regex": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-1.0.0.tgz", + "integrity": "sha1-2kL0l0DAtC2yypcoVxyxkMmO/qM=", + "dev": true + }, + "strip-ansi": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-5.2.0.tgz", + "integrity": "sha512-DuRs1gKbBqsMKIZlrffwlug8MHkcnpjs5VPmL1PAh+mA30U0DTotfDZ0d2UUsXpPmPmMMJ6W773MaA3J+lbiWA==", + "dev": true, + "requires": { + "ansi-regex": "^4.1.0" + } + }, + "type-check": { + "version": "0.3.2", + "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.3.2.tgz", + "integrity": "sha1-WITKtRLPHTVeP7eE8wgEsrUg23I=", + "dev": true, + "requires": { + "prelude-ls": "~1.1.2" + } + }, + "which": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/which/-/which-1.3.1.tgz", + "integrity": "sha512-HxJdYWq1MTIQbJ3nw0cqssHoTNU267KlrDuGZ1WYlxDStUtKUhOaJmh112/TZmHxxUfuJqPXSOm7tDyas0OSIQ==", + "dev": true, + "requires": { + "isexe": "^2.0.0" + } + } + } + }, + "eslint-config-node": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/eslint-config-node/-/eslint-config-node-4.1.0.tgz", + "integrity": "sha512-Wz17xV5O2WFG8fGdMYEBdbiL6TL7YNJSJvSX9V4sXQownewfYmoqlly7wxqLkOUv/57pq6LnnotMiQQrrPjCqQ==", + "dev": true, + "requires": { + "eslint": "^6.8.0", + "eslint-config-esnext": "^4.1.0" + }, + "dependencies": { + "ansi-regex": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-4.1.1.tgz", + "integrity": "sha512-ILlv4k/3f6vfQ4OoP2AGvirOktlQ98ZEL1k9FaQjxa3L1abBgbuTDAdPOpvbGncC0BTVQrl+OM8xZGK6tWXt7g==", + "dev": true + }, + "argparse": { + "version": "1.0.10", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-1.0.10.tgz", + "integrity": "sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==", + "dev": true, + "requires": { + "sprintf-js": "~1.0.2" + } + }, + "chalk": { + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", + "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==", + "dev": true, + "requires": { + "ansi-styles": "^3.2.1", + "escape-string-regexp": "^1.0.5", + "supports-color": "^5.3.0" + } + }, + "cross-spawn": { + "version": "6.0.5", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-6.0.5.tgz", + "integrity": "sha512-eTVLrBSt7fjbDygz805pMnstIs2VTBNkRm0qxZd+M7A5XDdxVRWO5MxGBXZhjY4cqLYLdtrGqRf8mBPmzwSpWQ==", + "dev": true, + "requires": { + "nice-try": "^1.0.4", + "path-key": "^2.0.1", + "semver": "^5.5.0", + "shebang-command": "^1.2.0", + "which": "^1.2.9" + }, + "dependencies": { + "semver": { + "version": "5.7.2", + "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.2.tgz", + "integrity": "sha512-cBznnQ9KjJqU67B52RMC65CMarK2600WFnbkcaiwWq3xy/5haFJlshgnpjovMVJ+Hff49d8GEn0b87C5pDQ10g==", + "dev": true + } + } + }, + "eslint": { + "version": "6.8.0", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-6.8.0.tgz", + "integrity": "sha512-K+Iayyo2LtyYhDSYwz5D5QdWw0hCacNzyq1Y821Xna2xSJj7cijoLLYmLxTQgcgZ9mC61nryMy9S7GRbYpI5Ig==", + "dev": true, + "requires": { + "@babel/code-frame": "^7.0.0", + "ajv": "^6.10.0", + "chalk": "^2.1.0", + "cross-spawn": "^6.0.5", + "debug": "^4.0.1", + "doctrine": "^3.0.0", + "eslint-scope": "^5.0.0", + "eslint-utils": "^1.4.3", + "eslint-visitor-keys": "^1.1.0", + "espree": "^6.1.2", + "esquery": "^1.0.1", + "esutils": "^2.0.2", + "file-entry-cache": "^5.0.1", + "functional-red-black-tree": "^1.0.1", + "glob-parent": "^5.0.0", + "globals": "^12.1.0", + "ignore": "^4.0.6", + "import-fresh": "^3.0.0", + "imurmurhash": "^0.1.4", + "inquirer": "^7.0.0", + "is-glob": "^4.0.0", + "js-yaml": "^3.13.1", + "json-stable-stringify-without-jsonify": "^1.0.1", + "levn": "^0.3.0", + "lodash": "^4.17.14", + "minimatch": "^3.0.4", + "mkdirp": "^0.5.1", + "natural-compare": "^1.4.0", + "optionator": "^0.8.3", + "progress": "^2.0.0", + "regexpp": "^2.0.1", + "semver": "^6.1.2", + "strip-ansi": "^5.2.0", + "strip-json-comments": "^3.0.1", + "table": "^5.2.3", + "text-table": "^0.2.0", + "v8-compile-cache": "^2.0.3" + }, + "dependencies": { + "js-yaml": { + "version": "3.14.1", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.14.1.tgz", + "integrity": "sha512-okMH7OXXJ7YrN9Ok3/SXrnu4iX9yOk+25nqX4imS2npuvTYDmo/QEZoqwZkYaIDk3jVvBOTOIEgEhaLOynBS9g==", + "dev": true, + "requires": { + "argparse": "^1.0.7", + "esprima": "^4.0.0" + } + } + } + }, + "eslint-utils": { + "version": "1.4.3", + "resolved": "https://registry.npmjs.org/eslint-utils/-/eslint-utils-1.4.3.tgz", + "integrity": "sha512-fbBN5W2xdY45KulGXmLHZ3c3FHfVYmKg0IrAKGOkT/464PQsx2UeIzfz1RmEci+KLm1bBaAzZAh8+/E+XAeZ8Q==", + "dev": true, + "requires": { + "eslint-visitor-keys": "^1.1.0" + } + }, + "espree": { + "version": "6.2.1", + "resolved": "https://registry.npmjs.org/espree/-/espree-6.2.1.tgz", + "integrity": "sha512-ysCxRQY3WaXJz9tdbWOwuWr5Y/XrPTGX9Kiz3yoUXwW0VZ4w30HTkQLaGx/+ttFjF8i+ACbArnB4ce68a9m5hw==", + "dev": true, + "requires": { + "acorn": "^7.1.1", + "acorn-jsx": "^5.2.0", + "eslint-visitor-keys": "^1.1.0" + } + }, + "ignore": { + "version": "4.0.6", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-4.0.6.tgz", + "integrity": "sha512-cyFDKrqc/YdcWFniJhzI42+AzS+gNwmUzOSFcRCQYwySuBBBy/KjuxWLZ/FHEH6Moq1NizMOBWyTcv8O4OZIMg==", + "dev": true + }, + "levn": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/levn/-/levn-0.3.0.tgz", + "integrity": "sha1-OwmSTt+fCDwEkP3UwLxEIeBHZO4=", + "dev": true, + "requires": { + "prelude-ls": "~1.1.2", + "type-check": "~0.3.2" + } + }, + "mkdirp": { + "version": "0.5.5", + "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.5.tgz", + "integrity": "sha512-NKmAlESf6jMGym1++R0Ra7wvhV+wFW63FaSOFPwRahvea0gMUcGUhVeAg/0BC0wiv9ih5NYPB1Wn1UEI1/L+xQ==", + "dev": true, + "requires": { + "minimist": "^1.2.5" + } + }, + "optionator": { + "version": "0.8.3", + "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.8.3.tgz", + "integrity": "sha512-+IW9pACdk3XWmmTXG8m3upGUJst5XRGzxMRjXzAuJ1XnIFNvfhjjIuYkDvysnPQ7qzqVzLt78BCruntqRhWQbA==", + "dev": true, + "requires": { + "deep-is": "~0.1.3", + "fast-levenshtein": "~2.0.6", + "levn": "~0.3.0", + "prelude-ls": "~1.1.2", + "type-check": "~0.3.2", + "word-wrap": "~1.2.3" + } + }, + "path-key": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-2.0.1.tgz", + "integrity": "sha1-QRyttXTFoUDTpLGRDUDYDMn0C0A=", + "dev": true + }, + "prelude-ls": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.1.2.tgz", + "integrity": "sha1-IZMqVJ9eUv/ZqCf1cOBL5iqX2lQ=", + "dev": true + }, + "regexpp": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/regexpp/-/regexpp-2.0.1.tgz", + "integrity": "sha512-lv0M6+TkDVniA3aD1Eg0DVpfU/booSu7Eev3TDO/mZKHBfVjgCGTV4t4buppESEYDtkArYFOxTJWv6S5C+iaNw==", + "dev": true + }, + "shebang-command": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-1.2.0.tgz", + "integrity": "sha1-RKrGW2lbAzmJaMOfNj/uXer98eo=", + "dev": true, + "requires": { + "shebang-regex": "^1.0.0" + } + }, + "shebang-regex": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-1.0.0.tgz", + "integrity": "sha1-2kL0l0DAtC2yypcoVxyxkMmO/qM=", + "dev": true + }, + "strip-ansi": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-5.2.0.tgz", + "integrity": "sha512-DuRs1gKbBqsMKIZlrffwlug8MHkcnpjs5VPmL1PAh+mA30U0DTotfDZ0d2UUsXpPmPmMMJ6W773MaA3J+lbiWA==", + "dev": true, + "requires": { + "ansi-regex": "^4.1.0" + } + }, + "type-check": { + "version": "0.3.2", + "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.3.2.tgz", + "integrity": "sha1-WITKtRLPHTVeP7eE8wgEsrUg23I=", + "dev": true, + "requires": { + "prelude-ls": "~1.1.2" + } + }, + "which": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/which/-/which-1.3.1.tgz", + "integrity": "sha512-HxJdYWq1MTIQbJ3nw0cqssHoTNU267KlrDuGZ1WYlxDStUtKUhOaJmh112/TZmHxxUfuJqPXSOm7tDyas0OSIQ==", + "dev": true, + "requires": { + "isexe": "^2.0.0" + } + } + } + }, + "eslint-config-react-native": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/eslint-config-react-native/-/eslint-config-react-native-4.1.0.tgz", + "integrity": "sha512-kNND+cs+ztawH7wgajf/K6FfNshjlDsFDAkkFZF9HAXDgH1w1sNMIfTfwzufg0hOcSK7rbiL4qbG/gg/oR507Q==", + "dev": true, + "requires": { + "eslint": "^6.8.0", + "eslint-config-esnext": "^4.1.0", + "eslint-plugin-react": "^7.19.0", + "eslint-plugin-react-native": "^3.8.1" + }, + "dependencies": { + "ansi-regex": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-4.1.1.tgz", + "integrity": "sha512-ILlv4k/3f6vfQ4OoP2AGvirOktlQ98ZEL1k9FaQjxa3L1abBgbuTDAdPOpvbGncC0BTVQrl+OM8xZGK6tWXt7g==", + "dev": true + }, + "argparse": { + "version": "1.0.10", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-1.0.10.tgz", + "integrity": "sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==", + "dev": true, + "requires": { + "sprintf-js": "~1.0.2" + } + }, + "chalk": { + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", + "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==", + "dev": true, + "requires": { + "ansi-styles": "^3.2.1", + "escape-string-regexp": "^1.0.5", + "supports-color": "^5.3.0" + } + }, + "cross-spawn": { + "version": "6.0.5", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-6.0.5.tgz", + "integrity": "sha512-eTVLrBSt7fjbDygz805pMnstIs2VTBNkRm0qxZd+M7A5XDdxVRWO5MxGBXZhjY4cqLYLdtrGqRf8mBPmzwSpWQ==", + "dev": true, + "requires": { + "nice-try": "^1.0.4", + "path-key": "^2.0.1", + "semver": "^5.5.0", + "shebang-command": "^1.2.0", + "which": "^1.2.9" + }, + "dependencies": { + "semver": { + "version": "5.7.2", + "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.2.tgz", + "integrity": "sha512-cBznnQ9KjJqU67B52RMC65CMarK2600WFnbkcaiwWq3xy/5haFJlshgnpjovMVJ+Hff49d8GEn0b87C5pDQ10g==", + "dev": true + } + } + }, + "eslint": { + "version": "6.8.0", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-6.8.0.tgz", + "integrity": "sha512-K+Iayyo2LtyYhDSYwz5D5QdWw0hCacNzyq1Y821Xna2xSJj7cijoLLYmLxTQgcgZ9mC61nryMy9S7GRbYpI5Ig==", + "dev": true, + "requires": { + "@babel/code-frame": "^7.0.0", + "ajv": "^6.10.0", + "chalk": "^2.1.0", + "cross-spawn": "^6.0.5", + "debug": "^4.0.1", + "doctrine": "^3.0.0", + "eslint-scope": "^5.0.0", + "eslint-utils": "^1.4.3", + "eslint-visitor-keys": "^1.1.0", + "espree": "^6.1.2", + "esquery": "^1.0.1", + "esutils": "^2.0.2", + "file-entry-cache": "^5.0.1", + "functional-red-black-tree": "^1.0.1", + "glob-parent": "^5.0.0", + "globals": "^12.1.0", + "ignore": "^4.0.6", + "import-fresh": "^3.0.0", + "imurmurhash": "^0.1.4", + "inquirer": "^7.0.0", + "is-glob": "^4.0.0", + "js-yaml": "^3.13.1", + "json-stable-stringify-without-jsonify": "^1.0.1", + "levn": "^0.3.0", + "lodash": "^4.17.14", + "minimatch": "^3.0.4", + "mkdirp": "^0.5.1", + "natural-compare": "^1.4.0", + "optionator": "^0.8.3", + "progress": "^2.0.0", + "regexpp": "^2.0.1", + "semver": "^6.1.2", + "strip-ansi": "^5.2.0", + "strip-json-comments": "^3.0.1", + "table": "^5.2.3", + "text-table": "^0.2.0", + "v8-compile-cache": "^2.0.3" + }, + "dependencies": { + "js-yaml": { + "version": "3.14.1", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.14.1.tgz", + "integrity": "sha512-okMH7OXXJ7YrN9Ok3/SXrnu4iX9yOk+25nqX4imS2npuvTYDmo/QEZoqwZkYaIDk3jVvBOTOIEgEhaLOynBS9g==", + "dev": true, + "requires": { + "argparse": "^1.0.7", + "esprima": "^4.0.0" + } + } + } + }, + "eslint-plugin-react": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/eslint-plugin-react/-/eslint-plugin-react-7.27.1.tgz", + "integrity": "sha512-meyunDjMMYeWr/4EBLTV1op3iSG3mjT/pz5gti38UzfM4OPpNc2m0t2xvKCOMU5D6FSdd34BIMFOvQbW+i8GAA==", + "dev": true, + "requires": { + "array-includes": "^3.1.4", + "array.prototype.flatmap": "^1.2.5", + "doctrine": "^2.1.0", + "estraverse": "^5.3.0", + "jsx-ast-utils": "^2.4.1 || ^3.0.0", + "minimatch": "^3.0.4", + "object.entries": "^1.1.5", + "object.fromentries": "^2.0.5", + "object.hasown": "^1.1.0", + "object.values": "^1.1.5", + "prop-types": "^15.7.2", + "resolve": "^2.0.0-next.3", + "semver": "^6.3.0", + "string.prototype.matchall": "^4.0.6" + }, + "dependencies": { + "doctrine": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/doctrine/-/doctrine-2.1.0.tgz", + "integrity": "sha512-35mSku4ZXK0vfCuHEDAwt55dg2jNajHZ1odvF+8SSr82EsZY4QmXfuWso8oEd8zRhVObSN18aM0CjSdoBX7zIw==", + "dev": true, + "requires": { + "esutils": "^2.0.2" + } + } + } + }, + "eslint-plugin-react-native": { + "version": "3.11.0", + "resolved": "https://registry.npmjs.org/eslint-plugin-react-native/-/eslint-plugin-react-native-3.11.0.tgz", + "integrity": "sha512-7F3OTwrtQPfPFd+VygqKA2VZ0f2fz0M4gJmry/TRE18JBb94/OtMxwbL7Oqwu7FGyrdeIOWnXQbBAveMcSTZIA==", + "dev": true, + "requires": { + "@babel/traverse": "^7.7.4", + "eslint-plugin-react-native-globals": "^0.1.1" + } + }, + "eslint-utils": { + "version": "1.4.3", + "resolved": "https://registry.npmjs.org/eslint-utils/-/eslint-utils-1.4.3.tgz", + "integrity": "sha512-fbBN5W2xdY45KulGXmLHZ3c3FHfVYmKg0IrAKGOkT/464PQsx2UeIzfz1RmEci+KLm1bBaAzZAh8+/E+XAeZ8Q==", + "dev": true, + "requires": { + "eslint-visitor-keys": "^1.1.0" + } + }, + "espree": { + "version": "6.2.1", + "resolved": "https://registry.npmjs.org/espree/-/espree-6.2.1.tgz", + "integrity": "sha512-ysCxRQY3WaXJz9tdbWOwuWr5Y/XrPTGX9Kiz3yoUXwW0VZ4w30HTkQLaGx/+ttFjF8i+ACbArnB4ce68a9m5hw==", + "dev": true, + "requires": { + "acorn": "^7.1.1", + "acorn-jsx": "^5.2.0", + "eslint-visitor-keys": "^1.1.0" + } + }, + "estraverse": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz", + "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==", + "dev": true + }, + "ignore": { + "version": "4.0.6", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-4.0.6.tgz", + "integrity": "sha512-cyFDKrqc/YdcWFniJhzI42+AzS+gNwmUzOSFcRCQYwySuBBBy/KjuxWLZ/FHEH6Moq1NizMOBWyTcv8O4OZIMg==", + "dev": true + }, + "levn": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/levn/-/levn-0.3.0.tgz", + "integrity": "sha1-OwmSTt+fCDwEkP3UwLxEIeBHZO4=", + "dev": true, + "requires": { + "prelude-ls": "~1.1.2", + "type-check": "~0.3.2" + } + }, + "mkdirp": { + "version": "0.5.5", + "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.5.tgz", + "integrity": "sha512-NKmAlESf6jMGym1++R0Ra7wvhV+wFW63FaSOFPwRahvea0gMUcGUhVeAg/0BC0wiv9ih5NYPB1Wn1UEI1/L+xQ==", + "dev": true, + "requires": { + "minimist": "^1.2.5" + } + }, + "optionator": { + "version": "0.8.3", + "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.8.3.tgz", + "integrity": "sha512-+IW9pACdk3XWmmTXG8m3upGUJst5XRGzxMRjXzAuJ1XnIFNvfhjjIuYkDvysnPQ7qzqVzLt78BCruntqRhWQbA==", + "dev": true, + "requires": { + "deep-is": "~0.1.3", + "fast-levenshtein": "~2.0.6", + "levn": "~0.3.0", + "prelude-ls": "~1.1.2", + "type-check": "~0.3.2", + "word-wrap": "~1.2.3" + } + }, + "path-key": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-2.0.1.tgz", + "integrity": "sha1-QRyttXTFoUDTpLGRDUDYDMn0C0A=", + "dev": true + }, + "prelude-ls": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.1.2.tgz", + "integrity": "sha1-IZMqVJ9eUv/ZqCf1cOBL5iqX2lQ=", + "dev": true + }, + "regexpp": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/regexpp/-/regexpp-2.0.1.tgz", + "integrity": "sha512-lv0M6+TkDVniA3aD1Eg0DVpfU/booSu7Eev3TDO/mZKHBfVjgCGTV4t4buppESEYDtkArYFOxTJWv6S5C+iaNw==", + "dev": true + }, + "resolve": { + "version": "2.0.0-next.3", + "resolved": "https://registry.npmjs.org/resolve/-/resolve-2.0.0-next.3.tgz", + "integrity": "sha512-W8LucSynKUIDu9ylraa7ueVZ7hc0uAgJBxVsQSKOXOyle8a93qXhcz+XAXZ8bIq2d6i4Ehddn6Evt+0/UwKk6Q==", + "dev": true, + "requires": { + "is-core-module": "^2.2.0", + "path-parse": "^1.0.6" + } + }, + "shebang-command": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-1.2.0.tgz", + "integrity": "sha1-RKrGW2lbAzmJaMOfNj/uXer98eo=", + "dev": true, + "requires": { + "shebang-regex": "^1.0.0" + } + }, + "shebang-regex": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-1.0.0.tgz", + "integrity": "sha1-2kL0l0DAtC2yypcoVxyxkMmO/qM=", + "dev": true + }, + "strip-ansi": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-5.2.0.tgz", + "integrity": "sha512-DuRs1gKbBqsMKIZlrffwlug8MHkcnpjs5VPmL1PAh+mA30U0DTotfDZ0d2UUsXpPmPmMMJ6W773MaA3J+lbiWA==", + "dev": true, + "requires": { + "ansi-regex": "^4.1.0" + } + }, + "type-check": { + "version": "0.3.2", + "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.3.2.tgz", + "integrity": "sha1-WITKtRLPHTVeP7eE8wgEsrUg23I=", + "dev": true, + "requires": { + "prelude-ls": "~1.1.2" + } + }, + "which": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/which/-/which-1.3.1.tgz", + "integrity": "sha512-HxJdYWq1MTIQbJ3nw0cqssHoTNU267KlrDuGZ1WYlxDStUtKUhOaJmh112/TZmHxxUfuJqPXSOm7tDyas0OSIQ==", + "dev": true, + "requires": { + "isexe": "^2.0.0" + } + } + } + }, + "eslint-config-recommended": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/eslint-config-recommended/-/eslint-config-recommended-4.1.0.tgz", + "integrity": "sha512-2evA0SX1VqtyFiExmBI2WAO4XQCKlr7wmNELE8rcT5PyZY2ixsY881ofVZWKuI/dywpgLiES1gR/XUQcnVLRzQ==", + "dev": true, + "requires": { + "eslint": "^6.8.0", + "eslint-config-esnext": "^4.1.0", + "eslint-config-node": "^4.1.0", + "eslint-config-react-native": "^4.1.0" + }, + "dependencies": { + "ansi-regex": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-4.1.1.tgz", + "integrity": "sha512-ILlv4k/3f6vfQ4OoP2AGvirOktlQ98ZEL1k9FaQjxa3L1abBgbuTDAdPOpvbGncC0BTVQrl+OM8xZGK6tWXt7g==", + "dev": true + }, + "argparse": { + "version": "1.0.10", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-1.0.10.tgz", + "integrity": "sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==", + "dev": true, + "requires": { + "sprintf-js": "~1.0.2" + } + }, + "chalk": { + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", + "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==", + "dev": true, + "requires": { + "ansi-styles": "^3.2.1", + "escape-string-regexp": "^1.0.5", + "supports-color": "^5.3.0" + } + }, + "cross-spawn": { + "version": "6.0.5", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-6.0.5.tgz", + "integrity": "sha512-eTVLrBSt7fjbDygz805pMnstIs2VTBNkRm0qxZd+M7A5XDdxVRWO5MxGBXZhjY4cqLYLdtrGqRf8mBPmzwSpWQ==", + "dev": true, + "requires": { + "nice-try": "^1.0.4", + "path-key": "^2.0.1", + "semver": "^5.5.0", + "shebang-command": "^1.2.0", + "which": "^1.2.9" + }, + "dependencies": { + "semver": { + "version": "5.7.2", + "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.2.tgz", + "integrity": "sha512-cBznnQ9KjJqU67B52RMC65CMarK2600WFnbkcaiwWq3xy/5haFJlshgnpjovMVJ+Hff49d8GEn0b87C5pDQ10g==", + "dev": true + } + } + }, + "eslint": { + "version": "6.8.0", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-6.8.0.tgz", + "integrity": "sha512-K+Iayyo2LtyYhDSYwz5D5QdWw0hCacNzyq1Y821Xna2xSJj7cijoLLYmLxTQgcgZ9mC61nryMy9S7GRbYpI5Ig==", + "dev": true, + "requires": { + "@babel/code-frame": "^7.0.0", + "ajv": "^6.10.0", + "chalk": "^2.1.0", + "cross-spawn": "^6.0.5", + "debug": "^4.0.1", + "doctrine": "^3.0.0", + "eslint-scope": "^5.0.0", + "eslint-utils": "^1.4.3", + "eslint-visitor-keys": "^1.1.0", + "espree": "^6.1.2", + "esquery": "^1.0.1", + "esutils": "^2.0.2", + "file-entry-cache": "^5.0.1", + "functional-red-black-tree": "^1.0.1", + "glob-parent": "^5.0.0", + "globals": "^12.1.0", + "ignore": "^4.0.6", + "import-fresh": "^3.0.0", + "imurmurhash": "^0.1.4", + "inquirer": "^7.0.0", + "is-glob": "^4.0.0", + "js-yaml": "^3.13.1", + "json-stable-stringify-without-jsonify": "^1.0.1", + "levn": "^0.3.0", + "lodash": "^4.17.14", + "minimatch": "^3.0.4", + "mkdirp": "^0.5.1", + "natural-compare": "^1.4.0", + "optionator": "^0.8.3", + "progress": "^2.0.0", + "regexpp": "^2.0.1", + "semver": "^6.1.2", + "strip-ansi": "^5.2.0", + "strip-json-comments": "^3.0.1", + "table": "^5.2.3", + "text-table": "^0.2.0", + "v8-compile-cache": "^2.0.3" + }, + "dependencies": { + "js-yaml": { + "version": "3.14.1", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.14.1.tgz", + "integrity": "sha512-okMH7OXXJ7YrN9Ok3/SXrnu4iX9yOk+25nqX4imS2npuvTYDmo/QEZoqwZkYaIDk3jVvBOTOIEgEhaLOynBS9g==", + "dev": true, + "requires": { + "argparse": "^1.0.7", + "esprima": "^4.0.0" + } + } + } + }, + "eslint-utils": { + "version": "1.4.3", + "resolved": "https://registry.npmjs.org/eslint-utils/-/eslint-utils-1.4.3.tgz", + "integrity": "sha512-fbBN5W2xdY45KulGXmLHZ3c3FHfVYmKg0IrAKGOkT/464PQsx2UeIzfz1RmEci+KLm1bBaAzZAh8+/E+XAeZ8Q==", + "dev": true, + "requires": { + "eslint-visitor-keys": "^1.1.0" + } + }, + "espree": { + "version": "6.2.1", + "resolved": "https://registry.npmjs.org/espree/-/espree-6.2.1.tgz", + "integrity": "sha512-ysCxRQY3WaXJz9tdbWOwuWr5Y/XrPTGX9Kiz3yoUXwW0VZ4w30HTkQLaGx/+ttFjF8i+ACbArnB4ce68a9m5hw==", + "dev": true, + "requires": { + "acorn": "^7.1.1", + "acorn-jsx": "^5.2.0", + "eslint-visitor-keys": "^1.1.0" + } + }, + "ignore": { + "version": "4.0.6", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-4.0.6.tgz", + "integrity": "sha512-cyFDKrqc/YdcWFniJhzI42+AzS+gNwmUzOSFcRCQYwySuBBBy/KjuxWLZ/FHEH6Moq1NizMOBWyTcv8O4OZIMg==", + "dev": true + }, + "levn": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/levn/-/levn-0.3.0.tgz", + "integrity": "sha1-OwmSTt+fCDwEkP3UwLxEIeBHZO4=", + "dev": true, + "requires": { + "prelude-ls": "~1.1.2", + "type-check": "~0.3.2" + } + }, + "mkdirp": { + "version": "0.5.5", + "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.5.tgz", + "integrity": "sha512-NKmAlESf6jMGym1++R0Ra7wvhV+wFW63FaSOFPwRahvea0gMUcGUhVeAg/0BC0wiv9ih5NYPB1Wn1UEI1/L+xQ==", + "dev": true, + "requires": { + "minimist": "^1.2.5" + } + }, + "optionator": { + "version": "0.8.3", + "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.8.3.tgz", + "integrity": "sha512-+IW9pACdk3XWmmTXG8m3upGUJst5XRGzxMRjXzAuJ1XnIFNvfhjjIuYkDvysnPQ7qzqVzLt78BCruntqRhWQbA==", + "dev": true, + "requires": { + "deep-is": "~0.1.3", + "fast-levenshtein": "~2.0.6", + "levn": "~0.3.0", + "prelude-ls": "~1.1.2", + "type-check": "~0.3.2", + "word-wrap": "~1.2.3" + } + }, + "path-key": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-2.0.1.tgz", + "integrity": "sha1-QRyttXTFoUDTpLGRDUDYDMn0C0A=", + "dev": true + }, + "prelude-ls": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.1.2.tgz", + "integrity": "sha1-IZMqVJ9eUv/ZqCf1cOBL5iqX2lQ=", + "dev": true + }, + "regexpp": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/regexpp/-/regexpp-2.0.1.tgz", + "integrity": "sha512-lv0M6+TkDVniA3aD1Eg0DVpfU/booSu7Eev3TDO/mZKHBfVjgCGTV4t4buppESEYDtkArYFOxTJWv6S5C+iaNw==", + "dev": true + }, + "shebang-command": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-1.2.0.tgz", + "integrity": "sha1-RKrGW2lbAzmJaMOfNj/uXer98eo=", + "dev": true, + "requires": { + "shebang-regex": "^1.0.0" + } + }, + "shebang-regex": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-1.0.0.tgz", + "integrity": "sha1-2kL0l0DAtC2yypcoVxyxkMmO/qM=", + "dev": true + }, + "strip-ansi": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-5.2.0.tgz", + "integrity": "sha512-DuRs1gKbBqsMKIZlrffwlug8MHkcnpjs5VPmL1PAh+mA30U0DTotfDZ0d2UUsXpPmPmMMJ6W773MaA3J+lbiWA==", + "dev": true, + "requires": { + "ansi-regex": "^4.1.0" + } + }, + "type-check": { + "version": "0.3.2", + "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.3.2.tgz", + "integrity": "sha1-WITKtRLPHTVeP7eE8wgEsrUg23I=", + "dev": true, + "requires": { + "prelude-ls": "~1.1.2" + } + }, + "which": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/which/-/which-1.3.1.tgz", + "integrity": "sha512-HxJdYWq1MTIQbJ3nw0cqssHoTNU267KlrDuGZ1WYlxDStUtKUhOaJmh112/TZmHxxUfuJqPXSOm7tDyas0OSIQ==", + "dev": true, + "requires": { + "isexe": "^2.0.0" + } + } + } + }, + "eslint-import-resolver-node": { + "version": "0.3.6", + "resolved": "https://registry.npmjs.org/eslint-import-resolver-node/-/eslint-import-resolver-node-0.3.6.tgz", + "integrity": "sha512-0En0w03NRVMn9Uiyn8YRPDKvWjxCWkslUEhGNTdGx15RvPJYQ+lbOlqrlNI2vEAs4pDYK4f/HN2TbDmk5TP0iw==", + "dev": true, + "requires": { + "debug": "^3.2.7", + "resolve": "^1.20.0" + }, + "dependencies": { + "debug": { + "version": "3.2.7", + "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.7.tgz", + "integrity": "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==", + "dev": true, + "requires": { + "ms": "^2.1.1" + } + }, + "ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "dev": true + } + } + }, + "eslint-module-utils": { + "version": "2.7.1", + "resolved": "https://registry.npmjs.org/eslint-module-utils/-/eslint-module-utils-2.7.1.tgz", + "integrity": "sha512-fjoetBXQZq2tSTWZ9yWVl2KuFrTZZH3V+9iD1V1RfpDgxzJR+mPd/KZmMiA8gbPqdBzpNiEHOuT7IYEWxrH0zQ==", + "dev": true, + "requires": { + "debug": "^3.2.7", + "find-up": "^2.1.0", + "pkg-dir": "^2.0.0" + }, + "dependencies": { + "debug": { + "version": "3.2.7", + "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.7.tgz", + "integrity": "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==", + "dev": true, + "requires": { + "ms": "^2.1.1" + } + }, + "ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "dev": true + } + } + }, + "eslint-plugin-babel": { + "version": "5.3.1", + "resolved": "https://registry.npmjs.org/eslint-plugin-babel/-/eslint-plugin-babel-5.3.1.tgz", + "integrity": "sha512-VsQEr6NH3dj664+EyxJwO4FCYm/00JhYb3Sk3ft8o+fpKuIfQ9TaW6uVUfvwMXHcf/lsnRIoyFPsLMyiWCSL/g==", + "dev": true, + "requires": { + "eslint-rule-composer": "^0.3.0" + } + }, + "eslint-plugin-prettier": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/eslint-plugin-prettier/-/eslint-plugin-prettier-5.0.0.tgz", + "integrity": "sha512-AgaZCVuYDXHUGxj/ZGu1u8H8CYgDY3iG6w5kUFw4AzMVXzB7VvbKgYR4nATIN+OvUrghMbiDLeimVjVY5ilq3w==", + "dev": true, + "requires": { + "prettier-linter-helpers": "^1.0.0", + "synckit": "^0.8.5" + } + }, + "eslint-plugin-react-native-globals": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/eslint-plugin-react-native-globals/-/eslint-plugin-react-native-globals-0.1.2.tgz", + "integrity": "sha512-9aEPf1JEpiTjcFAmmyw8eiIXmcNZOqaZyHO77wgm0/dWfT/oxC1SrIq8ET38pMxHYrcB6Uew+TzUVsBeczF88g==", + "dev": true + }, + "eslint-rule-composer": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/eslint-rule-composer/-/eslint-rule-composer-0.3.0.tgz", + "integrity": "sha512-bt+Sh8CtDmn2OajxvNO+BX7Wn4CIWMpTRm3MaiKPCQcnnlm0CS2mhui6QaoeQugs+3Kj2ESKEEGJUdVafwhiCg==", + "dev": true + }, + "eslint-scope": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-5.1.0.tgz", + "integrity": "sha512-iiGRvtxWqgtx5m8EyQUJihBloE4EnYeGE/bz1wSPwJE6tZuJUtHlhqDM4Xj2ukE8Dyy1+HCZ4hE0fzIVMzb58w==", + "dev": true, + "requires": { + "esrecurse": "^4.1.0", + "estraverse": "^4.1.1" + } + }, + "eslint-visitor-keys": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-1.3.0.tgz", + "integrity": "sha512-6J72N8UNa462wa/KFODt/PJ3IU60SDpC3QXC1Hjc1BXXpfL2C9R5+AU7jhe0F6GREqVMh4Juu+NY7xn+6dipUQ==", + "dev": true + }, + "esm": { + "version": "3.2.25", + "resolved": "https://registry.npmjs.org/esm/-/esm-3.2.25.tgz", + "integrity": "sha512-U1suiZ2oDVWv4zPO56S0NcR5QriEahGtdN2OR6FiOG4WJvcjBVFB0qI4+eKoWFH483PKGuLuu6V8Z4T5g63UVA==" + }, + "espree": { + "version": "9.6.1", + "resolved": "https://registry.npmjs.org/espree/-/espree-9.6.1.tgz", + "integrity": "sha512-oruZaFkjorTpF32kDSI5/75ViwGeZginGGy2NoOSg3Q9bnwlnmDm4HLnkl0RE3n+njDXR037aY1+x58Z/zFdwQ==", + "dev": true, + "requires": { + "acorn": "^8.9.0", + "acorn-jsx": "^5.3.2", + "eslint-visitor-keys": "^3.4.1" + }, + "dependencies": { + "acorn": { + "version": "8.10.0", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.10.0.tgz", + "integrity": "sha512-F0SAmZ8iUtS//m8DmCTA0jlh6TDKkHQyK6xc6V4KDTyZKA9dnvX9/3sRTVQrWm79glUAZbnmmNcdYwUIHWVybw==", + "dev": true + }, + "eslint-visitor-keys": { + "version": "3.4.3", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.4.3.tgz", + "integrity": "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==", + "dev": true + } + } + }, + "esprima": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/esprima/-/esprima-4.0.1.tgz", + "integrity": "sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==", + "dev": true + }, + "esquery": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/esquery/-/esquery-1.5.0.tgz", + "integrity": "sha512-YQLXUplAwJgCydQ78IMJywZCceoqk1oH01OERdSAJc/7U2AylwjhSCLDEtqwg811idIS/9fIU5GjG73IgjKMVg==", + "dev": true, + "requires": { + "estraverse": "^5.1.0" + }, + "dependencies": { + "estraverse": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.1.0.tgz", + "integrity": "sha512-FyohXK+R0vE+y1nHLoBM7ZTyqRpqAlhdZHCWIWEviFLiGB8b04H6bQs8G+XTthacvT8VuwvteiP7RJSxMs8UEw==", + "dev": true + } + } + }, + "esrecurse": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/esrecurse/-/esrecurse-4.3.0.tgz", + "integrity": "sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==", + "dev": true, + "requires": { + "estraverse": "^5.2.0" + }, + "dependencies": { + "estraverse": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz", + "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==", + "dev": true + } + } + }, + "estraverse": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-4.3.0.tgz", + "integrity": "sha512-39nnKffWz8xN1BU/2c79n9nB9HDzo0niYUqx6xyqUnyoAnQyyWpOTdZEeiCch8BBu515t4wp9ZmgVfVhn9EBpw==", + "dev": true + }, + "esutils": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz", + "integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==", + "dev": true + }, + "etag": { + "version": "1.8.1", + "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz", + "integrity": "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==" + }, + "events": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/events/-/events-3.3.0.tgz", + "integrity": "sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q==" + }, + "execa": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/execa/-/execa-5.1.1.tgz", + "integrity": "sha512-8uSpZZocAZRBAPIEINJj3Lo9HyGitllczc27Eh5YYojjMFMn8yHMDMaUHE2Jqfq05D/wucwI4JGURyXt1vchyg==", + "dev": true, + "requires": { + "cross-spawn": "^7.0.3", + "get-stream": "^6.0.0", + "human-signals": "^2.1.0", + "is-stream": "^2.0.0", + "merge-stream": "^2.0.0", + "npm-run-path": "^4.0.1", + "onetime": "^5.1.2", + "signal-exit": "^3.0.3", + "strip-final-newline": "^2.0.0" + }, + "dependencies": { + "get-stream": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-6.0.1.tgz", + "integrity": "sha512-ts6Wi+2j3jQjqi70w5AlN8DFnkSwC+MqmxEzdEALB2qXZYV3X/b1CTfgPLGJNMeAWxdPfU8FO1ms3NUfaHCPYg==", + "dev": true + } + } + }, + "exit": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/exit/-/exit-0.1.2.tgz", + "integrity": "sha512-Zk/eNKV2zbjpKzrsQ+n1G6poVbErQxJ0LBOJXaKZ1EViLzH+hrLu9cdXI4zw9dBQJslwBEpbQ2P1oS7nDxs6jQ==", + "dev": true + }, + "expect": { + "version": "28.1.3", + "resolved": "https://registry.npmjs.org/expect/-/expect-28.1.3.tgz", + "integrity": "sha512-eEh0xn8HlsuOBxFgIss+2mX85VAS4Qy3OSkjV7rlBWljtA4oWH37glVGyOZSZvErDT/yBywZdPGwCXuTvSG85g==", + "dev": true, + "requires": { + "@jest/expect-utils": "^28.1.3", + "jest-get-type": "^28.0.2", + "jest-matcher-utils": "^28.1.3", + "jest-message-util": "^28.1.3", + "jest-util": "^28.1.3" + } + }, + "express": { + "version": "4.18.2", + "resolved": "https://registry.npmjs.org/express/-/express-4.18.2.tgz", + "integrity": "sha512-5/PsL6iGPdfQ/lKM1UuielYgv3BUoJfz1aUwU9vHZ+J7gyvwdQXFEBIEIaxeGf0GIcreATNyBExtalisDbuMqQ==", + "requires": { + "accepts": "~1.3.8", + "array-flatten": "1.1.1", + "body-parser": "1.20.1", + "content-disposition": "0.5.4", + "content-type": "~1.0.4", + "cookie": "0.5.0", + "cookie-signature": "1.0.6", + "debug": "2.6.9", + "depd": "2.0.0", + "encodeurl": "~1.0.2", + "escape-html": "~1.0.3", + "etag": "~1.8.1", + "finalhandler": "1.2.0", + "fresh": "0.5.2", + "http-errors": "2.0.0", + "merge-descriptors": "1.0.1", + "methods": "~1.1.2", + "on-finished": "2.4.1", + "parseurl": "~1.3.3", + "path-to-regexp": "0.1.7", + "proxy-addr": "~2.0.7", + "qs": "6.11.0", + "range-parser": "~1.2.1", + "safe-buffer": "5.2.1", + "send": "0.18.0", + "serve-static": "1.15.0", + "setprototypeof": "1.2.0", + "statuses": "2.0.1", + "type-is": "~1.6.18", + "utils-merge": "1.0.1", + "vary": "~1.1.2" + }, + "dependencies": { + "debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "requires": { + "ms": "2.0.0" + } + }, + "safe-buffer": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", + "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==" + } + } + }, + "express-basic-auth": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/express-basic-auth/-/express-basic-auth-1.2.1.tgz", + "integrity": "sha512-L6YQ1wQ/mNjVLAmK3AG1RK6VkokA1BIY6wmiH304Xtt/cLTps40EusZsU1Uop+v9lTDPxdtzbFmdXfFO3KEnwA==", + "requires": { + "basic-auth": "^2.0.1" + } + }, + "express-winston": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/express-winston/-/express-winston-4.2.0.tgz", + "integrity": "sha512-EMD74g63nVHi7pFleQw7KHCxiA1pjF5uCwbCfzGqmFxs9KvlDPIVS3cMGpULm6MshExMT9TjC3SqmRGB9kb7yw==", + "requires": { + "chalk": "^2.4.2", + "lodash": "^4.17.21" + }, + "dependencies": { + "chalk": { + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", + "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==", + "requires": { + "ansi-styles": "^3.2.1", + "escape-string-regexp": "^1.0.5", + "supports-color": "^5.3.0" + } + } + } + }, + "external-editor": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/external-editor/-/external-editor-3.1.0.tgz", + "integrity": "sha512-hMQ4CX1p1izmuLYyZqLMO/qGNw10wSv9QDCPfzXfyFrOaCSSoRfqE1Kf1s5an66J5JZC62NewG+mK49jOCtQew==", + "dev": true, + "requires": { + "chardet": "^0.7.0", + "iconv-lite": "^0.4.24", + "tmp": "^0.0.33" + } + }, + "fast-deep-equal": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", + "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==" + }, + "fast-diff": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/fast-diff/-/fast-diff-1.2.0.tgz", + "integrity": "sha512-xJuoT5+L99XlZ8twedaRf6Ax2TgQVxvgZOYoPKqZufmJib0tL2tegPBOZb1pVNgIhlqDlA0eO0c3wBvQcmzx4w==", + "dev": true + }, + "fast-glob": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.1.tgz", + "integrity": "sha512-kNFPyjhh5cKjrUltxs+wFx+ZkbRaxxmZ+X0ZU31SOsxCEtP9VPgtq2teZw1DebupL5GmDaNQ6yKMMVcM41iqDg==", + "dev": true, + "requires": { + "@nodelib/fs.stat": "^2.0.2", + "@nodelib/fs.walk": "^1.2.3", + "glob-parent": "^5.1.2", + "merge2": "^1.3.0", + "micromatch": "^4.0.4" + } + }, + "fast-json-stable-stringify": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", + "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==", + "dev": true + }, + "fast-levenshtein": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz", + "integrity": "sha1-PYpcZog6FqMMqGQ+hR8Zuqd5eRc=", + "dev": true + }, + "fast-safe-stringify": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/fast-safe-stringify/-/fast-safe-stringify-2.1.1.tgz", + "integrity": "sha512-W+KJc2dmILlPplD/H4K9l9LcAHAfPtP6BY84uVLXQ6Evcz9Lcg33Y2z1IVblT6xdY54PXYVHEv+0Wpq8Io6zkA==", + "dev": true + }, + "fast-xml-parser": { + "version": "4.2.5", + "resolved": "https://registry.npmjs.org/fast-xml-parser/-/fast-xml-parser-4.2.5.tgz", + "integrity": "sha512-B9/wizE4WngqQftFPmdaMYlXoJlJOYxGQOanC77fq9k8+Z0v5dDSVh+3glErdIROP//s/jgb7ZuxKfB8nVyo0g==", + "requires": { + "strnum": "^1.0.5" + } + }, + "fastq": { + "version": "1.13.0", + "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.13.0.tgz", + "integrity": "sha512-YpkpUnK8od0o1hmeSc7UUs/eB/vIPWJYjKck2QKIzAf71Vm1AAQ3EbuZB3g2JIy+pg+ERD0vqI79KyZiB2e2Nw==", + "dev": true, + "requires": { + "reusify": "^1.0.4" + } + }, + "fb-watchman": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/fb-watchman/-/fb-watchman-2.0.2.tgz", + "integrity": "sha512-p5161BqbuCaSnB8jIbzQHOlpgsPmK5rJVDfDKO91Axs5NC1uu3HRQm6wt9cd9/+GtQQIO53JdGXXoyDpTAsgYA==", + "dev": true, + "requires": { + "bser": "2.1.1" + } + }, + "fecha": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/fecha/-/fecha-4.2.1.tgz", + "integrity": "sha512-MMMQ0ludy/nBs1/o0zVOiKTpG7qMbonKUzjJgQFEuvq6INZ1OraKPRAWkBq5vlKLOUMpmNYG1JoN3oDPUQ9m3Q==" + }, + "figures": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/figures/-/figures-3.2.0.tgz", + "integrity": "sha512-yaduQFRKLXYOGgEn6AZau90j3ggSOyiqXU0F9JZfeXYhNa+Jk4X+s45A2zg5jns87GAFa34BBm2kXw4XpNcbdg==", + "dev": true, + "requires": { + "escape-string-regexp": "^1.0.5" + } + }, + "file-entry-cache": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-5.0.1.tgz", + "integrity": "sha512-bCg29ictuBaKUwwArK4ouCaqDgLZcysCFLmM/Yn/FDoqndh/9vNuQfXRDvTuXKLxfD/JtZQGKFT8MGcJBK644g==", + "dev": true, + "requires": { + "flat-cache": "^2.0.1" + } + }, + "fill-range": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.0.1.tgz", + "integrity": "sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ==", + "dev": true, + "requires": { + "to-regex-range": "^5.0.1" + } + }, + "finalhandler": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.2.0.tgz", + "integrity": "sha512-5uXcUVftlQMFnWC9qu/svkWv3GTd2PfUhK/3PLkYNAe7FbqJMt3515HaxE6eRL74GdsriiwujiawdaB1BpEISg==", + "requires": { + "debug": "2.6.9", + "encodeurl": "~1.0.2", + "escape-html": "~1.0.3", + "on-finished": "2.4.1", + "parseurl": "~1.3.3", + "statuses": "2.0.1", + "unpipe": "~1.0.0" + }, + "dependencies": { + "debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "requires": { + "ms": "2.0.0" + } + } + } + }, + "find-up": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-2.1.0.tgz", + "integrity": "sha1-RdG35QbHF93UgndaK3eSCjwMV6c=", + "dev": true, + "requires": { + "locate-path": "^2.0.0" + } + }, + "flat-cache": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-2.0.1.tgz", + "integrity": "sha512-LoQe6yDuUMDzQAEH8sgmh4Md6oZnc/7PjtwjNFSzveXqSHt6ka9fPBuso7IGf9Rz4uqnSnWiFH2B/zj24a5ReA==", + "dev": true, + "requires": { + "flatted": "^2.0.0", + "rimraf": "2.6.3", + "write": "1.0.3" + }, + "dependencies": { + "rimraf": { + "version": "2.6.3", + "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-2.6.3.tgz", + "integrity": "sha512-mwqeW5XsA2qAejG46gYdENaxXjx9onRNCfn7L0duuP4hCuTIi/QO7PDK07KJfp1d+izWPrzEJDcSqBa0OZQriA==", + "dev": true, + "requires": { + "glob": "^7.1.3" + } + } + } + }, + "flatted": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/flatted/-/flatted-2.0.2.tgz", + "integrity": "sha512-r5wGx7YeOwNWNlCA0wQ86zKyDLMQr+/RB8xy74M4hTphfmjlijTSSXGuH8rnvKZnfT9i+75zmd8jcKdMR4O6jA==", + "dev": true + }, + "fn.name": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/fn.name/-/fn.name-1.1.0.tgz", + "integrity": "sha512-GRnmB5gPyJpAhTQdSZTSp9uaPSvl09KoYcMQtsB9rQoOmzs9dH6ffeccH+Z+cv6P68Hu5bC6JjRh4Ah/mHSNRw==" + }, + "form-data": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.0.tgz", + "integrity": "sha512-ETEklSGi5t0QMZuiXoA/Q6vcnxcLQP5vdugSpuAyi6SVGi2clPPp+xgEhuMaHC+zGgn31Kd235W35f7Hykkaww==", + "dev": true, + "requires": { + "asynckit": "^0.4.0", + "combined-stream": "^1.0.8", + "mime-types": "^2.1.12" + } + }, + "formidable": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/formidable/-/formidable-2.1.1.tgz", + "integrity": "sha512-0EcS9wCFEzLvfiks7omJ+SiYJAiD+TzK4Pcw1UlUoGnhUxDcMKjt0P7x8wEb0u6OHu8Nb98WG3nxtlF5C7bvUQ==", + "dev": true, + "requires": { + "dezalgo": "^1.0.4", + "hexoid": "^1.0.0", + "once": "^1.4.0", + "qs": "^6.11.0" + } + }, + "forwarded": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz", + "integrity": "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==" + }, + "fresh": { + "version": "0.5.2", + "resolved": "https://registry.npmjs.org/fresh/-/fresh-0.5.2.tgz", + "integrity": "sha512-zJ2mQYM18rEFOudeV4GShTGIQ7RbzA7ozbU9I/XBpm7kqgMywgmylMwXHxZJmkVoYkna9d2pVXVXPdYTP9ej8Q==" + }, + "fs.realpath": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", + "integrity": "sha1-FQStJSMVjKpA20onh8sBQRmU6k8=", + "dev": true + }, + "fsevents": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz", + "integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==", + "dev": true, + "optional": true + }, + "function-bind": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.1.tgz", + "integrity": "sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A==" + }, + "functional-red-black-tree": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/functional-red-black-tree/-/functional-red-black-tree-1.0.1.tgz", + "integrity": "sha1-GwqzvVU7Kg1jmdKcDj6gslIHgyc=", + "dev": true + }, + "gensync": { + "version": "1.0.0-beta.2", + "resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz", + "integrity": "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==", + "dev": true + }, + "get-caller-file": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz", + "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==", + "dev": true + }, + "get-intrinsic": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.1.1.tgz", + "integrity": "sha512-kWZrnVM42QCiEA2Ig1bG8zjoIMOgxWwYCEeNdwY6Tv/cOSeGpcoX4pXHfKUxNKVoArnrEr2e9srnAxxGIraS9Q==", + "requires": { + "function-bind": "^1.1.1", + "has": "^1.0.3", + "has-symbols": "^1.0.1" + } + }, + "get-package-type": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/get-package-type/-/get-package-type-0.1.0.tgz", + "integrity": "sha512-pjzuKtY64GYfWizNAJ0fr9VqttZkNiK2iS430LtIHzjBEr6bX8Am2zm4sW4Ro5wjWW5cAlRL1qAMTcXbjNAO2Q==" + }, + "get-symbol-description": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/get-symbol-description/-/get-symbol-description-1.0.0.tgz", + "integrity": "sha512-2EmdH1YvIQiZpltCNgkuiUnyukzxM/R6NDJX31Ke3BG1Nq5b0S2PhX59UKi9vZpPDQVdqn+1IcaAwnzTT5vCjw==", + "dev": true, + "requires": { + "call-bind": "^1.0.2", + "get-intrinsic": "^1.1.1" + } + }, + "getopts": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/getopts/-/getopts-2.3.0.tgz", + "integrity": "sha512-5eDf9fuSXwxBL6q5HX+dhDj+dslFGWzU5thZ9kNKUkcPtaPdatmUFKwHFrLb/uf/WpA4BHET+AX3Scl56cAjpA==" + }, + "glob": { + "version": "7.1.6", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.1.6.tgz", + "integrity": "sha512-LwaxwyZ72Lk7vZINtNNrywX0ZuLyStrdDtabefZKAY5ZGJhVtgdznluResxNmPitE0SAO+O26sWTHeKSI2wMBA==", + "dev": true, + "requires": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.0.4", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + } + }, + "glob-parent": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", + "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", + "dev": true, + "requires": { + "is-glob": "^4.0.1" + } + }, + "globals": { + "version": "12.4.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-12.4.0.tgz", + "integrity": "sha512-BWICuzzDvDoH54NHKCseDanAhE3CeDorgDL5MT6LMXXj2WCnd9UC2szdk4AWLfjdgNBCXLUanXYcpBBKOSWGwg==", + "dev": true, + "requires": { + "type-fest": "^0.8.1" + } + }, + "graceful-fs": { + "version": "4.2.9", + "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.9.tgz", + "integrity": "sha512-NtNxqUcXgpW2iMrfqSfR73Glt39K+BLwWsPs94yR63v45T0Wbej7eRmL5cWfwEgqXnmjQp3zaJTshdRW/qC2ZQ==", + "dev": true + }, + "graphemer": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/graphemer/-/graphemer-1.4.0.tgz", + "integrity": "sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag==", + "dev": true + }, + "has": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/has/-/has-1.0.3.tgz", + "integrity": "sha512-f2dvO0VU6Oej7RkWJGrehjbzMAjFp5/VKPp5tTpWIV4JHHZK1/BxbFRtf/siA2SWTe09caDmVtYYzWEIbBS4zw==", + "requires": { + "function-bind": "^1.1.1" + } + }, + "has-bigints": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/has-bigints/-/has-bigints-1.0.1.tgz", + "integrity": "sha512-LSBS2LjbNBTf6287JEbEzvJgftkF5qFkmCo9hDRpAzKhUOlJ+hx8dd4USs00SgsUNwc4617J9ki5YtEClM2ffA==", + "dev": true + }, + "has-flag": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", + "integrity": "sha1-tdRU3CGZriJWmfNGfloH87lVuv0=" + }, + "has-symbols": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.0.2.tgz", + "integrity": "sha512-chXa79rL/UC2KlX17jo3vRGz0azaWEx5tGqZg5pO3NUyEJVB17dMruQlzCCOfUvElghKcm5194+BCRvi2Rv/Gw==" + }, + "has-tostringtag": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.0.tgz", + "integrity": "sha512-kFjcSNhnlGV1kyoGk7OXKSawH5JOb/LzUc5w9B02hOTO0dfFRjbHQKvg1d6cf3HbeUmtU9VbbV3qzZ2Teh97WQ==", + "dev": true, + "requires": { + "has-symbols": "^1.0.2" + } + }, + "hexoid": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/hexoid/-/hexoid-1.0.0.tgz", + "integrity": "sha512-QFLV0taWQOZtvIRIAdBChesmogZrtuXvVWsFHZTk2SU+anspqZ2vMnoLg7IE1+Uk16N19APic1BuF8bC8c2m5g==", + "dev": true + }, + "html-escaper": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/html-escaper/-/html-escaper-2.0.2.tgz", + "integrity": "sha512-H2iMtd0I4Mt5eYiapRdIDjp+XzelXQ0tFE4JS7YFwFevXXMmOp9myNrUvCg0D6ws8iqkRPBfKHgbwig1SmlLfg==", + "dev": true + }, + "http-errors": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.0.tgz", + "integrity": "sha512-FtwrG/euBzaEjYeRqOgly7G0qviiXoJWnvEH2Z1plBdXgbyjv34pHTSb9zoeHMyDy33+DWy5Wt9Wo+TURtOYSQ==", + "requires": { + "depd": "2.0.0", + "inherits": "2.0.4", + "setprototypeof": "1.2.0", + "statuses": "2.0.1", + "toidentifier": "1.0.1" + } + }, + "human-signals": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/human-signals/-/human-signals-2.1.0.tgz", + "integrity": "sha512-B4FFZ6q/T2jhhksgkbEW3HBvWIfDW85snkQgawt07S7J5QXTk6BkNV+0yAeZrM5QpMAdYlocGoljn0sJ/WQkFw==", + "dev": true + }, + "iconv-lite": { + "version": "0.4.24", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz", + "integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==", + "requires": { + "safer-buffer": ">= 2.1.2 < 3" + } + }, + "ieee754": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz", + "integrity": "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==" + }, + "ignore": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.2.0.tgz", + "integrity": "sha512-CmxgYGiEPCLhfLnpPp1MoRmifwEIOgjcHXxOBjv7mY96c+eWScsOP9c112ZyLdWHi0FxHjI+4uVhKYp/gcdRmQ==", + "dev": true + }, + "ignore-by-default": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/ignore-by-default/-/ignore-by-default-1.0.1.tgz", + "integrity": "sha512-Ius2VYcGNk7T90CppJqcIkS5ooHUZyIQK+ClZfMfMNFEF9VSE73Fq+906u/CWu92x4gzZMWOwfFYckPObzdEbA==", + "dev": true + }, + "import-fresh": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.2.1.tgz", + "integrity": "sha512-6e1q1cnWP2RXD9/keSkxHScg508CdXqXWgWBaETNhyuBFz+kUZlKboh+ISK+bU++DmbHimVBrOz/zzPe0sZ3sQ==", + "dev": true, + "requires": { + "parent-module": "^1.0.0", + "resolve-from": "^4.0.0" + } + }, + "import-local": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/import-local/-/import-local-3.1.0.tgz", + "integrity": "sha512-ASB07uLtnDs1o6EHjKpX34BKYDSqnFerfTOJL2HvMqF70LnxpjkzDB8J44oT9pu4AMPkQwf8jl6szgvNd2tRIg==", + "dev": true, + "requires": { + "pkg-dir": "^4.2.0", + "resolve-cwd": "^3.0.0" + }, + "dependencies": { + "find-up": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-4.1.0.tgz", + "integrity": "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==", + "dev": true, + "requires": { + "locate-path": "^5.0.0", + "path-exists": "^4.0.0" + } + }, + "locate-path": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-5.0.0.tgz", + "integrity": "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==", + "dev": true, + "requires": { + "p-locate": "^4.1.0" + } + }, + "p-limit": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz", + "integrity": "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==", + "dev": true, + "requires": { + "p-try": "^2.0.0" + } + }, + "p-locate": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-4.1.0.tgz", + "integrity": "sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==", + "dev": true, + "requires": { + "p-limit": "^2.2.0" + } + }, + "p-try": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/p-try/-/p-try-2.2.0.tgz", + "integrity": "sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==", + "dev": true + }, + "path-exists": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", + "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", + "dev": true + }, + "pkg-dir": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/pkg-dir/-/pkg-dir-4.2.0.tgz", + "integrity": "sha512-HRDzbaKjC+AOWVXxAU/x54COGeIv9eb+6CkDSQoNTt4XyWoIJvuPsXizxu/Fr23EiekbtZwmh1IcIG/l/a10GQ==", + "dev": true, + "requires": { + "find-up": "^4.0.0" + } + } + } + }, + "imurmurhash": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz", + "integrity": "sha1-khi5srkoojixPcT7a21XbyMUU+o=", + "dev": true + }, + "inflight": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", + "integrity": "sha1-Sb1jMdfQLQwJvJEKEHW6gWW1bfk=", + "dev": true, + "requires": { + "once": "^1.3.0", + "wrappy": "1" + } + }, + "inherits": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==" + }, + "inquirer": { + "version": "7.3.2", + "resolved": "https://registry.npmjs.org/inquirer/-/inquirer-7.3.2.tgz", + "integrity": "sha512-DF4osh1FM6l0RJc5YWYhSDB6TawiBRlbV9Cox8MWlidU218Tb7fm3lQTULyUJDfJ0tjbzl0W4q651mrCCEM55w==", + "dev": true, + "requires": { + "ansi-escapes": "^4.2.1", + "chalk": "^4.1.0", + "cli-cursor": "^3.1.0", + "cli-width": "^3.0.0", + "external-editor": "^3.0.3", + "figures": "^3.0.0", + "lodash": "^4.17.16", + "mute-stream": "0.0.8", + "run-async": "^2.4.0", + "rxjs": "^6.6.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0", + "through": "^2.3.6" + } + }, + "internal-slot": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/internal-slot/-/internal-slot-1.0.3.tgz", + "integrity": "sha512-O0DB1JC/sPyZl7cIo78n5dR7eUSwwpYPiXRhTzNxZVAMUuB8vlnRFyLxdrVToks6XPLVnFfbzaVd5WLjhgg+vA==", + "dev": true, + "requires": { + "get-intrinsic": "^1.1.0", + "has": "^1.0.3", + "side-channel": "^1.0.4" + } + }, + "interpret": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/interpret/-/interpret-2.2.0.tgz", + "integrity": "sha512-Ju0Bz/cEia55xDwUWEa8+olFpCiQoypjnQySseKtmjNrnps3P+xfpUmGr90T7yjlVJmOtybRvPXhKMbHr+fWnw==" + }, + "ipaddr.js": { + "version": "1.9.1", + "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz", + "integrity": "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==" + }, + "is-arrayish": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.2.1.tgz", + "integrity": "sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg==", + "dev": true + }, + "is-bigint": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/is-bigint/-/is-bigint-1.0.4.tgz", + "integrity": "sha512-zB9CruMamjym81i2JZ3UMn54PKGsQzsJeo6xvN3HJJ4CAsQNB6iRutp2To77OfCNuoxspsIhzaPoO1zyCEhFOg==", + "dev": true, + "requires": { + "has-bigints": "^1.0.1" + } + }, + "is-binary-path": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz", + "integrity": "sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==", + "dev": true, + "requires": { + "binary-extensions": "^2.0.0" + } + }, + "is-boolean-object": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/is-boolean-object/-/is-boolean-object-1.1.2.tgz", + "integrity": "sha512-gDYaKHJmnj4aWxyj6YHyXVpdQawtVLHU5cb+eztPGczf6cjuTdwve5ZIEfgXqH4e57An1D1AKf8CZ3kYrQRqYA==", + "dev": true, + "requires": { + "call-bind": "^1.0.2", + "has-tostringtag": "^1.0.0" + } + }, + "is-callable": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/is-callable/-/is-callable-1.2.4.tgz", + "integrity": "sha512-nsuwtxZfMX67Oryl9LCQ+upnC0Z0BgpwntpS89m1H/TLF0zNfzfLMV/9Wa/6MZsj0acpEjAO0KF1xT6ZdLl95w==", + "dev": true + }, + "is-core-module": { + "version": "2.8.0", + "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.8.0.tgz", + "integrity": "sha512-vd15qHsaqrRL7dtH6QNuy0ndJmRDrS9HAM1CAiSifNUFv4x1a0CCVsj18hJ1mShxIG6T2i1sO78MkP56r0nYRw==", + "requires": { + "has": "^1.0.3" + } + }, + "is-date-object": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/is-date-object/-/is-date-object-1.0.5.tgz", + "integrity": "sha512-9YQaSxsAiSwcvS33MBk3wTCVnWK+HhF8VZR2jRxehM16QcVOdHqPn4VPHmRK4lSr38n9JriurInLcP90xsYNfQ==", + "dev": true, + "requires": { + "has-tostringtag": "^1.0.0" + } + }, + "is-docker": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-docker/-/is-docker-3.0.0.tgz", + "integrity": "sha512-eljcgEDlEns/7AXFosB5K/2nCM4P7FQPkGc/DWLy5rmFEWvZayGrik1d9/QIY5nJ4f9YsVvBkA6kJpHn9rISdQ==", + "dev": true + }, + "is-extglob": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", + "integrity": "sha1-qIwCU1eR8C7TfHahueqXc8gz+MI=", + "dev": true + }, + "is-fullwidth-code-point": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", + "dev": true + }, + "is-generator-fn": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/is-generator-fn/-/is-generator-fn-2.1.0.tgz", + "integrity": "sha512-cTIB4yPYL/Grw0EaSzASzg6bBy9gqCofvWN8okThAYIxKJZC+udlRAmGbM0XLeniEJSs8uEgHPGuHSe1XsOLSQ==", + "dev": true + }, + "is-glob": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", + "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", + "dev": true, + "requires": { + "is-extglob": "^2.1.1" + } + }, + "is-inside-container": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-inside-container/-/is-inside-container-1.0.0.tgz", + "integrity": "sha512-KIYLCCJghfHZxqjYBE7rEy0OBuTd5xCHS7tHVgvCLkx7StIoaxwNW3hCALgEUjFfeRk+MG/Qxmp/vtETEF3tRA==", + "dev": true, + "requires": { + "is-docker": "^3.0.0" + } + }, + "is-negative-zero": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/is-negative-zero/-/is-negative-zero-2.0.1.tgz", + "integrity": "sha512-2z6JzQvZRa9A2Y7xC6dQQm4FSTSTNWjKIYYTt4246eMTJmIo0Q+ZyOsU66X8lxK1AbB92dFeglPLrhwpeRKO6w==", + "dev": true + }, + "is-number": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", + "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", + "dev": true + }, + "is-number-object": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/is-number-object/-/is-number-object-1.0.6.tgz", + "integrity": "sha512-bEVOqiRcvo3zO1+G2lVMy+gkkEm9Yh7cDMRusKKu5ZJKPUYSJwICTKZrNKHA2EbSP0Tu0+6B/emsYNHZyn6K8g==", + "dev": true, + "requires": { + "has-tostringtag": "^1.0.0" + } + }, + "is-path-inside": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/is-path-inside/-/is-path-inside-3.0.3.tgz", + "integrity": "sha512-Fd4gABb+ycGAmKou8eMftCupSir5lRxqf4aD/vd0cD2qc4HL07OjCeuHMr8Ro4CoMaeCKDB0/ECBOVWjTwUvPQ==", + "dev": true + }, + "is-regex": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/is-regex/-/is-regex-1.1.4.tgz", + "integrity": "sha512-kvRdxDsxZjhzUX07ZnLydzS1TU/TJlTUHHY4YLL87e37oUA49DfkLqgy+VjFocowy29cKvcSiu+kIv728jTTVg==", + "dev": true, + "requires": { + "call-bind": "^1.0.2", + "has-tostringtag": "^1.0.0" + } + }, + "is-shared-array-buffer": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/is-shared-array-buffer/-/is-shared-array-buffer-1.0.1.tgz", + "integrity": "sha512-IU0NmyknYZN0rChcKhRO1X8LYz5Isj/Fsqh8NJOSf+N/hCOTwy29F32Ik7a+QszE63IdvmwdTPDd6cZ5pg4cwA==", + "dev": true + }, + "is-stream": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-2.0.0.tgz", + "integrity": "sha512-XCoy+WlUr7d1+Z8GgSuXmpuUFC9fOhRXglJMx+dwLKTkL44Cjd4W1Z5P+BQZpr+cR93aGP4S/s7Ftw6Nd/kiEw==" + }, + "is-string": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/is-string/-/is-string-1.0.7.tgz", + "integrity": "sha512-tE2UXzivje6ofPW7l23cjDOMa09gb7xlAqG6jG5ej6uPV32TlWP3NKPigtaGeHNu9fohccRYvIiZMfOOnOYUtg==", + "dev": true, + "requires": { + "has-tostringtag": "^1.0.0" + } + }, + "is-symbol": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/is-symbol/-/is-symbol-1.0.4.tgz", + "integrity": "sha512-C/CPBqKWnvdcxqIARxyOh4v1UUEOCHpgDa0WYgpKDFMszcrPcffg5uhwSgPCLD2WWxmq6isisz87tzT01tuGhg==", + "dev": true, + "requires": { + "has-symbols": "^1.0.2" + } + }, + "is-weakref": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/is-weakref/-/is-weakref-1.0.1.tgz", + "integrity": "sha512-b2jKc2pQZjaeFYWEf7ScFj+Be1I+PXmlu572Q8coTXZ+LD/QQZ7ShPMst8h16riVgyXTQwUsFEl74mDvc/3MHQ==", + "dev": true, + "requires": { + "call-bind": "^1.0.0" + } + }, + "is-wsl": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/is-wsl/-/is-wsl-2.2.0.tgz", + "integrity": "sha512-fKzAra0rGJUUBwGBgNkHZuToZcn+TtXHpeCgmkMJMMYx1sQDYaCSyjJBSCa2nH1DGm7s3n1oBnohoVTBaN7Lww==", + "dev": true, + "requires": { + "is-docker": "^2.0.0" + }, + "dependencies": { + "is-docker": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/is-docker/-/is-docker-2.2.1.tgz", + "integrity": "sha512-F+i2BKsFrH66iaUFc0woD8sLy8getkwTwtOBjvs56Cx4CgJDeKQeqfz8wAYiSb8JOprWhHH5p77PbmYCvvUuXQ==", + "dev": true + } + } + }, + "isarray": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-0.0.1.tgz", + "integrity": "sha512-D2S+3GLxWH+uhrNEcoh/fnmYeP8E8/zHl644d/jdA0g2uyXvy3sb0qxotE+ne0LtccHknQzWwZEzhak7oJ0COQ==", + "dev": true + }, + "isexe": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", + "integrity": "sha1-6PvzdNxVb/iUehDcsFctYz8s+hA=", + "dev": true + }, + "istanbul-lib-coverage": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/istanbul-lib-coverage/-/istanbul-lib-coverage-3.2.0.tgz", + "integrity": "sha512-eOeJ5BHCmHYvQK7xt9GkdHuzuCGS1Y6g9Gvnx3Ym33fz/HpLRYxiS0wHNr+m/MBC8B647Xt608vCDEvhl9c6Mw==", + "dev": true + }, + "istanbul-lib-instrument": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/istanbul-lib-instrument/-/istanbul-lib-instrument-6.0.1.tgz", + "integrity": "sha512-EAMEJBsYuyyztxMxW3g7ugGPkrZsV57v0Hmv3mm1uQsmB+QnZuepg731CRaIgeUVSdmsTngOkSnauNF8p7FIhA==", + "dev": true, + "requires": { + "@babel/core": "^7.12.3", + "@babel/parser": "^7.14.7", + "@istanbuljs/schema": "^0.1.2", + "istanbul-lib-coverage": "^3.2.0", + "semver": "^7.5.4" + }, + "dependencies": { + "semver": { + "version": "7.5.4", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.5.4.tgz", + "integrity": "sha512-1bCSESV6Pv+i21Hvpxp3Dx+pSD8lIPt8uVjRrxAUt/nbswYc+tK6Y2btiULjd4+fnq15PX+nqQDC7Oft7WkwcA==", + "dev": true, + "requires": { + "lru-cache": "^6.0.0" + } + } + } + }, + "istanbul-lib-report": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/istanbul-lib-report/-/istanbul-lib-report-3.0.1.tgz", + "integrity": "sha512-GCfE1mtsHGOELCU8e/Z7YWzpmybrx/+dSTfLrvY8qRmaY6zXTKWn6WQIjaAFw069icm6GVMNkgu0NzI4iPZUNw==", + "dev": true, + "requires": { + "istanbul-lib-coverage": "^3.0.0", + "make-dir": "^4.0.0", + "supports-color": "^7.1.0" + }, + "dependencies": { + "has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true + }, + "supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "requires": { + "has-flag": "^4.0.0" + } + } + } + }, + "istanbul-lib-source-maps": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/istanbul-lib-source-maps/-/istanbul-lib-source-maps-4.0.1.tgz", + "integrity": "sha512-n3s8EwkdFIJCG3BPKBYvskgXGoy88ARzvegkitk60NxRdwltLOTaH7CUiMRXvwYorl0Q712iEjcWB+fK/MrWVw==", + "dev": true, + "requires": { + "debug": "^4.1.1", + "istanbul-lib-coverage": "^3.0.0", + "source-map": "^0.6.1" + } + }, + "istanbul-reports": { + "version": "3.1.6", + "resolved": "https://registry.npmjs.org/istanbul-reports/-/istanbul-reports-3.1.6.tgz", + "integrity": "sha512-TLgnMkKg3iTDsQ9PbPTdpfAK2DzjF9mqUG7RMgcQl8oFjad8ob4laGxv5XV5U9MAfx8D6tSJiUyuAwzLicaxlg==", + "dev": true, + "requires": { + "html-escaper": "^2.0.0", + "istanbul-lib-report": "^3.0.0" + } + }, + "jest": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest/-/jest-29.7.0.tgz", + "integrity": "sha512-NIy3oAFp9shda19hy4HK0HRTWKtPJmGdnvywu01nOqNC2vZg+Z+fvJDxpMQA88eb2I9EcafcdjYgsDthnYTvGw==", + "dev": true, + "requires": { + "@jest/core": "^29.7.0", + "@jest/types": "^29.6.3", + "import-local": "^3.0.2", + "jest-cli": "^29.7.0" + }, + "dependencies": { + "@jest/schemas": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/@jest/schemas/-/schemas-29.6.3.tgz", + "integrity": "sha512-mo5j5X+jIZmJQveBKeS/clAueipV7KgiX1vMgCxam1RNYiqE1w62n0/tJJnHtjW8ZHcQco5gY85jA3mi0L+nSA==", + "dev": true, + "requires": { + "@sinclair/typebox": "^0.27.8" + } + }, + "@jest/types": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/@jest/types/-/types-29.6.3.tgz", + "integrity": "sha512-u3UPsIilWKOM3F9CXtrG8LEJmNxwoCQC/XVj4IKYXvvpx7QIi/Kg1LI5uDmDpKlac62NUtX7eLjRh+jVZcLOzw==", + "dev": true, + "requires": { + "@jest/schemas": "^29.6.3", + "@types/istanbul-lib-coverage": "^2.0.0", + "@types/istanbul-reports": "^3.0.0", + "@types/node": "*", + "@types/yargs": "^17.0.8", + "chalk": "^4.0.0" + } + }, + "@sinclair/typebox": { + "version": "0.27.8", + "resolved": "https://registry.npmjs.org/@sinclair/typebox/-/typebox-0.27.8.tgz", + "integrity": "sha512-+Fj43pSMwJs4KRrH/938Uf+uAELIgVBmQzg/q1YG10djyfA3TnrU8N8XzqCh/okZdszqBQTZf96idMfE5lnwTA==", + "dev": true + } + } + }, + "jest-changed-files": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-changed-files/-/jest-changed-files-29.7.0.tgz", + "integrity": "sha512-fEArFiwf1BpQ+4bXSprcDc3/x4HSzL4al2tozwVpDFpsxALjLYdyiIK4e5Vz66GQJIbXJ82+35PtysofptNX2w==", + "dev": true, + "requires": { + "execa": "^5.0.0", + "jest-util": "^29.7.0", + "p-limit": "^3.1.0" + }, + "dependencies": { + "@jest/schemas": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/@jest/schemas/-/schemas-29.6.3.tgz", + "integrity": "sha512-mo5j5X+jIZmJQveBKeS/clAueipV7KgiX1vMgCxam1RNYiqE1w62n0/tJJnHtjW8ZHcQco5gY85jA3mi0L+nSA==", + "dev": true, + "requires": { + "@sinclair/typebox": "^0.27.8" + } + }, + "@jest/types": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/@jest/types/-/types-29.6.3.tgz", + "integrity": "sha512-u3UPsIilWKOM3F9CXtrG8LEJmNxwoCQC/XVj4IKYXvvpx7QIi/Kg1LI5uDmDpKlac62NUtX7eLjRh+jVZcLOzw==", + "dev": true, + "requires": { + "@jest/schemas": "^29.6.3", + "@types/istanbul-lib-coverage": "^2.0.0", + "@types/istanbul-reports": "^3.0.0", + "@types/node": "*", + "@types/yargs": "^17.0.8", + "chalk": "^4.0.0" + } + }, + "@sinclair/typebox": { + "version": "0.27.8", + "resolved": "https://registry.npmjs.org/@sinclair/typebox/-/typebox-0.27.8.tgz", + "integrity": "sha512-+Fj43pSMwJs4KRrH/938Uf+uAELIgVBmQzg/q1YG10djyfA3TnrU8N8XzqCh/okZdszqBQTZf96idMfE5lnwTA==", + "dev": true + }, + "jest-util": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-util/-/jest-util-29.7.0.tgz", + "integrity": "sha512-z6EbKajIpqGKU56y5KBUgy1dt1ihhQJgWzUlZHArA/+X2ad7Cb5iF+AK1EWVL/Bo7Rz9uurpqw6SiBCefUbCGA==", + "dev": true, + "requires": { + "@jest/types": "^29.6.3", + "@types/node": "*", + "chalk": "^4.0.0", + "ci-info": "^3.2.0", + "graceful-fs": "^4.2.9", + "picomatch": "^2.2.3" + } + }, + "p-limit": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", + "integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==", + "dev": true, + "requires": { + "yocto-queue": "^0.1.0" + } + } + } + }, + "jest-circus": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-circus/-/jest-circus-29.7.0.tgz", + "integrity": "sha512-3E1nCMgipcTkCocFwM90XXQab9bS+GMsjdpmPrlelaxwD93Ad8iVEjX/vvHPdLPnFf+L40u+5+iutRdA1N9myw==", + "dev": true, + "requires": { + "@jest/environment": "^29.7.0", + "@jest/expect": "^29.7.0", + "@jest/test-result": "^29.7.0", + "@jest/types": "^29.6.3", + "@types/node": "*", + "chalk": "^4.0.0", + "co": "^4.6.0", + "dedent": "^1.0.0", + "is-generator-fn": "^2.0.0", + "jest-each": "^29.7.0", + "jest-matcher-utils": "^29.7.0", + "jest-message-util": "^29.7.0", + "jest-runtime": "^29.7.0", + "jest-snapshot": "^29.7.0", + "jest-util": "^29.7.0", + "p-limit": "^3.1.0", + "pretty-format": "^29.7.0", + "pure-rand": "^6.0.0", + "slash": "^3.0.0", + "stack-utils": "^2.0.3" + }, + "dependencies": { + "@jest/schemas": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/@jest/schemas/-/schemas-29.6.3.tgz", + "integrity": "sha512-mo5j5X+jIZmJQveBKeS/clAueipV7KgiX1vMgCxam1RNYiqE1w62n0/tJJnHtjW8ZHcQco5gY85jA3mi0L+nSA==", + "dev": true, + "requires": { + "@sinclair/typebox": "^0.27.8" + } + }, + "@jest/types": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/@jest/types/-/types-29.6.3.tgz", + "integrity": "sha512-u3UPsIilWKOM3F9CXtrG8LEJmNxwoCQC/XVj4IKYXvvpx7QIi/Kg1LI5uDmDpKlac62NUtX7eLjRh+jVZcLOzw==", + "dev": true, + "requires": { + "@jest/schemas": "^29.6.3", + "@types/istanbul-lib-coverage": "^2.0.0", + "@types/istanbul-reports": "^3.0.0", + "@types/node": "*", + "@types/yargs": "^17.0.8", + "chalk": "^4.0.0" + } + }, + "@sinclair/typebox": { + "version": "0.27.8", + "resolved": "https://registry.npmjs.org/@sinclair/typebox/-/typebox-0.27.8.tgz", + "integrity": "sha512-+Fj43pSMwJs4KRrH/938Uf+uAELIgVBmQzg/q1YG10djyfA3TnrU8N8XzqCh/okZdszqBQTZf96idMfE5lnwTA==", + "dev": true + }, + "ansi-styles": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz", + "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==", + "dev": true + }, + "diff-sequences": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/diff-sequences/-/diff-sequences-29.6.3.tgz", + "integrity": "sha512-EjePK1srD3P08o2j4f0ExnylqRs5B9tJjcp9t1krH2qRi8CCdsYfwe9JgSLurFBWwq4uOlipzfk5fHNvwFKr8Q==", + "dev": true + }, + "jest-diff": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-diff/-/jest-diff-29.7.0.tgz", + "integrity": "sha512-LMIgiIrhigmPrs03JHpxUh2yISK3vLFPkAodPeo0+BuF7wA2FoQbkEg1u8gBYBThncu7e1oEDUfIXVuTqLRUjw==", + "dev": true, + "requires": { + "chalk": "^4.0.0", + "diff-sequences": "^29.6.3", + "jest-get-type": "^29.6.3", + "pretty-format": "^29.7.0" + } + }, + "jest-get-type": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/jest-get-type/-/jest-get-type-29.6.3.tgz", + "integrity": "sha512-zrteXnqYxfQh7l5FHyL38jL39di8H8rHoecLH3JNxH3BwOrBsNeabdap5e0I23lD4HHI8W5VFBZqG4Eaq5LNcw==", + "dev": true + }, + "jest-matcher-utils": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-matcher-utils/-/jest-matcher-utils-29.7.0.tgz", + "integrity": "sha512-sBkD+Xi9DtcChsI3L3u0+N0opgPYnCRPtGcQYrgXmR+hmt/fYfWAL0xRXYU8eWOdfuLgBe0YCW3AFtnRLagq/g==", + "dev": true, + "requires": { + "chalk": "^4.0.0", + "jest-diff": "^29.7.0", + "jest-get-type": "^29.6.3", + "pretty-format": "^29.7.0" + } + }, + "jest-message-util": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-message-util/-/jest-message-util-29.7.0.tgz", + "integrity": "sha512-GBEV4GRADeP+qtB2+6u61stea8mGcOT4mCtrYISZwfu9/ISHFJ/5zOMXYbpBE9RsS5+Gb63DW4FgmnKJ79Kf6w==", + "dev": true, + "requires": { + "@babel/code-frame": "^7.12.13", + "@jest/types": "^29.6.3", + "@types/stack-utils": "^2.0.0", + "chalk": "^4.0.0", + "graceful-fs": "^4.2.9", + "micromatch": "^4.0.4", + "pretty-format": "^29.7.0", + "slash": "^3.0.0", + "stack-utils": "^2.0.3" + } + }, + "jest-util": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-util/-/jest-util-29.7.0.tgz", + "integrity": "sha512-z6EbKajIpqGKU56y5KBUgy1dt1ihhQJgWzUlZHArA/+X2ad7Cb5iF+AK1EWVL/Bo7Rz9uurpqw6SiBCefUbCGA==", + "dev": true, + "requires": { + "@jest/types": "^29.6.3", + "@types/node": "*", + "chalk": "^4.0.0", + "ci-info": "^3.2.0", + "graceful-fs": "^4.2.9", + "picomatch": "^2.2.3" + } + }, + "p-limit": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", + "integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==", + "dev": true, + "requires": { + "yocto-queue": "^0.1.0" + } + }, + "pretty-format": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-29.7.0.tgz", + "integrity": "sha512-Pdlw/oPxN+aXdmM9R00JVC9WVFoCLTKJvDVLgmJ+qAffBMxsV85l/Lu7sNx4zSzPyoL2euImuEwHhOXdEgNFZQ==", + "dev": true, + "requires": { + "@jest/schemas": "^29.6.3", + "ansi-styles": "^5.0.0", + "react-is": "^18.0.0" + } + }, + "react-is": { + "version": "18.2.0", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-18.2.0.tgz", + "integrity": "sha512-xWGDIW6x921xtzPkhiULtthJHoJvBbF3q26fzloPCK0hsvxtPVelvftw3zjbHWSkR2km9Z+4uxbDDK/6Zw9B8w==", + "dev": true + } + } + }, + "jest-cli": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-cli/-/jest-cli-29.7.0.tgz", + "integrity": "sha512-OVVobw2IubN/GSYsxETi+gOe7Ka59EFMR/twOU3Jb2GnKKeMGJB5SGUUrEz3SFVmJASUdZUzy83sLNNQ2gZslg==", + "dev": true, + "requires": { + "@jest/core": "^29.7.0", + "@jest/test-result": "^29.7.0", + "@jest/types": "^29.6.3", + "chalk": "^4.0.0", + "create-jest": "^29.7.0", + "exit": "^0.1.2", + "import-local": "^3.0.2", + "jest-config": "^29.7.0", + "jest-util": "^29.7.0", + "jest-validate": "^29.7.0", + "yargs": "^17.3.1" + }, + "dependencies": { + "@jest/schemas": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/@jest/schemas/-/schemas-29.6.3.tgz", + "integrity": "sha512-mo5j5X+jIZmJQveBKeS/clAueipV7KgiX1vMgCxam1RNYiqE1w62n0/tJJnHtjW8ZHcQco5gY85jA3mi0L+nSA==", + "dev": true, + "requires": { + "@sinclair/typebox": "^0.27.8" + } + }, + "@jest/types": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/@jest/types/-/types-29.6.3.tgz", + "integrity": "sha512-u3UPsIilWKOM3F9CXtrG8LEJmNxwoCQC/XVj4IKYXvvpx7QIi/Kg1LI5uDmDpKlac62NUtX7eLjRh+jVZcLOzw==", + "dev": true, + "requires": { + "@jest/schemas": "^29.6.3", + "@types/istanbul-lib-coverage": "^2.0.0", + "@types/istanbul-reports": "^3.0.0", + "@types/node": "*", + "@types/yargs": "^17.0.8", + "chalk": "^4.0.0" + } + }, + "@sinclair/typebox": { + "version": "0.27.8", + "resolved": "https://registry.npmjs.org/@sinclair/typebox/-/typebox-0.27.8.tgz", + "integrity": "sha512-+Fj43pSMwJs4KRrH/938Uf+uAELIgVBmQzg/q1YG10djyfA3TnrU8N8XzqCh/okZdszqBQTZf96idMfE5lnwTA==", + "dev": true + }, + "jest-util": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-util/-/jest-util-29.7.0.tgz", + "integrity": "sha512-z6EbKajIpqGKU56y5KBUgy1dt1ihhQJgWzUlZHArA/+X2ad7Cb5iF+AK1EWVL/Bo7Rz9uurpqw6SiBCefUbCGA==", + "dev": true, + "requires": { + "@jest/types": "^29.6.3", + "@types/node": "*", + "chalk": "^4.0.0", + "ci-info": "^3.2.0", + "graceful-fs": "^4.2.9", + "picomatch": "^2.2.3" + } + } + } + }, + "jest-config": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-config/-/jest-config-29.7.0.tgz", + "integrity": "sha512-uXbpfeQ7R6TZBqI3/TxCU4q4ttk3u0PJeC+E0zbfSoSjq6bJ7buBPxzQPL0ifrkY4DNu4JUdk0ImlBUYi840eQ==", + "dev": true, + "requires": { + "@babel/core": "^7.11.6", + "@jest/test-sequencer": "^29.7.0", + "@jest/types": "^29.6.3", + "babel-jest": "^29.7.0", + "chalk": "^4.0.0", + "ci-info": "^3.2.0", + "deepmerge": "^4.2.2", + "glob": "^7.1.3", + "graceful-fs": "^4.2.9", + "jest-circus": "^29.7.0", + "jest-environment-node": "^29.7.0", + "jest-get-type": "^29.6.3", + "jest-regex-util": "^29.6.3", + "jest-resolve": "^29.7.0", + "jest-runner": "^29.7.0", + "jest-util": "^29.7.0", + "jest-validate": "^29.7.0", + "micromatch": "^4.0.4", + "parse-json": "^5.2.0", + "pretty-format": "^29.7.0", + "slash": "^3.0.0", + "strip-json-comments": "^3.1.1" + }, + "dependencies": { + "@jest/schemas": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/@jest/schemas/-/schemas-29.6.3.tgz", + "integrity": "sha512-mo5j5X+jIZmJQveBKeS/clAueipV7KgiX1vMgCxam1RNYiqE1w62n0/tJJnHtjW8ZHcQco5gY85jA3mi0L+nSA==", + "dev": true, + "requires": { + "@sinclair/typebox": "^0.27.8" + } + }, + "@jest/types": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/@jest/types/-/types-29.6.3.tgz", + "integrity": "sha512-u3UPsIilWKOM3F9CXtrG8LEJmNxwoCQC/XVj4IKYXvvpx7QIi/Kg1LI5uDmDpKlac62NUtX7eLjRh+jVZcLOzw==", + "dev": true, + "requires": { + "@jest/schemas": "^29.6.3", + "@types/istanbul-lib-coverage": "^2.0.0", + "@types/istanbul-reports": "^3.0.0", + "@types/node": "*", + "@types/yargs": "^17.0.8", + "chalk": "^4.0.0" + } + }, + "@sinclair/typebox": { + "version": "0.27.8", + "resolved": "https://registry.npmjs.org/@sinclair/typebox/-/typebox-0.27.8.tgz", + "integrity": "sha512-+Fj43pSMwJs4KRrH/938Uf+uAELIgVBmQzg/q1YG10djyfA3TnrU8N8XzqCh/okZdszqBQTZf96idMfE5lnwTA==", + "dev": true + }, + "ansi-styles": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz", + "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==", + "dev": true + }, + "jest-get-type": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/jest-get-type/-/jest-get-type-29.6.3.tgz", + "integrity": "sha512-zrteXnqYxfQh7l5FHyL38jL39di8H8rHoecLH3JNxH3BwOrBsNeabdap5e0I23lD4HHI8W5VFBZqG4Eaq5LNcw==", + "dev": true + }, + "jest-util": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-util/-/jest-util-29.7.0.tgz", + "integrity": "sha512-z6EbKajIpqGKU56y5KBUgy1dt1ihhQJgWzUlZHArA/+X2ad7Cb5iF+AK1EWVL/Bo7Rz9uurpqw6SiBCefUbCGA==", + "dev": true, + "requires": { + "@jest/types": "^29.6.3", + "@types/node": "*", + "chalk": "^4.0.0", + "ci-info": "^3.2.0", + "graceful-fs": "^4.2.9", + "picomatch": "^2.2.3" + } + }, + "pretty-format": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-29.7.0.tgz", + "integrity": "sha512-Pdlw/oPxN+aXdmM9R00JVC9WVFoCLTKJvDVLgmJ+qAffBMxsV85l/Lu7sNx4zSzPyoL2euImuEwHhOXdEgNFZQ==", + "dev": true, + "requires": { + "@jest/schemas": "^29.6.3", + "ansi-styles": "^5.0.0", + "react-is": "^18.0.0" + } + }, + "react-is": { + "version": "18.2.0", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-18.2.0.tgz", + "integrity": "sha512-xWGDIW6x921xtzPkhiULtthJHoJvBbF3q26fzloPCK0hsvxtPVelvftw3zjbHWSkR2km9Z+4uxbDDK/6Zw9B8w==", + "dev": true + } + } + }, + "jest-diff": { + "version": "28.1.3", + "resolved": "https://registry.npmjs.org/jest-diff/-/jest-diff-28.1.3.tgz", + "integrity": "sha512-8RqP1B/OXzjjTWkqMX67iqgwBVJRgCyKD3L9nq+6ZqJMdvjE8RgHktqZ6jNrkdMT+dJuYNI3rhQpxaz7drJHfw==", + "dev": true, + "requires": { + "chalk": "^4.0.0", + "diff-sequences": "^28.1.1", + "jest-get-type": "^28.0.2", + "pretty-format": "^28.1.3" + } + }, + "jest-docblock": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-docblock/-/jest-docblock-29.7.0.tgz", + "integrity": "sha512-q617Auw3A612guyaFgsbFeYpNP5t2aoUNLwBUbc/0kD1R4t9ixDbyFTHd1nok4epoVFpr7PmeWHrhvuV3XaJ4g==", + "dev": true, + "requires": { + "detect-newline": "^3.0.0" + } + }, + "jest-each": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-each/-/jest-each-29.7.0.tgz", + "integrity": "sha512-gns+Er14+ZrEoC5fhOfYCY1LOHHr0TI+rQUHZS8Ttw2l7gl+80eHc/gFf2Ktkw0+SIACDTeWvpFcv3B04VembQ==", + "dev": true, + "requires": { + "@jest/types": "^29.6.3", + "chalk": "^4.0.0", + "jest-get-type": "^29.6.3", + "jest-util": "^29.7.0", + "pretty-format": "^29.7.0" + }, + "dependencies": { + "@jest/schemas": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/@jest/schemas/-/schemas-29.6.3.tgz", + "integrity": "sha512-mo5j5X+jIZmJQveBKeS/clAueipV7KgiX1vMgCxam1RNYiqE1w62n0/tJJnHtjW8ZHcQco5gY85jA3mi0L+nSA==", + "dev": true, + "requires": { + "@sinclair/typebox": "^0.27.8" + } + }, + "@jest/types": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/@jest/types/-/types-29.6.3.tgz", + "integrity": "sha512-u3UPsIilWKOM3F9CXtrG8LEJmNxwoCQC/XVj4IKYXvvpx7QIi/Kg1LI5uDmDpKlac62NUtX7eLjRh+jVZcLOzw==", + "dev": true, + "requires": { + "@jest/schemas": "^29.6.3", + "@types/istanbul-lib-coverage": "^2.0.0", + "@types/istanbul-reports": "^3.0.0", + "@types/node": "*", + "@types/yargs": "^17.0.8", + "chalk": "^4.0.0" + } + }, + "@sinclair/typebox": { + "version": "0.27.8", + "resolved": "https://registry.npmjs.org/@sinclair/typebox/-/typebox-0.27.8.tgz", + "integrity": "sha512-+Fj43pSMwJs4KRrH/938Uf+uAELIgVBmQzg/q1YG10djyfA3TnrU8N8XzqCh/okZdszqBQTZf96idMfE5lnwTA==", + "dev": true + }, + "ansi-styles": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz", + "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==", + "dev": true + }, + "jest-get-type": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/jest-get-type/-/jest-get-type-29.6.3.tgz", + "integrity": "sha512-zrteXnqYxfQh7l5FHyL38jL39di8H8rHoecLH3JNxH3BwOrBsNeabdap5e0I23lD4HHI8W5VFBZqG4Eaq5LNcw==", + "dev": true + }, + "jest-util": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-util/-/jest-util-29.7.0.tgz", + "integrity": "sha512-z6EbKajIpqGKU56y5KBUgy1dt1ihhQJgWzUlZHArA/+X2ad7Cb5iF+AK1EWVL/Bo7Rz9uurpqw6SiBCefUbCGA==", + "dev": true, + "requires": { + "@jest/types": "^29.6.3", + "@types/node": "*", + "chalk": "^4.0.0", + "ci-info": "^3.2.0", + "graceful-fs": "^4.2.9", + "picomatch": "^2.2.3" + } + }, + "pretty-format": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-29.7.0.tgz", + "integrity": "sha512-Pdlw/oPxN+aXdmM9R00JVC9WVFoCLTKJvDVLgmJ+qAffBMxsV85l/Lu7sNx4zSzPyoL2euImuEwHhOXdEgNFZQ==", + "dev": true, + "requires": { + "@jest/schemas": "^29.6.3", + "ansi-styles": "^5.0.0", + "react-is": "^18.0.0" + } + }, + "react-is": { + "version": "18.2.0", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-18.2.0.tgz", + "integrity": "sha512-xWGDIW6x921xtzPkhiULtthJHoJvBbF3q26fzloPCK0hsvxtPVelvftw3zjbHWSkR2km9Z+4uxbDDK/6Zw9B8w==", + "dev": true + } + } + }, + "jest-environment-node": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-environment-node/-/jest-environment-node-29.7.0.tgz", + "integrity": "sha512-DOSwCRqXirTOyheM+4d5YZOrWcdu0LNZ87ewUoywbcb2XR4wKgqiG8vNeYwhjFMbEkfju7wx2GYH0P2gevGvFw==", + "dev": true, + "requires": { + "@jest/environment": "^29.7.0", + "@jest/fake-timers": "^29.7.0", + "@jest/types": "^29.6.3", + "@types/node": "*", + "jest-mock": "^29.7.0", + "jest-util": "^29.7.0" + }, + "dependencies": { + "@jest/schemas": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/@jest/schemas/-/schemas-29.6.3.tgz", + "integrity": "sha512-mo5j5X+jIZmJQveBKeS/clAueipV7KgiX1vMgCxam1RNYiqE1w62n0/tJJnHtjW8ZHcQco5gY85jA3mi0L+nSA==", + "dev": true, + "requires": { + "@sinclair/typebox": "^0.27.8" + } + }, + "@jest/types": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/@jest/types/-/types-29.6.3.tgz", + "integrity": "sha512-u3UPsIilWKOM3F9CXtrG8LEJmNxwoCQC/XVj4IKYXvvpx7QIi/Kg1LI5uDmDpKlac62NUtX7eLjRh+jVZcLOzw==", + "dev": true, + "requires": { + "@jest/schemas": "^29.6.3", + "@types/istanbul-lib-coverage": "^2.0.0", + "@types/istanbul-reports": "^3.0.0", + "@types/node": "*", + "@types/yargs": "^17.0.8", + "chalk": "^4.0.0" + } + }, + "@sinclair/typebox": { + "version": "0.27.8", + "resolved": "https://registry.npmjs.org/@sinclair/typebox/-/typebox-0.27.8.tgz", + "integrity": "sha512-+Fj43pSMwJs4KRrH/938Uf+uAELIgVBmQzg/q1YG10djyfA3TnrU8N8XzqCh/okZdszqBQTZf96idMfE5lnwTA==", + "dev": true + }, + "jest-util": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-util/-/jest-util-29.7.0.tgz", + "integrity": "sha512-z6EbKajIpqGKU56y5KBUgy1dt1ihhQJgWzUlZHArA/+X2ad7Cb5iF+AK1EWVL/Bo7Rz9uurpqw6SiBCefUbCGA==", + "dev": true, + "requires": { + "@jest/types": "^29.6.3", + "@types/node": "*", + "chalk": "^4.0.0", + "ci-info": "^3.2.0", + "graceful-fs": "^4.2.9", + "picomatch": "^2.2.3" + } + } + } + }, + "jest-get-type": { + "version": "28.0.2", + "resolved": "https://registry.npmjs.org/jest-get-type/-/jest-get-type-28.0.2.tgz", + "integrity": "sha512-ioj2w9/DxSYHfOm5lJKCdcAmPJzQXmbM/Url3rhlghrPvT3tt+7a/+oXc9azkKmLvoiXjtV83bEWqi+vs5nlPA==", + "dev": true + }, + "jest-haste-map": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-haste-map/-/jest-haste-map-29.7.0.tgz", + "integrity": "sha512-fP8u2pyfqx0K1rGn1R9pyE0/KTn+G7PxktWidOBTqFPLYX0b9ksaMFkhK5vrS3DVun09pckLdlx90QthlW7AmA==", + "dev": true, + "requires": { + "@jest/types": "^29.6.3", + "@types/graceful-fs": "^4.1.3", + "@types/node": "*", + "anymatch": "^3.0.3", + "fb-watchman": "^2.0.0", + "fsevents": "^2.3.2", + "graceful-fs": "^4.2.9", + "jest-regex-util": "^29.6.3", + "jest-util": "^29.7.0", + "jest-worker": "^29.7.0", + "micromatch": "^4.0.4", + "walker": "^1.0.8" + }, + "dependencies": { + "@jest/schemas": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/@jest/schemas/-/schemas-29.6.3.tgz", + "integrity": "sha512-mo5j5X+jIZmJQveBKeS/clAueipV7KgiX1vMgCxam1RNYiqE1w62n0/tJJnHtjW8ZHcQco5gY85jA3mi0L+nSA==", + "dev": true, + "requires": { + "@sinclair/typebox": "^0.27.8" + } + }, + "@jest/types": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/@jest/types/-/types-29.6.3.tgz", + "integrity": "sha512-u3UPsIilWKOM3F9CXtrG8LEJmNxwoCQC/XVj4IKYXvvpx7QIi/Kg1LI5uDmDpKlac62NUtX7eLjRh+jVZcLOzw==", + "dev": true, + "requires": { + "@jest/schemas": "^29.6.3", + "@types/istanbul-lib-coverage": "^2.0.0", + "@types/istanbul-reports": "^3.0.0", + "@types/node": "*", + "@types/yargs": "^17.0.8", + "chalk": "^4.0.0" + } + }, + "@sinclair/typebox": { + "version": "0.27.8", + "resolved": "https://registry.npmjs.org/@sinclair/typebox/-/typebox-0.27.8.tgz", + "integrity": "sha512-+Fj43pSMwJs4KRrH/938Uf+uAELIgVBmQzg/q1YG10djyfA3TnrU8N8XzqCh/okZdszqBQTZf96idMfE5lnwTA==", + "dev": true + }, + "jest-util": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-util/-/jest-util-29.7.0.tgz", + "integrity": "sha512-z6EbKajIpqGKU56y5KBUgy1dt1ihhQJgWzUlZHArA/+X2ad7Cb5iF+AK1EWVL/Bo7Rz9uurpqw6SiBCefUbCGA==", + "dev": true, + "requires": { + "@jest/types": "^29.6.3", + "@types/node": "*", + "chalk": "^4.0.0", + "ci-info": "^3.2.0", + "graceful-fs": "^4.2.9", + "picomatch": "^2.2.3" + } + } + } + }, + "jest-joi": { + "version": "1.1.17", + "resolved": "https://registry.npmjs.org/jest-joi/-/jest-joi-1.1.17.tgz", + "integrity": "sha512-5FBCnY27yx4eMBXiU83a1DYjjCJKtp8Auc2hD0vGiPN9/EHDR514O6zh0o4glMBoHBOyWOQ2qkGWoouu1456hA==", + "dev": true, + "requires": { + "@aitodotai/json-stringify-pretty-compact": "^1.3.0", + "chalk": "^4.1.2", + "jest-matcher-utils": "^29.4.1" + }, + "dependencies": { + "@jest/schemas": { + "version": "29.4.3", + "resolved": "https://registry.npmjs.org/@jest/schemas/-/schemas-29.4.3.tgz", + "integrity": "sha512-VLYKXQmtmuEz6IxJsrZwzG9NvtkQsWNnWMsKxqWNu3+CnfzJQhp0WDDKWLVV9hLKr0l3SLLFRqcYHjhtyuDVxg==", + "dev": true, + "requires": { + "@sinclair/typebox": "^0.25.16" + } + }, + "@sinclair/typebox": { + "version": "0.25.24", + "resolved": "https://registry.npmjs.org/@sinclair/typebox/-/typebox-0.25.24.tgz", + "integrity": "sha512-XJfwUVUKDHF5ugKwIcxEgc9k8b7HbznCp6eUfWgu710hMPNIO4aw4/zB5RogDQz8nd6gyCDpU9O/m6qYEWY6yQ==", + "dev": true + }, + "ansi-styles": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz", + "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==", + "dev": true + }, + "diff-sequences": { + "version": "29.4.3", + "resolved": "https://registry.npmjs.org/diff-sequences/-/diff-sequences-29.4.3.tgz", + "integrity": "sha512-ofrBgwpPhCD85kMKtE9RYFFq6OC1A89oW2vvgWZNCwxrUpRUILopY7lsYyMDSjc8g6U6aiO0Qubg6r4Wgt5ZnA==", + "dev": true + }, + "jest-diff": { + "version": "29.5.0", + "resolved": "https://registry.npmjs.org/jest-diff/-/jest-diff-29.5.0.tgz", + "integrity": "sha512-LtxijLLZBduXnHSniy0WMdaHjmQnt3g5sa16W4p0HqukYTTsyTW3GD1q41TyGl5YFXj/5B2U6dlh5FM1LIMgxw==", + "dev": true, + "requires": { + "chalk": "^4.0.0", + "diff-sequences": "^29.4.3", + "jest-get-type": "^29.4.3", + "pretty-format": "^29.5.0" + } + }, + "jest-get-type": { + "version": "29.4.3", + "resolved": "https://registry.npmjs.org/jest-get-type/-/jest-get-type-29.4.3.tgz", + "integrity": "sha512-J5Xez4nRRMjk8emnTpWrlkyb9pfRQQanDrvWHhsR1+VUfbwxi30eVcZFlcdGInRibU4G5LwHXpI7IRHU0CY+gg==", + "dev": true + }, + "jest-matcher-utils": { + "version": "29.5.0", + "resolved": "https://registry.npmjs.org/jest-matcher-utils/-/jest-matcher-utils-29.5.0.tgz", + "integrity": "sha512-lecRtgm/rjIK0CQ7LPQwzCs2VwW6WAahA55YBuI+xqmhm7LAaxokSB8C97yJeYyT+HvQkH741StzpU41wohhWw==", + "dev": true, + "requires": { + "chalk": "^4.0.0", + "jest-diff": "^29.5.0", + "jest-get-type": "^29.4.3", + "pretty-format": "^29.5.0" + } + }, + "pretty-format": { + "version": "29.5.0", + "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-29.5.0.tgz", + "integrity": "sha512-V2mGkI31qdttvTFX7Mt4efOqHXqJWMu4/r66Xh3Z3BwZaPfPJgp6/gbwoujRpPUtfEF6AUUWx3Jim3GCw5g/Qw==", + "dev": true, + "requires": { + "@jest/schemas": "^29.4.3", + "ansi-styles": "^5.0.0", + "react-is": "^18.0.0" + } + }, + "react-is": { + "version": "18.2.0", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-18.2.0.tgz", + "integrity": "sha512-xWGDIW6x921xtzPkhiULtthJHoJvBbF3q26fzloPCK0hsvxtPVelvftw3zjbHWSkR2km9Z+4uxbDDK/6Zw9B8w==", + "dev": true + } + } + }, + "jest-leak-detector": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-leak-detector/-/jest-leak-detector-29.7.0.tgz", + "integrity": "sha512-kYA8IJcSYtST2BY9I+SMC32nDpBT3J2NvWJx8+JCuCdl/CR1I4EKUJROiP8XtCcxqgTTBGJNdbB1A8XRKbTetw==", + "dev": true, + "requires": { + "jest-get-type": "^29.6.3", + "pretty-format": "^29.7.0" + }, + "dependencies": { + "@jest/schemas": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/@jest/schemas/-/schemas-29.6.3.tgz", + "integrity": "sha512-mo5j5X+jIZmJQveBKeS/clAueipV7KgiX1vMgCxam1RNYiqE1w62n0/tJJnHtjW8ZHcQco5gY85jA3mi0L+nSA==", + "dev": true, + "requires": { + "@sinclair/typebox": "^0.27.8" + } + }, + "@sinclair/typebox": { + "version": "0.27.8", + "resolved": "https://registry.npmjs.org/@sinclair/typebox/-/typebox-0.27.8.tgz", + "integrity": "sha512-+Fj43pSMwJs4KRrH/938Uf+uAELIgVBmQzg/q1YG10djyfA3TnrU8N8XzqCh/okZdszqBQTZf96idMfE5lnwTA==", + "dev": true + }, + "ansi-styles": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz", + "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==", + "dev": true + }, + "jest-get-type": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/jest-get-type/-/jest-get-type-29.6.3.tgz", + "integrity": "sha512-zrteXnqYxfQh7l5FHyL38jL39di8H8rHoecLH3JNxH3BwOrBsNeabdap5e0I23lD4HHI8W5VFBZqG4Eaq5LNcw==", + "dev": true + }, + "pretty-format": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-29.7.0.tgz", + "integrity": "sha512-Pdlw/oPxN+aXdmM9R00JVC9WVFoCLTKJvDVLgmJ+qAffBMxsV85l/Lu7sNx4zSzPyoL2euImuEwHhOXdEgNFZQ==", + "dev": true, + "requires": { + "@jest/schemas": "^29.6.3", + "ansi-styles": "^5.0.0", + "react-is": "^18.0.0" + } + }, + "react-is": { + "version": "18.2.0", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-18.2.0.tgz", + "integrity": "sha512-xWGDIW6x921xtzPkhiULtthJHoJvBbF3q26fzloPCK0hsvxtPVelvftw3zjbHWSkR2km9Z+4uxbDDK/6Zw9B8w==", + "dev": true + } + } + }, + "jest-matcher-utils": { + "version": "28.1.3", + "resolved": "https://registry.npmjs.org/jest-matcher-utils/-/jest-matcher-utils-28.1.3.tgz", + "integrity": "sha512-kQeJ7qHemKfbzKoGjHHrRKH6atgxMk8Enkk2iPQ3XwO6oE/KYD8lMYOziCkeSB9G4adPM4nR1DE8Tf5JeWH6Bw==", + "dev": true, + "requires": { + "chalk": "^4.0.0", + "jest-diff": "^28.1.3", + "jest-get-type": "^28.0.2", + "pretty-format": "^28.1.3" + } + }, + "jest-message-util": { + "version": "28.1.3", + "resolved": "https://registry.npmjs.org/jest-message-util/-/jest-message-util-28.1.3.tgz", + "integrity": "sha512-PFdn9Iewbt575zKPf1286Ht9EPoJmYT7P0kY+RibeYZ2XtOr53pDLEFoTWXbd1h4JiGiWpTBC84fc8xMXQMb7g==", + "dev": true, + "requires": { + "@babel/code-frame": "^7.12.13", + "@jest/types": "^28.1.3", + "@types/stack-utils": "^2.0.0", + "chalk": "^4.0.0", + "graceful-fs": "^4.2.9", + "micromatch": "^4.0.4", + "pretty-format": "^28.1.3", + "slash": "^3.0.0", + "stack-utils": "^2.0.3" + } + }, + "jest-mock": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-mock/-/jest-mock-29.7.0.tgz", + "integrity": "sha512-ITOMZn+UkYS4ZFh83xYAOzWStloNzJFO2s8DWrE4lhtGD+AorgnbkiKERe4wQVBydIGPx059g6riW5Btp6Llnw==", + "dev": true, + "requires": { + "@jest/types": "^29.6.3", + "@types/node": "*", + "jest-util": "^29.7.0" + }, + "dependencies": { + "@jest/schemas": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/@jest/schemas/-/schemas-29.6.3.tgz", + "integrity": "sha512-mo5j5X+jIZmJQveBKeS/clAueipV7KgiX1vMgCxam1RNYiqE1w62n0/tJJnHtjW8ZHcQco5gY85jA3mi0L+nSA==", + "dev": true, + "requires": { + "@sinclair/typebox": "^0.27.8" + } + }, + "@jest/types": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/@jest/types/-/types-29.6.3.tgz", + "integrity": "sha512-u3UPsIilWKOM3F9CXtrG8LEJmNxwoCQC/XVj4IKYXvvpx7QIi/Kg1LI5uDmDpKlac62NUtX7eLjRh+jVZcLOzw==", + "dev": true, + "requires": { + "@jest/schemas": "^29.6.3", + "@types/istanbul-lib-coverage": "^2.0.0", + "@types/istanbul-reports": "^3.0.0", + "@types/node": "*", + "@types/yargs": "^17.0.8", + "chalk": "^4.0.0" + } + }, + "@sinclair/typebox": { + "version": "0.27.8", + "resolved": "https://registry.npmjs.org/@sinclair/typebox/-/typebox-0.27.8.tgz", + "integrity": "sha512-+Fj43pSMwJs4KRrH/938Uf+uAELIgVBmQzg/q1YG10djyfA3TnrU8N8XzqCh/okZdszqBQTZf96idMfE5lnwTA==", + "dev": true + }, + "jest-util": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-util/-/jest-util-29.7.0.tgz", + "integrity": "sha512-z6EbKajIpqGKU56y5KBUgy1dt1ihhQJgWzUlZHArA/+X2ad7Cb5iF+AK1EWVL/Bo7Rz9uurpqw6SiBCefUbCGA==", + "dev": true, + "requires": { + "@jest/types": "^29.6.3", + "@types/node": "*", + "chalk": "^4.0.0", + "ci-info": "^3.2.0", + "graceful-fs": "^4.2.9", + "picomatch": "^2.2.3" + } + } + } + }, + "jest-pnp-resolver": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/jest-pnp-resolver/-/jest-pnp-resolver-1.2.3.tgz", + "integrity": "sha512-+3NpwQEnRoIBtx4fyhblQDPgJI0H1IEIkX7ShLUjPGA7TtUTvI1oiKi3SR4oBR0hQhQR80l4WAe5RrXBwWMA8w==", + "dev": true + }, + "jest-regex-util": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/jest-regex-util/-/jest-regex-util-29.6.3.tgz", + "integrity": "sha512-KJJBsRCyyLNWCNBOvZyRDnAIfUiRJ8v+hOBQYGn8gDyF3UegwiP4gwRR3/SDa42g1YbVycTidUF3rKjyLFDWbg==", + "dev": true + }, + "jest-resolve": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-resolve/-/jest-resolve-29.7.0.tgz", + "integrity": "sha512-IOVhZSrg+UvVAshDSDtHyFCCBUl/Q3AAJv8iZ6ZjnZ74xzvwuzLXid9IIIPgTnY62SJjfuupMKZsZQRsCvxEgA==", + "dev": true, + "requires": { + "chalk": "^4.0.0", + "graceful-fs": "^4.2.9", + "jest-haste-map": "^29.7.0", + "jest-pnp-resolver": "^1.2.2", + "jest-util": "^29.7.0", + "jest-validate": "^29.7.0", + "resolve": "^1.20.0", + "resolve.exports": "^2.0.0", + "slash": "^3.0.0" + }, + "dependencies": { + "@jest/schemas": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/@jest/schemas/-/schemas-29.6.3.tgz", + "integrity": "sha512-mo5j5X+jIZmJQveBKeS/clAueipV7KgiX1vMgCxam1RNYiqE1w62n0/tJJnHtjW8ZHcQco5gY85jA3mi0L+nSA==", + "dev": true, + "requires": { + "@sinclair/typebox": "^0.27.8" + } + }, + "@jest/types": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/@jest/types/-/types-29.6.3.tgz", + "integrity": "sha512-u3UPsIilWKOM3F9CXtrG8LEJmNxwoCQC/XVj4IKYXvvpx7QIi/Kg1LI5uDmDpKlac62NUtX7eLjRh+jVZcLOzw==", + "dev": true, + "requires": { + "@jest/schemas": "^29.6.3", + "@types/istanbul-lib-coverage": "^2.0.0", + "@types/istanbul-reports": "^3.0.0", + "@types/node": "*", + "@types/yargs": "^17.0.8", + "chalk": "^4.0.0" + } + }, + "@sinclair/typebox": { + "version": "0.27.8", + "resolved": "https://registry.npmjs.org/@sinclair/typebox/-/typebox-0.27.8.tgz", + "integrity": "sha512-+Fj43pSMwJs4KRrH/938Uf+uAELIgVBmQzg/q1YG10djyfA3TnrU8N8XzqCh/okZdszqBQTZf96idMfE5lnwTA==", + "dev": true + }, + "jest-util": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-util/-/jest-util-29.7.0.tgz", + "integrity": "sha512-z6EbKajIpqGKU56y5KBUgy1dt1ihhQJgWzUlZHArA/+X2ad7Cb5iF+AK1EWVL/Bo7Rz9uurpqw6SiBCefUbCGA==", + "dev": true, + "requires": { + "@jest/types": "^29.6.3", + "@types/node": "*", + "chalk": "^4.0.0", + "ci-info": "^3.2.0", + "graceful-fs": "^4.2.9", + "picomatch": "^2.2.3" + } + } + } + }, + "jest-resolve-dependencies": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-resolve-dependencies/-/jest-resolve-dependencies-29.7.0.tgz", + "integrity": "sha512-un0zD/6qxJ+S0et7WxeI3H5XSe9lTBBR7bOHCHXkKR6luG5mwDDlIzVQ0V5cZCuoTgEdcdwzTghYkTWfubi+nA==", + "dev": true, + "requires": { + "jest-regex-util": "^29.6.3", + "jest-snapshot": "^29.7.0" + } + }, + "jest-runner": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-runner/-/jest-runner-29.7.0.tgz", + "integrity": "sha512-fsc4N6cPCAahybGBfTRcq5wFR6fpLznMg47sY5aDpsoejOcVYFb07AHuSnR0liMcPTgBsA3ZJL6kFOjPdoNipQ==", + "dev": true, + "requires": { + "@jest/console": "^29.7.0", + "@jest/environment": "^29.7.0", + "@jest/test-result": "^29.7.0", + "@jest/transform": "^29.7.0", + "@jest/types": "^29.6.3", + "@types/node": "*", + "chalk": "^4.0.0", + "emittery": "^0.13.1", + "graceful-fs": "^4.2.9", + "jest-docblock": "^29.7.0", + "jest-environment-node": "^29.7.0", + "jest-haste-map": "^29.7.0", + "jest-leak-detector": "^29.7.0", + "jest-message-util": "^29.7.0", + "jest-resolve": "^29.7.0", + "jest-runtime": "^29.7.0", + "jest-util": "^29.7.0", + "jest-watcher": "^29.7.0", + "jest-worker": "^29.7.0", + "p-limit": "^3.1.0", + "source-map-support": "0.5.13" + }, + "dependencies": { + "@jest/schemas": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/@jest/schemas/-/schemas-29.6.3.tgz", + "integrity": "sha512-mo5j5X+jIZmJQveBKeS/clAueipV7KgiX1vMgCxam1RNYiqE1w62n0/tJJnHtjW8ZHcQco5gY85jA3mi0L+nSA==", + "dev": true, + "requires": { + "@sinclair/typebox": "^0.27.8" + } + }, + "@jest/types": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/@jest/types/-/types-29.6.3.tgz", + "integrity": "sha512-u3UPsIilWKOM3F9CXtrG8LEJmNxwoCQC/XVj4IKYXvvpx7QIi/Kg1LI5uDmDpKlac62NUtX7eLjRh+jVZcLOzw==", + "dev": true, + "requires": { + "@jest/schemas": "^29.6.3", + "@types/istanbul-lib-coverage": "^2.0.0", + "@types/istanbul-reports": "^3.0.0", + "@types/node": "*", + "@types/yargs": "^17.0.8", + "chalk": "^4.0.0" + } + }, + "@sinclair/typebox": { + "version": "0.27.8", + "resolved": "https://registry.npmjs.org/@sinclair/typebox/-/typebox-0.27.8.tgz", + "integrity": "sha512-+Fj43pSMwJs4KRrH/938Uf+uAELIgVBmQzg/q1YG10djyfA3TnrU8N8XzqCh/okZdszqBQTZf96idMfE5lnwTA==", + "dev": true + }, + "ansi-styles": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz", + "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==", + "dev": true + }, + "jest-message-util": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-message-util/-/jest-message-util-29.7.0.tgz", + "integrity": "sha512-GBEV4GRADeP+qtB2+6u61stea8mGcOT4mCtrYISZwfu9/ISHFJ/5zOMXYbpBE9RsS5+Gb63DW4FgmnKJ79Kf6w==", + "dev": true, + "requires": { + "@babel/code-frame": "^7.12.13", + "@jest/types": "^29.6.3", + "@types/stack-utils": "^2.0.0", + "chalk": "^4.0.0", + "graceful-fs": "^4.2.9", + "micromatch": "^4.0.4", + "pretty-format": "^29.7.0", + "slash": "^3.0.0", + "stack-utils": "^2.0.3" + } + }, + "jest-util": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-util/-/jest-util-29.7.0.tgz", + "integrity": "sha512-z6EbKajIpqGKU56y5KBUgy1dt1ihhQJgWzUlZHArA/+X2ad7Cb5iF+AK1EWVL/Bo7Rz9uurpqw6SiBCefUbCGA==", + "dev": true, + "requires": { + "@jest/types": "^29.6.3", + "@types/node": "*", + "chalk": "^4.0.0", + "ci-info": "^3.2.0", + "graceful-fs": "^4.2.9", + "picomatch": "^2.2.3" + } + }, + "p-limit": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", + "integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==", + "dev": true, + "requires": { + "yocto-queue": "^0.1.0" + } + }, + "pretty-format": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-29.7.0.tgz", + "integrity": "sha512-Pdlw/oPxN+aXdmM9R00JVC9WVFoCLTKJvDVLgmJ+qAffBMxsV85l/Lu7sNx4zSzPyoL2euImuEwHhOXdEgNFZQ==", + "dev": true, + "requires": { + "@jest/schemas": "^29.6.3", + "ansi-styles": "^5.0.0", + "react-is": "^18.0.0" + } + }, + "react-is": { + "version": "18.2.0", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-18.2.0.tgz", + "integrity": "sha512-xWGDIW6x921xtzPkhiULtthJHoJvBbF3q26fzloPCK0hsvxtPVelvftw3zjbHWSkR2km9Z+4uxbDDK/6Zw9B8w==", + "dev": true + } + } + }, + "jest-runtime": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-runtime/-/jest-runtime-29.7.0.tgz", + "integrity": "sha512-gUnLjgwdGqW7B4LvOIkbKs9WGbn+QLqRQQ9juC6HndeDiezIwhDP+mhMwHWCEcfQ5RUXa6OPnFF8BJh5xegwwQ==", + "dev": true, + "requires": { + "@jest/environment": "^29.7.0", + "@jest/fake-timers": "^29.7.0", + "@jest/globals": "^29.7.0", + "@jest/source-map": "^29.6.3", + "@jest/test-result": "^29.7.0", + "@jest/transform": "^29.7.0", + "@jest/types": "^29.6.3", + "@types/node": "*", + "chalk": "^4.0.0", + "cjs-module-lexer": "^1.0.0", + "collect-v8-coverage": "^1.0.0", + "glob": "^7.1.3", + "graceful-fs": "^4.2.9", + "jest-haste-map": "^29.7.0", + "jest-message-util": "^29.7.0", + "jest-mock": "^29.7.0", + "jest-regex-util": "^29.6.3", + "jest-resolve": "^29.7.0", + "jest-snapshot": "^29.7.0", + "jest-util": "^29.7.0", + "slash": "^3.0.0", + "strip-bom": "^4.0.0" + }, + "dependencies": { + "@jest/schemas": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/@jest/schemas/-/schemas-29.6.3.tgz", + "integrity": "sha512-mo5j5X+jIZmJQveBKeS/clAueipV7KgiX1vMgCxam1RNYiqE1w62n0/tJJnHtjW8ZHcQco5gY85jA3mi0L+nSA==", + "dev": true, + "requires": { + "@sinclair/typebox": "^0.27.8" + } + }, + "@jest/types": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/@jest/types/-/types-29.6.3.tgz", + "integrity": "sha512-u3UPsIilWKOM3F9CXtrG8LEJmNxwoCQC/XVj4IKYXvvpx7QIi/Kg1LI5uDmDpKlac62NUtX7eLjRh+jVZcLOzw==", + "dev": true, + "requires": { + "@jest/schemas": "^29.6.3", + "@types/istanbul-lib-coverage": "^2.0.0", + "@types/istanbul-reports": "^3.0.0", + "@types/node": "*", + "@types/yargs": "^17.0.8", + "chalk": "^4.0.0" + } + }, + "@sinclair/typebox": { + "version": "0.27.8", + "resolved": "https://registry.npmjs.org/@sinclair/typebox/-/typebox-0.27.8.tgz", + "integrity": "sha512-+Fj43pSMwJs4KRrH/938Uf+uAELIgVBmQzg/q1YG10djyfA3TnrU8N8XzqCh/okZdszqBQTZf96idMfE5lnwTA==", + "dev": true + }, + "ansi-styles": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz", + "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==", + "dev": true + }, + "jest-message-util": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-message-util/-/jest-message-util-29.7.0.tgz", + "integrity": "sha512-GBEV4GRADeP+qtB2+6u61stea8mGcOT4mCtrYISZwfu9/ISHFJ/5zOMXYbpBE9RsS5+Gb63DW4FgmnKJ79Kf6w==", + "dev": true, + "requires": { + "@babel/code-frame": "^7.12.13", + "@jest/types": "^29.6.3", + "@types/stack-utils": "^2.0.0", + "chalk": "^4.0.0", + "graceful-fs": "^4.2.9", + "micromatch": "^4.0.4", + "pretty-format": "^29.7.0", + "slash": "^3.0.0", + "stack-utils": "^2.0.3" + } + }, + "jest-util": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-util/-/jest-util-29.7.0.tgz", + "integrity": "sha512-z6EbKajIpqGKU56y5KBUgy1dt1ihhQJgWzUlZHArA/+X2ad7Cb5iF+AK1EWVL/Bo7Rz9uurpqw6SiBCefUbCGA==", + "dev": true, + "requires": { + "@jest/types": "^29.6.3", + "@types/node": "*", + "chalk": "^4.0.0", + "ci-info": "^3.2.0", + "graceful-fs": "^4.2.9", + "picomatch": "^2.2.3" + } + }, + "pretty-format": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-29.7.0.tgz", + "integrity": "sha512-Pdlw/oPxN+aXdmM9R00JVC9WVFoCLTKJvDVLgmJ+qAffBMxsV85l/Lu7sNx4zSzPyoL2euImuEwHhOXdEgNFZQ==", + "dev": true, + "requires": { + "@jest/schemas": "^29.6.3", + "ansi-styles": "^5.0.0", + "react-is": "^18.0.0" + } + }, + "react-is": { + "version": "18.2.0", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-18.2.0.tgz", + "integrity": "sha512-xWGDIW6x921xtzPkhiULtthJHoJvBbF3q26fzloPCK0hsvxtPVelvftw3zjbHWSkR2km9Z+4uxbDDK/6Zw9B8w==", + "dev": true + }, + "strip-bom": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-4.0.0.tgz", + "integrity": "sha512-3xurFv5tEgii33Zi8Jtp55wEIILR9eh34FAW00PZf+JnSsTmV/ioewSgQl97JHvgjoRGwPShsWm+IdrxB35d0w==", + "dev": true + } + } + }, + "jest-snapshot": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-snapshot/-/jest-snapshot-29.7.0.tgz", + "integrity": "sha512-Rm0BMWtxBcioHr1/OX5YCP8Uov4riHvKPknOGs804Zg9JGZgmIBkbtlxJC/7Z4msKYVbIJtfU+tKb8xlYNfdkw==", + "dev": true, + "requires": { + "@babel/core": "^7.11.6", + "@babel/generator": "^7.7.2", + "@babel/plugin-syntax-jsx": "^7.7.2", + "@babel/plugin-syntax-typescript": "^7.7.2", + "@babel/types": "^7.3.3", + "@jest/expect-utils": "^29.7.0", + "@jest/transform": "^29.7.0", + "@jest/types": "^29.6.3", + "babel-preset-current-node-syntax": "^1.0.0", + "chalk": "^4.0.0", + "expect": "^29.7.0", + "graceful-fs": "^4.2.9", + "jest-diff": "^29.7.0", + "jest-get-type": "^29.6.3", + "jest-matcher-utils": "^29.7.0", + "jest-message-util": "^29.7.0", + "jest-util": "^29.7.0", + "natural-compare": "^1.4.0", + "pretty-format": "^29.7.0", + "semver": "^7.5.3" + }, + "dependencies": { + "@jest/expect-utils": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/expect-utils/-/expect-utils-29.7.0.tgz", + "integrity": "sha512-GlsNBWiFQFCVi9QVSx7f5AgMeLxe9YCCs5PuP2O2LdjDAA8Jh9eX7lA1Jq/xdXw3Wb3hyvlFNfZIfcRetSzYcA==", + "dev": true, + "requires": { + "jest-get-type": "^29.6.3" + } + }, + "@jest/schemas": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/@jest/schemas/-/schemas-29.6.3.tgz", + "integrity": "sha512-mo5j5X+jIZmJQveBKeS/clAueipV7KgiX1vMgCxam1RNYiqE1w62n0/tJJnHtjW8ZHcQco5gY85jA3mi0L+nSA==", + "dev": true, + "requires": { + "@sinclair/typebox": "^0.27.8" + } + }, + "@jest/types": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/@jest/types/-/types-29.6.3.tgz", + "integrity": "sha512-u3UPsIilWKOM3F9CXtrG8LEJmNxwoCQC/XVj4IKYXvvpx7QIi/Kg1LI5uDmDpKlac62NUtX7eLjRh+jVZcLOzw==", + "dev": true, + "requires": { + "@jest/schemas": "^29.6.3", + "@types/istanbul-lib-coverage": "^2.0.0", + "@types/istanbul-reports": "^3.0.0", + "@types/node": "*", + "@types/yargs": "^17.0.8", + "chalk": "^4.0.0" + } + }, + "@sinclair/typebox": { + "version": "0.27.8", + "resolved": "https://registry.npmjs.org/@sinclair/typebox/-/typebox-0.27.8.tgz", + "integrity": "sha512-+Fj43pSMwJs4KRrH/938Uf+uAELIgVBmQzg/q1YG10djyfA3TnrU8N8XzqCh/okZdszqBQTZf96idMfE5lnwTA==", + "dev": true + }, + "ansi-styles": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz", + "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==", + "dev": true + }, + "diff-sequences": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/diff-sequences/-/diff-sequences-29.6.3.tgz", + "integrity": "sha512-EjePK1srD3P08o2j4f0ExnylqRs5B9tJjcp9t1krH2qRi8CCdsYfwe9JgSLurFBWwq4uOlipzfk5fHNvwFKr8Q==", + "dev": true + }, + "expect": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/expect/-/expect-29.7.0.tgz", + "integrity": "sha512-2Zks0hf1VLFYI1kbh0I5jP3KHHyCHpkfyHBzsSXRFgl/Bg9mWYfMW8oD+PdMPlEwy5HNsR9JutYy6pMeOh61nw==", + "dev": true, + "requires": { + "@jest/expect-utils": "^29.7.0", + "jest-get-type": "^29.6.3", + "jest-matcher-utils": "^29.7.0", + "jest-message-util": "^29.7.0", + "jest-util": "^29.7.0" + } + }, + "jest-diff": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-diff/-/jest-diff-29.7.0.tgz", + "integrity": "sha512-LMIgiIrhigmPrs03JHpxUh2yISK3vLFPkAodPeo0+BuF7wA2FoQbkEg1u8gBYBThncu7e1oEDUfIXVuTqLRUjw==", + "dev": true, + "requires": { + "chalk": "^4.0.0", + "diff-sequences": "^29.6.3", + "jest-get-type": "^29.6.3", + "pretty-format": "^29.7.0" + } + }, + "jest-get-type": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/jest-get-type/-/jest-get-type-29.6.3.tgz", + "integrity": "sha512-zrteXnqYxfQh7l5FHyL38jL39di8H8rHoecLH3JNxH3BwOrBsNeabdap5e0I23lD4HHI8W5VFBZqG4Eaq5LNcw==", + "dev": true + }, + "jest-matcher-utils": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-matcher-utils/-/jest-matcher-utils-29.7.0.tgz", + "integrity": "sha512-sBkD+Xi9DtcChsI3L3u0+N0opgPYnCRPtGcQYrgXmR+hmt/fYfWAL0xRXYU8eWOdfuLgBe0YCW3AFtnRLagq/g==", + "dev": true, + "requires": { + "chalk": "^4.0.0", + "jest-diff": "^29.7.0", + "jest-get-type": "^29.6.3", + "pretty-format": "^29.7.0" + } + }, + "jest-message-util": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-message-util/-/jest-message-util-29.7.0.tgz", + "integrity": "sha512-GBEV4GRADeP+qtB2+6u61stea8mGcOT4mCtrYISZwfu9/ISHFJ/5zOMXYbpBE9RsS5+Gb63DW4FgmnKJ79Kf6w==", + "dev": true, + "requires": { + "@babel/code-frame": "^7.12.13", + "@jest/types": "^29.6.3", + "@types/stack-utils": "^2.0.0", + "chalk": "^4.0.0", + "graceful-fs": "^4.2.9", + "micromatch": "^4.0.4", + "pretty-format": "^29.7.0", + "slash": "^3.0.0", + "stack-utils": "^2.0.3" + } + }, + "jest-util": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-util/-/jest-util-29.7.0.tgz", + "integrity": "sha512-z6EbKajIpqGKU56y5KBUgy1dt1ihhQJgWzUlZHArA/+X2ad7Cb5iF+AK1EWVL/Bo7Rz9uurpqw6SiBCefUbCGA==", + "dev": true, + "requires": { + "@jest/types": "^29.6.3", + "@types/node": "*", + "chalk": "^4.0.0", + "ci-info": "^3.2.0", + "graceful-fs": "^4.2.9", + "picomatch": "^2.2.3" + } + }, + "pretty-format": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-29.7.0.tgz", + "integrity": "sha512-Pdlw/oPxN+aXdmM9R00JVC9WVFoCLTKJvDVLgmJ+qAffBMxsV85l/Lu7sNx4zSzPyoL2euImuEwHhOXdEgNFZQ==", + "dev": true, + "requires": { + "@jest/schemas": "^29.6.3", + "ansi-styles": "^5.0.0", + "react-is": "^18.0.0" + } + }, + "react-is": { + "version": "18.2.0", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-18.2.0.tgz", + "integrity": "sha512-xWGDIW6x921xtzPkhiULtthJHoJvBbF3q26fzloPCK0hsvxtPVelvftw3zjbHWSkR2km9Z+4uxbDDK/6Zw9B8w==", + "dev": true + }, + "semver": { + "version": "7.5.4", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.5.4.tgz", + "integrity": "sha512-1bCSESV6Pv+i21Hvpxp3Dx+pSD8lIPt8uVjRrxAUt/nbswYc+tK6Y2btiULjd4+fnq15PX+nqQDC7Oft7WkwcA==", + "dev": true, + "requires": { + "lru-cache": "^6.0.0" + } + } + } + }, + "jest-util": { + "version": "28.1.3", + "resolved": "https://registry.npmjs.org/jest-util/-/jest-util-28.1.3.tgz", + "integrity": "sha512-XdqfpHwpcSRko/C35uLYFM2emRAltIIKZiJ9eAmhjsj0CqZMa0p1ib0R5fWIqGhn1a103DebTbpqIaP1qCQ6tQ==", + "dev": true, + "requires": { + "@jest/types": "^28.1.3", + "@types/node": "*", + "chalk": "^4.0.0", + "ci-info": "^3.2.0", + "graceful-fs": "^4.2.9", + "picomatch": "^2.2.3" + } + }, + "jest-validate": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-validate/-/jest-validate-29.7.0.tgz", + "integrity": "sha512-ZB7wHqaRGVw/9hST/OuFUReG7M8vKeq0/J2egIGLdvjHCmYqGARhzXmtgi+gVeZ5uXFF219aOc3Ls2yLg27tkw==", + "dev": true, + "requires": { + "@jest/types": "^29.6.3", + "camelcase": "^6.2.0", + "chalk": "^4.0.0", + "jest-get-type": "^29.6.3", + "leven": "^3.1.0", + "pretty-format": "^29.7.0" + }, + "dependencies": { + "@jest/schemas": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/@jest/schemas/-/schemas-29.6.3.tgz", + "integrity": "sha512-mo5j5X+jIZmJQveBKeS/clAueipV7KgiX1vMgCxam1RNYiqE1w62n0/tJJnHtjW8ZHcQco5gY85jA3mi0L+nSA==", + "dev": true, + "requires": { + "@sinclair/typebox": "^0.27.8" + } + }, + "@jest/types": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/@jest/types/-/types-29.6.3.tgz", + "integrity": "sha512-u3UPsIilWKOM3F9CXtrG8LEJmNxwoCQC/XVj4IKYXvvpx7QIi/Kg1LI5uDmDpKlac62NUtX7eLjRh+jVZcLOzw==", + "dev": true, + "requires": { + "@jest/schemas": "^29.6.3", + "@types/istanbul-lib-coverage": "^2.0.0", + "@types/istanbul-reports": "^3.0.0", + "@types/node": "*", + "@types/yargs": "^17.0.8", + "chalk": "^4.0.0" + } + }, + "@sinclair/typebox": { + "version": "0.27.8", + "resolved": "https://registry.npmjs.org/@sinclair/typebox/-/typebox-0.27.8.tgz", + "integrity": "sha512-+Fj43pSMwJs4KRrH/938Uf+uAELIgVBmQzg/q1YG10djyfA3TnrU8N8XzqCh/okZdszqBQTZf96idMfE5lnwTA==", + "dev": true + }, + "ansi-styles": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz", + "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==", + "dev": true + }, + "camelcase": { + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-6.3.0.tgz", + "integrity": "sha512-Gmy6FhYlCY7uOElZUSbxo2UCDH8owEk996gkbrpsgGtrJLM3J7jGxl9Ic7Qwwj4ivOE5AWZWRMecDdF7hqGjFA==", + "dev": true + }, + "jest-get-type": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/jest-get-type/-/jest-get-type-29.6.3.tgz", + "integrity": "sha512-zrteXnqYxfQh7l5FHyL38jL39di8H8rHoecLH3JNxH3BwOrBsNeabdap5e0I23lD4HHI8W5VFBZqG4Eaq5LNcw==", + "dev": true + }, + "pretty-format": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-29.7.0.tgz", + "integrity": "sha512-Pdlw/oPxN+aXdmM9R00JVC9WVFoCLTKJvDVLgmJ+qAffBMxsV85l/Lu7sNx4zSzPyoL2euImuEwHhOXdEgNFZQ==", + "dev": true, + "requires": { + "@jest/schemas": "^29.6.3", + "ansi-styles": "^5.0.0", + "react-is": "^18.0.0" + } + }, + "react-is": { + "version": "18.2.0", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-18.2.0.tgz", + "integrity": "sha512-xWGDIW6x921xtzPkhiULtthJHoJvBbF3q26fzloPCK0hsvxtPVelvftw3zjbHWSkR2km9Z+4uxbDDK/6Zw9B8w==", + "dev": true + } + } + }, + "jest-watcher": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-watcher/-/jest-watcher-29.7.0.tgz", + "integrity": "sha512-49Fg7WXkU3Vl2h6LbLtMQ/HyB6rXSIX7SqvBLQmssRBGN9I0PNvPmAmCWSOY6SOvrjhI/F7/bGAv9RtnsPA03g==", + "dev": true, + "requires": { + "@jest/test-result": "^29.7.0", + "@jest/types": "^29.6.3", + "@types/node": "*", + "ansi-escapes": "^4.2.1", + "chalk": "^4.0.0", + "emittery": "^0.13.1", + "jest-util": "^29.7.0", + "string-length": "^4.0.1" + }, + "dependencies": { + "@jest/schemas": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/@jest/schemas/-/schemas-29.6.3.tgz", + "integrity": "sha512-mo5j5X+jIZmJQveBKeS/clAueipV7KgiX1vMgCxam1RNYiqE1w62n0/tJJnHtjW8ZHcQco5gY85jA3mi0L+nSA==", + "dev": true, + "requires": { + "@sinclair/typebox": "^0.27.8" + } + }, + "@jest/types": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/@jest/types/-/types-29.6.3.tgz", + "integrity": "sha512-u3UPsIilWKOM3F9CXtrG8LEJmNxwoCQC/XVj4IKYXvvpx7QIi/Kg1LI5uDmDpKlac62NUtX7eLjRh+jVZcLOzw==", + "dev": true, + "requires": { + "@jest/schemas": "^29.6.3", + "@types/istanbul-lib-coverage": "^2.0.0", + "@types/istanbul-reports": "^3.0.0", + "@types/node": "*", + "@types/yargs": "^17.0.8", + "chalk": "^4.0.0" + } + }, + "@sinclair/typebox": { + "version": "0.27.8", + "resolved": "https://registry.npmjs.org/@sinclair/typebox/-/typebox-0.27.8.tgz", + "integrity": "sha512-+Fj43pSMwJs4KRrH/938Uf+uAELIgVBmQzg/q1YG10djyfA3TnrU8N8XzqCh/okZdszqBQTZf96idMfE5lnwTA==", + "dev": true + }, + "jest-util": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-util/-/jest-util-29.7.0.tgz", + "integrity": "sha512-z6EbKajIpqGKU56y5KBUgy1dt1ihhQJgWzUlZHArA/+X2ad7Cb5iF+AK1EWVL/Bo7Rz9uurpqw6SiBCefUbCGA==", + "dev": true, + "requires": { + "@jest/types": "^29.6.3", + "@types/node": "*", + "chalk": "^4.0.0", + "ci-info": "^3.2.0", + "graceful-fs": "^4.2.9", + "picomatch": "^2.2.3" + } + } + } + }, + "jest-worker": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-worker/-/jest-worker-29.7.0.tgz", + "integrity": "sha512-eIz2msL/EzL9UFTFFx7jBTkeZfku0yUAyZZZmJ93H2TYEiroIx2PQjEXcwYtYl8zXCxb+PAmA2hLIt/6ZEkPHw==", + "dev": true, + "requires": { + "@types/node": "*", + "jest-util": "^29.7.0", + "merge-stream": "^2.0.0", + "supports-color": "^8.0.0" + }, + "dependencies": { + "@jest/schemas": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/@jest/schemas/-/schemas-29.6.3.tgz", + "integrity": "sha512-mo5j5X+jIZmJQveBKeS/clAueipV7KgiX1vMgCxam1RNYiqE1w62n0/tJJnHtjW8ZHcQco5gY85jA3mi0L+nSA==", + "dev": true, + "requires": { + "@sinclair/typebox": "^0.27.8" + } + }, + "@jest/types": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/@jest/types/-/types-29.6.3.tgz", + "integrity": "sha512-u3UPsIilWKOM3F9CXtrG8LEJmNxwoCQC/XVj4IKYXvvpx7QIi/Kg1LI5uDmDpKlac62NUtX7eLjRh+jVZcLOzw==", + "dev": true, + "requires": { + "@jest/schemas": "^29.6.3", + "@types/istanbul-lib-coverage": "^2.0.0", + "@types/istanbul-reports": "^3.0.0", + "@types/node": "*", + "@types/yargs": "^17.0.8", + "chalk": "^4.0.0" + } + }, + "@sinclair/typebox": { + "version": "0.27.8", + "resolved": "https://registry.npmjs.org/@sinclair/typebox/-/typebox-0.27.8.tgz", + "integrity": "sha512-+Fj43pSMwJs4KRrH/938Uf+uAELIgVBmQzg/q1YG10djyfA3TnrU8N8XzqCh/okZdszqBQTZf96idMfE5lnwTA==", + "dev": true + }, + "has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true + }, + "jest-util": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-util/-/jest-util-29.7.0.tgz", + "integrity": "sha512-z6EbKajIpqGKU56y5KBUgy1dt1ihhQJgWzUlZHArA/+X2ad7Cb5iF+AK1EWVL/Bo7Rz9uurpqw6SiBCefUbCGA==", + "dev": true, + "requires": { + "@jest/types": "^29.6.3", + "@types/node": "*", + "chalk": "^4.0.0", + "ci-info": "^3.2.0", + "graceful-fs": "^4.2.9", + "picomatch": "^2.2.3" + } + }, + "supports-color": { + "version": "8.1.1", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-8.1.1.tgz", + "integrity": "sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==", + "dev": true, + "requires": { + "has-flag": "^4.0.0" + } + } + } + }, + "joi": { + "version": "17.11.0", + "resolved": "https://registry.npmjs.org/joi/-/joi-17.11.0.tgz", + "integrity": "sha512-NgB+lZLNoqISVy1rZocE9PZI36bL/77ie924Ri43yEvi9GUUMPeyVIr8KdFTMUlby1p0PBYMk9spIxEUQYqrJQ==", + "requires": { + "@hapi/hoek": "^9.0.0", + "@hapi/topo": "^5.0.0", + "@sideway/address": "^4.1.3", + "@sideway/formula": "^3.0.1", + "@sideway/pinpoint": "^2.0.0" + } + }, + "js-tokens": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", + "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", + "dev": true + }, + "js-yaml": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz", + "integrity": "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==", + "requires": { + "argparse": "^2.0.1" + } + }, + "jsesc": { + "version": "2.5.2", + "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-2.5.2.tgz", + "integrity": "sha512-OYu7XEzjkCQ3C5Ps3QIZsQfNpqoJyZZA99wd9aWd05NCtC5pWOkShK2mkL6HXQR6/Cy2lbNdPlZBpuQHXE63gA==", + "dev": true + }, + "json-parse-even-better-errors": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/json-parse-even-better-errors/-/json-parse-even-better-errors-2.3.1.tgz", + "integrity": "sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w==", + "dev": true + }, + "json-schema-traverse": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", + "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", + "dev": true + }, + "json-stable-stringify-without-jsonify": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz", + "integrity": "sha1-nbe1lJatPzz+8wp1FC0tkwrXJlE=", + "dev": true + }, + "json5": { + "version": "2.2.3", + "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz", + "integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==" + }, + "jsonwebtoken": { + "version": "9.0.2", + "resolved": "https://registry.npmjs.org/jsonwebtoken/-/jsonwebtoken-9.0.2.tgz", + "integrity": "sha512-PRp66vJ865SSqOlgqS8hujT5U4AOgMfhrwYIuIhfKaoSCZcirrmASQr8CX7cUg+RMih+hgznrjp99o+W4pJLHQ==", + "requires": { + "jws": "^3.2.2", + "lodash.includes": "^4.3.0", + "lodash.isboolean": "^3.0.3", + "lodash.isinteger": "^4.0.4", + "lodash.isnumber": "^3.0.3", + "lodash.isplainobject": "^4.0.6", + "lodash.isstring": "^4.0.1", + "lodash.once": "^4.0.0", + "ms": "^2.1.1", + "semver": "^7.5.4" + }, + "dependencies": { + "ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==" + }, + "semver": { + "version": "7.5.4", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.5.4.tgz", + "integrity": "sha512-1bCSESV6Pv+i21Hvpxp3Dx+pSD8lIPt8uVjRrxAUt/nbswYc+tK6Y2btiULjd4+fnq15PX+nqQDC7Oft7WkwcA==", + "requires": { + "lru-cache": "^6.0.0" + } + } + } + }, + "jsx-ast-utils": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/jsx-ast-utils/-/jsx-ast-utils-3.2.1.tgz", + "integrity": "sha512-uP5vu8xfy2F9A6LGC22KO7e2/vGTS1MhP+18f++ZNlf0Ohaxbc9nIEwHAsejlJKyzfZzU5UIhe5ItYkitcZnZA==", + "dev": true, + "requires": { + "array-includes": "^3.1.3", + "object.assign": "^4.1.2" + } + }, + "just-extend": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/just-extend/-/just-extend-4.2.1.tgz", + "integrity": "sha512-g3UB796vUFIY90VIv/WX3L2c8CS2MdWUww3CNrYmqza1Fg0DURc2K/O4YrnklBdQarSJ/y8JnJYDGc+1iumQjg==", + "dev": true + }, + "jwa": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/jwa/-/jwa-1.4.1.tgz", + "integrity": "sha512-qiLX/xhEEFKUAJ6FiBMbes3w9ATzyk5W7Hvzpa/SLYdxNtng+gcurvrI7TbACjIXlsJyr05/S1oUhZrc63evQA==", + "requires": { + "buffer-equal-constant-time": "1.0.1", + "ecdsa-sig-formatter": "1.0.11", + "safe-buffer": "^5.0.1" + } + }, + "jws": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/jws/-/jws-3.2.2.tgz", + "integrity": "sha512-YHlZCB6lMTllWDtSPHz/ZXTsi8S00usEV6v1tjq8tOUZzw7DpSDWVXjXDre6ed1w/pd495ODpHZYSdkRTsa0HA==", + "requires": { + "jwa": "^1.4.1", + "safe-buffer": "^5.0.1" + } + }, + "kleur": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/kleur/-/kleur-3.0.3.tgz", + "integrity": "sha512-eTIzlVOSUR+JxdDFepEYcBMtZ9Qqdef+rnzWdRZuMbOywu5tO2w2N7rqjoANZ5k9vywhL6Br1VRjUIgTQx4E8w==", + "dev": true + }, + "knex": { + "version": "2.5.1", + "resolved": "https://registry.npmjs.org/knex/-/knex-2.5.1.tgz", + "integrity": "sha512-z78DgGKUr4SE/6cm7ku+jHvFT0X97aERh/f0MUKAKgFnwCYBEW4TFBqtHWFYiJFid7fMrtpZ/gxJthvz5mEByA==", + "requires": { + "colorette": "2.0.19", + "commander": "^10.0.0", + "debug": "4.3.4", + "escalade": "^3.1.1", + "esm": "^3.2.25", + "get-package-type": "^0.1.0", + "getopts": "2.3.0", + "interpret": "^2.2.0", + "lodash": "^4.17.21", + "pg-connection-string": "2.6.1", + "rechoir": "^0.8.0", + "resolve-from": "^5.0.0", + "tarn": "^3.0.2", + "tildify": "2.0.0" + }, + "dependencies": { + "debug": { + "version": "4.3.4", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", + "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==", + "requires": { + "ms": "2.1.2" + } + }, + "ms": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==" + }, + "resolve-from": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-5.0.0.tgz", + "integrity": "sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw==" + } + } + }, + "kuler": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/kuler/-/kuler-2.0.0.tgz", + "integrity": "sha512-Xq9nH7KlWZmXAtodXDDRE7vs6DU1gTU8zYDHDiWLSip45Egwq3plLHzPn27NgvzL2r1LMPC1vdqh98sQxtqj4A==" + }, + "leven": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/leven/-/leven-3.1.0.tgz", + "integrity": "sha512-qsda+H8jTaUaN/x5vzW2rzc+8Rw4TAQ/4KjB46IwK5VH+IlVeeeje/EoZRpiXvIqjFgK84QffqPztGI3VBLG1A==", + "dev": true + }, + "levn": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/levn/-/levn-0.4.1.tgz", + "integrity": "sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==", + "dev": true, + "requires": { + "prelude-ls": "^1.2.1", + "type-check": "~0.4.0" + } + }, + "lines-and-columns": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-1.2.4.tgz", + "integrity": "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==", + "dev": true + }, + "locate-path": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-2.0.0.tgz", + "integrity": "sha1-K1aLJl7slExtnA3pw9u7ygNUzY4=", + "dev": true, + "requires": { + "p-locate": "^2.0.0", + "path-exists": "^3.0.0" + } + }, + "lodash": { + "version": "4.17.21", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz", + "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==" + }, + "lodash.get": { + "version": "4.4.2", + "resolved": "https://registry.npmjs.org/lodash.get/-/lodash.get-4.4.2.tgz", + "integrity": "sha512-z+Uw/vLuy6gQe8cfaFWD7p0wVv8fJl3mbzXh33RS+0oW2wvUqiRXiQ69gLWSLpgB5/6sU+r6BlQR0MBILadqTQ==", + "dev": true + }, + "lodash.includes": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/lodash.includes/-/lodash.includes-4.3.0.tgz", + "integrity": "sha512-W3Bx6mdkRTGtlJISOvVD/lbqjTlPPUDTMnlXZFnVwi9NKJ6tiAk6LVdlhZMm17VZisqhKcgzpO5Wz91PCt5b0w==" + }, + "lodash.isboolean": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/lodash.isboolean/-/lodash.isboolean-3.0.3.tgz", + "integrity": "sha512-Bz5mupy2SVbPHURB98VAcw+aHh4vRV5IPNhILUCsOzRmsTmSQ17jIuqopAentWoehktxGd9e/hbIXq980/1QJg==" + }, + "lodash.isinteger": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/lodash.isinteger/-/lodash.isinteger-4.0.4.tgz", + "integrity": "sha512-DBwtEWN2caHQ9/imiNeEA5ys1JoRtRfY3d7V9wkqtbycnAmTvRRmbHKDV4a0EYc678/dia0jrte4tjYwVBaZUA==" + }, + "lodash.isnumber": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/lodash.isnumber/-/lodash.isnumber-3.0.3.tgz", + "integrity": "sha512-QYqzpfwO3/CWf3XP+Z+tkQsfaLL/EnUlXWVkIk5FUPc4sBdTehEqZONuyRt2P67PXAk+NXmTBcc97zw9t1FQrw==" + }, + "lodash.isplainobject": { + "version": "4.0.6", + "resolved": "https://registry.npmjs.org/lodash.isplainobject/-/lodash.isplainobject-4.0.6.tgz", + "integrity": "sha512-oSXzaWypCMHkPC3NvBEaPHf0KsA5mvPrOPgQWDsbg8n7orZ290M0BmC/jgRZ4vcJ6DTAhjrsSYgdsW/F+MFOBA==" + }, + "lodash.isstring": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/lodash.isstring/-/lodash.isstring-4.0.1.tgz", + "integrity": "sha512-0wJxfxH1wgO3GrbuP+dTTk7op+6L41QCXbGINEmD+ny/G/eCqGzxyCsh7159S+mgDDcoarnBw6PC1PS5+wUGgw==" + }, + "lodash.merge": { + "version": "4.6.2", + "resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz", + "integrity": "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==", + "dev": true + }, + "lodash.once": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/lodash.once/-/lodash.once-4.1.1.tgz", + "integrity": "sha512-Sb487aTOCr9drQVL8pIxOzVhafOjZN9UU54hiN8PU3uAiSV7lx1yYNpbNmex2PK6dSJoNTSJUUswT651yww3Mg==" + }, + "logform": { + "version": "2.4.0", + "resolved": "https://registry.npmjs.org/logform/-/logform-2.4.0.tgz", + "integrity": "sha512-CPSJw4ftjf517EhXZGGvTHHkYobo7ZCc0kvwUoOYcjfR2UVrI66RHj8MCrfAdEitdmFqbu2BYdYs8FHHZSb6iw==", + "requires": { + "@colors/colors": "1.5.0", + "fecha": "^4.2.0", + "ms": "^2.1.1", + "safe-stable-stringify": "^2.3.1", + "triple-beam": "^1.3.0" + }, + "dependencies": { + "ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==" + } + } + }, + "loose-envify": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz", + "integrity": "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==", + "dev": true, + "requires": { + "js-tokens": "^3.0.0 || ^4.0.0" + } + }, + "lru-cache": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", + "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==", + "requires": { + "yallist": "^4.0.0" + } + }, + "make-dir": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-4.0.0.tgz", + "integrity": "sha512-hXdUTZYIVOt1Ex//jAQi+wTZZpUpwBj/0QsOzqegb3rGMMeJiSEu5xLHnYfBrRV4RH2+OCSOO95Is/7x1WJ4bw==", + "dev": true, + "requires": { + "semver": "^7.5.3" + }, + "dependencies": { + "semver": { + "version": "7.5.4", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.5.4.tgz", + "integrity": "sha512-1bCSESV6Pv+i21Hvpxp3Dx+pSD8lIPt8uVjRrxAUt/nbswYc+tK6Y2btiULjd4+fnq15PX+nqQDC7Oft7WkwcA==", + "dev": true, + "requires": { + "lru-cache": "^6.0.0" + } + } + } + }, + "makeerror": { + "version": "1.0.12", + "resolved": "https://registry.npmjs.org/makeerror/-/makeerror-1.0.12.tgz", + "integrity": "sha512-JmqCvUhmt43madlpFzG4BQzG2Z3m6tvQDNKdClZnO3VbIudJYmxsT0FNJMeiB2+JTSlTQTSbU8QdesVmwJcmLg==", + "dev": true, + "requires": { + "tmpl": "1.0.5" + } + }, + "media-typer": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz", + "integrity": "sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ==" + }, + "merge-descriptors": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.1.tgz", + "integrity": "sha1-sAqqVW3YtEVoFQ7J0blT8/kMu2E=" + }, + "merge-stream": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/merge-stream/-/merge-stream-2.0.0.tgz", + "integrity": "sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w==", + "dev": true + }, + "merge2": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz", + "integrity": "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==", + "dev": true + }, + "methods": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/methods/-/methods-1.1.2.tgz", + "integrity": "sha1-VSmk1nZUE07cxSZmVoNbD4Ua/O4=" + }, + "micromatch": { + "version": "4.0.5", + "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.5.tgz", + "integrity": "sha512-DMy+ERcEW2q8Z2Po+WNXuw3c5YaUSFjAO5GsJqfEl7UjvtIuFKO6ZrKvcItdy98dwFI2N1tg3zNIdKaQT+aNdA==", + "dev": true, + "requires": { + "braces": "^3.0.2", + "picomatch": "^2.3.1" + } + }, + "mime": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/mime/-/mime-1.6.0.tgz", + "integrity": "sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==" + }, + "mime-db": { + "version": "1.51.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.51.0.tgz", + "integrity": "sha512-5y8A56jg7XVQx2mbv1lu49NR4dokRnhZYTtL+KGfaa27uq4pSTXkwQkFJl4pkRMyNFz/EtYDSkiiEHx3F7UN6g==" + }, + "mime-types": { + "version": "2.1.34", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.34.tgz", + "integrity": "sha512-6cP692WwGIs9XXdOO4++N+7qjqv0rqxxVvJ3VHPh/Sc9mVZcQP+ZGhkKiTvWMQRr2tbHkJP/Yn7Y0npb3ZBs4A==", + "requires": { + "mime-db": "1.51.0" + } + }, + "mimic-fn": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-2.1.0.tgz", + "integrity": "sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg==", + "dev": true + }, + "minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "dev": true, + "requires": { + "brace-expansion": "^1.1.7" + } + }, + "minimist": { + "version": "1.2.6", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.6.tgz", + "integrity": "sha512-Jsjnk4bw3YJqYzbdyBiNsPWHPfO++UGG749Cxs6peCu5Xg4nrena6OVxOYxrQTqww0Jmwt+Ref8rggumkTLz9Q==", + "dev": true + }, + "ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=" + }, + "mute-stream": { + "version": "0.0.8", + "resolved": "https://registry.npmjs.org/mute-stream/-/mute-stream-0.0.8.tgz", + "integrity": "sha512-nnbWWOkoWyUsTjKrhgD0dcz22mdkSnpYqbEjIm2nhwhuxlSkpywJmBo8h0ZqJdkp73mb90SssHkN4rsRaBAfAA==", + "dev": true + }, + "natural-compare": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz", + "integrity": "sha1-Sr6/7tdUHywnrPspvbvRXI1bpPc=", + "dev": true + }, + "negotiator": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.3.tgz", + "integrity": "sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg==" + }, + "nice-try": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/nice-try/-/nice-try-1.0.5.tgz", + "integrity": "sha512-1nh45deeb5olNY7eX82BkPO7SSxR5SSYJiPTrTdFUVYwAl8CKMA5N9PjTYkHiRjisVcxcQ1HXdLhx2qxxJzLNQ==", + "dev": true + }, + "nise": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/nise/-/nise-5.1.2.tgz", + "integrity": "sha512-+gQjFi8v+tkfCuSCxfURHLhRhniE/+IaYbIphxAN2JRR9SHKhY8hgXpaXiYfHdw+gcGe4buxgbprBQFab9FkhA==", + "dev": true, + "requires": { + "@sinonjs/commons": "^2.0.0", + "@sinonjs/fake-timers": "^7.0.4", + "@sinonjs/text-encoding": "^0.7.1", + "just-extend": "^4.0.2", + "path-to-regexp": "^1.7.0" + }, + "dependencies": { + "@sinonjs/commons": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/@sinonjs/commons/-/commons-2.0.0.tgz", + "integrity": "sha512-uLa0j859mMrg2slwQYdO/AkrOfmH+X6LTVmNTS9CqexuE2IvVORIkSpJLqePAbEnKJ77aMmCwr1NUZ57120Xcg==", + "dev": true, + "requires": { + "type-detect": "4.0.8" + } + }, + "@sinonjs/fake-timers": { + "version": "7.1.2", + "resolved": "https://registry.npmjs.org/@sinonjs/fake-timers/-/fake-timers-7.1.2.tgz", + "integrity": "sha512-iQADsW4LBMISqZ6Ci1dupJL9pprqwcVFTcOsEmQOEhW+KLCVn/Y4Jrvg2k19fIHCp+iFprriYPTdRcQR8NbUPg==", + "dev": true, + "requires": { + "@sinonjs/commons": "^1.7.0" + }, + "dependencies": { + "@sinonjs/commons": { + "version": "1.8.6", + "resolved": "https://registry.npmjs.org/@sinonjs/commons/-/commons-1.8.6.tgz", + "integrity": "sha512-Ky+XkAkqPZSm3NLBeUng77EBQl3cmeJhITaGHdYH8kjVB+aun3S4XBRti2zt17mtt0mIUDiNxYeoJm6drVvBJQ==", + "dev": true, + "requires": { + "type-detect": "4.0.8" + } + } + } + }, + "path-to-regexp": { + "version": "1.8.0", + "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-1.8.0.tgz", + "integrity": "sha512-n43JRhlUKUAlibEJhPeir1ncUID16QnEjNpwzNdO3Lm4ywrBpBZ5oLD0I6br9evr1Y9JTqwRtAh7JLoOzAQdVA==", + "dev": true, + "requires": { + "isarray": "0.0.1" + } + } + } + }, + "node-int64": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/node-int64/-/node-int64-0.4.0.tgz", + "integrity": "sha512-O5lz91xSOeoXP6DulyHfllpq+Eg00MWitZIbtPfoSEvqIHdl5gfcY6hYzDWnj0qD5tz52PI08u9qUvSVeUBeHw==", + "dev": true + }, + "node-releases": { + "version": "2.0.13", + "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.13.tgz", + "integrity": "sha512-uYr7J37ae/ORWdZeQ1xxMJe3NtdmqMC/JZK+geofDrkLUApKRHPd18/TxtBOJ4A0/+uUIliorNrfYV6s1b02eQ==", + "dev": true + }, + "nodemon": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/nodemon/-/nodemon-3.0.1.tgz", + "integrity": "sha512-g9AZ7HmkhQkqXkRc20w+ZfQ73cHLbE8hnPbtaFbFtCumZsjyMhKk9LajQ07U5Ux28lvFjZ5X7HvWR1xzU8jHVw==", + "dev": true, + "requires": { + "chokidar": "^3.5.2", + "debug": "^3.2.7", + "ignore-by-default": "^1.0.1", + "minimatch": "^3.1.2", + "pstree.remy": "^1.1.8", + "semver": "^7.5.3", + "simple-update-notifier": "^2.0.0", + "supports-color": "^5.5.0", + "touch": "^3.1.0", + "undefsafe": "^2.0.5" + }, + "dependencies": { + "debug": { + "version": "3.2.7", + "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.7.tgz", + "integrity": "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==", + "dev": true, + "requires": { + "ms": "^2.1.1" + } + }, + "ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "dev": true + }, + "semver": { + "version": "7.5.4", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.5.4.tgz", + "integrity": "sha512-1bCSESV6Pv+i21Hvpxp3Dx+pSD8lIPt8uVjRrxAUt/nbswYc+tK6Y2btiULjd4+fnq15PX+nqQDC7Oft7WkwcA==", + "dev": true, + "requires": { + "lru-cache": "^6.0.0" + } + } + } + }, + "nopt": { + "version": "1.0.10", + "resolved": "https://registry.npmjs.org/nopt/-/nopt-1.0.10.tgz", + "integrity": "sha512-NWmpvLSqUrgrAC9HCuxEvb+PSloHpqVu+FqcO4eeF2h5qYRhA7ev6KvelyQAKtegUbC6RypJnlEOhd8vloNKYg==", + "dev": true, + "requires": { + "abbrev": "1" + } + }, + "normalize-path": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", + "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==", + "dev": true + }, + "npm-run-path": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-4.0.1.tgz", + "integrity": "sha512-S48WzZW777zhNIrn7gxOlISNAqi9ZC/uQFnRdbeIHhZhCA6UqpkOT8T1G7BvfdgP4Er8gF4sUbaS0i7QvIfCWw==", + "dev": true, + "requires": { + "path-key": "^3.0.0" + } + }, + "object-assign": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", + "integrity": "sha1-IQmtx5ZYh8/AXLvUQsrIv7s2CGM=" + }, + "object-inspect": { + "version": "1.11.0", + "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.11.0.tgz", + "integrity": "sha512-jp7ikS6Sd3GxQfZJPyH3cjcbJF6GZPClgdV+EFygjFLQ5FmW/dRUnTd9PQ9k0JhoNDabWFbpF1yCdSWCC6gexg==" + }, + "object-keys": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/object-keys/-/object-keys-1.1.1.tgz", + "integrity": "sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA==", + "dev": true + }, + "object.assign": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/object.assign/-/object.assign-4.1.2.tgz", + "integrity": "sha512-ixT2L5THXsApyiUPYKmW+2EHpXXe5Ii3M+f4e+aJFAHao5amFRW6J0OO6c/LU8Be47utCx2GL89hxGB6XSmKuQ==", + "dev": true, + "requires": { + "call-bind": "^1.0.0", + "define-properties": "^1.1.3", + "has-symbols": "^1.0.1", + "object-keys": "^1.1.1" + } + }, + "object.entries": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/object.entries/-/object.entries-1.1.5.tgz", + "integrity": "sha512-TyxmjUoZggd4OrrU1W66FMDG6CuqJxsFvymeyXI51+vQLN67zYfZseptRge703kKQdo4uccgAKebXFcRCzk4+g==", + "dev": true, + "requires": { + "call-bind": "^1.0.2", + "define-properties": "^1.1.3", + "es-abstract": "^1.19.1" + } + }, + "object.fromentries": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/object.fromentries/-/object.fromentries-2.0.5.tgz", + "integrity": "sha512-CAyG5mWQRRiBU57Re4FKoTBjXfDoNwdFVH2Y1tS9PqCsfUTymAohOkEMSG3aRNKmv4lV3O7p1et7c187q6bynw==", + "dev": true, + "requires": { + "call-bind": "^1.0.2", + "define-properties": "^1.1.3", + "es-abstract": "^1.19.1" + } + }, + "object.hasown": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/object.hasown/-/object.hasown-1.1.0.tgz", + "integrity": "sha512-MhjYRfj3GBlhSkDHo6QmvgjRLXQ2zndabdf3nX0yTyZK9rPfxb6uRpAac8HXNLy1GpqWtZ81Qh4v3uOls2sRAg==", + "dev": true, + "requires": { + "define-properties": "^1.1.3", + "es-abstract": "^1.19.1" + } + }, + "object.values": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/object.values/-/object.values-1.1.5.tgz", + "integrity": "sha512-QUZRW0ilQ3PnPpbNtgdNV1PDbEqLIiSFB3l+EnGtBQ/8SUTLj1PZwtQHABZtLgwpJZTSZhuGLOGk57Drx2IvYg==", + "dev": true, + "requires": { + "call-bind": "^1.0.2", + "define-properties": "^1.1.3", + "es-abstract": "^1.19.1" + } + }, + "objection": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/objection/-/objection-3.1.2.tgz", + "integrity": "sha512-V8YwRWz+DFbB9JS/m7TBLhRPVAFK/VX7yV3ZDAMkfUG9qYHLRyG/K4ZS0acKtGPWtRdVrCuN4qM1VkH3PuJ5Lg==", + "requires": { + "ajv": "^8.6.2", + "ajv-formats": "^2.1.1", + "db-errors": "^0.2.3" + }, + "dependencies": { + "ajv": { + "version": "8.9.0", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.9.0.tgz", + "integrity": "sha512-qOKJyNj/h+OWx7s5DePL6Zu1KeM9jPZhwBqs+7DzP6bGOvqzVCSf0xueYmVuaC/oQ/VtS2zLMLHdQFbkka+XDQ==", + "requires": { + "fast-deep-equal": "^3.1.1", + "json-schema-traverse": "^1.0.0", + "require-from-string": "^2.0.2", + "uri-js": "^4.2.2" + } + }, + "json-schema-traverse": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", + "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==" + } + } + }, + "on-finished": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz", + "integrity": "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==", + "requires": { + "ee-first": "1.1.1" + } + }, + "on-headers": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/on-headers/-/on-headers-1.0.2.tgz", + "integrity": "sha512-pZAE+FJLoyITytdqK0U5s+FIpjN0JP3OzFi/u8Rx+EV5/W+JTWGXG8xFzevE7AjBfDqHv/8vL8qQsIhHnqRkrA==" + }, + "once": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", + "integrity": "sha1-WDsap3WWHUsROsF9nFC6753Xa9E=", + "dev": true, + "requires": { + "wrappy": "1" + } + }, + "one-time": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/one-time/-/one-time-1.0.0.tgz", + "integrity": "sha512-5DXOiRKwuSEcQ/l0kGCF6Q3jcADFv5tSmRaJck/OqkVFcOzutB134KRSfF0xDrL39MNnqxbHBbUUcjZIhTgb2g==", + "requires": { + "fn.name": "1.x.x" + } + }, + "onetime": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/onetime/-/onetime-5.1.2.tgz", + "integrity": "sha512-kbpaSSGJTWdAY5KPVeMOKXSrPtr8C8C7wodJbcsd51jRnmD+GZu8Y0VoU6Dm5Z4vWr0Ig/1NKuWRKf7j5aaYSg==", + "dev": true, + "requires": { + "mimic-fn": "^2.1.0" + } + }, + "open": { + "version": "9.1.0", + "resolved": "https://registry.npmjs.org/open/-/open-9.1.0.tgz", + "integrity": "sha512-OS+QTnw1/4vrf+9hh1jc1jnYjzSG4ttTBB8UxOwAnInG3Uo4ssetzC1ihqaIHjLJnA5GGlRl6QlZXOTQhRBUvg==", + "dev": true, + "requires": { + "default-browser": "^4.0.0", + "define-lazy-prop": "^3.0.0", + "is-inside-container": "^1.0.0", + "is-wsl": "^2.2.0" + } + }, + "optionator": { + "version": "0.9.3", + "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.3.tgz", + "integrity": "sha512-JjCoypp+jKn1ttEFExxhetCKeJt9zhAgAve5FXHixTvFDW/5aEktX9bufBKLRRMdU7bNtpLfcGu94B3cdEJgjg==", + "dev": true, + "requires": { + "@aashutoshrathi/word-wrap": "^1.2.3", + "deep-is": "^0.1.3", + "fast-levenshtein": "^2.0.6", + "levn": "^0.4.1", + "prelude-ls": "^1.2.1", + "type-check": "^0.4.0" + } + }, + "os-tmpdir": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/os-tmpdir/-/os-tmpdir-1.0.2.tgz", + "integrity": "sha1-u+Z0BseaqFxc/sdm/lc0VV36EnQ=", + "dev": true + }, + "p-limit": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-1.3.0.tgz", + "integrity": "sha512-vvcXsLAJ9Dr5rQOPk7toZQZJApBl2K4J6dANSsEuh6QI41JYcsS/qhTGa9ErIUUgK3WNQoJYvylxvjqmiqEA9Q==", + "dev": true, + "requires": { + "p-try": "^1.0.0" + } + }, + "p-locate": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-2.0.0.tgz", + "integrity": "sha1-IKAQOyIqcMj9OcwuWAaA893l7EM=", + "dev": true, + "requires": { + "p-limit": "^1.1.0" + } + }, + "p-try": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/p-try/-/p-try-1.0.0.tgz", + "integrity": "sha1-y8ec26+P1CKOE/Yh8rGiN8GyB7M=", + "dev": true + }, + "packet-reader": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/packet-reader/-/packet-reader-1.0.0.tgz", + "integrity": "sha512-HAKu/fG3HpHFO0AA8WE8q2g+gBJaZ9MG7fcKk+IJPLTGAD6Psw4443l+9DGRbOIh3/aXr7Phy0TjilYivJo5XQ==" + }, + "parent-module": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz", + "integrity": "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==", + "dev": true, + "requires": { + "callsites": "^3.0.0" + } + }, + "parse-json": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-5.2.0.tgz", + "integrity": "sha512-ayCKvm/phCGxOkYRSCM82iDwct8/EonSEgCSxWxD7ve6jHggsFl4fZVQBPRNgQoKiuV/odhFrGzQXZwbifC8Rg==", + "dev": true, + "requires": { + "@babel/code-frame": "^7.0.0", + "error-ex": "^1.3.1", + "json-parse-even-better-errors": "^2.3.0", + "lines-and-columns": "^1.1.6" + } + }, + "parseurl": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz", + "integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==" + }, + "path-exists": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-3.0.0.tgz", + "integrity": "sha1-zg6+ql94yxiSXqfYENe1mwEP1RU=", + "dev": true + }, + "path-is-absolute": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", + "integrity": "sha1-F0uSaHNVNP+8es5r9TpanhtcX18=", + "dev": true + }, + "path-key": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", + "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", + "dev": true + }, + "path-parse": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz", + "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==" + }, + "path-to-regexp": { + "version": "0.1.7", + "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.7.tgz", + "integrity": "sha1-32BBeABfUi8V60SQ5yR6G/qmf4w=" + }, + "pg": { + "version": "8.11.3", + "resolved": "https://registry.npmjs.org/pg/-/pg-8.11.3.tgz", + "integrity": "sha512-+9iuvG8QfaaUrrph+kpF24cXkH1YOOUeArRNYIxq1viYHZagBxrTno7cecY1Fa44tJeZvaoG+Djpkc3JwehN5g==", + "requires": { + "buffer-writer": "2.0.0", + "packet-reader": "1.0.0", + "pg-cloudflare": "^1.1.1", + "pg-connection-string": "^2.6.2", + "pg-pool": "^3.6.1", + "pg-protocol": "^1.6.0", + "pg-types": "^2.1.0", + "pgpass": "1.x" + }, + "dependencies": { + "pg-connection-string": { + "version": "2.6.2", + "resolved": "https://registry.npmjs.org/pg-connection-string/-/pg-connection-string-2.6.2.tgz", + "integrity": "sha512-ch6OwaeaPYcova4kKZ15sbJ2hKb/VP48ZD2gE7i1J+L4MspCtBMAx8nMgz7bksc7IojCIIWuEhHibSMFH8m8oA==" + } + } + }, + "pg-cloudflare": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/pg-cloudflare/-/pg-cloudflare-1.1.1.tgz", + "integrity": "sha512-xWPagP/4B6BgFO+EKz3JONXv3YDgvkbVrGw2mTo3D6tVDQRh1e7cqVGvyR3BE+eQgAvx1XhW/iEASj4/jCWl3Q==", + "optional": true + }, + "pg-connection-string": { + "version": "2.6.1", + "resolved": "https://registry.npmjs.org/pg-connection-string/-/pg-connection-string-2.6.1.tgz", + "integrity": "sha512-w6ZzNu6oMmIzEAYVw+RLK0+nqHPt8K3ZnknKi+g48Ak2pr3dtljJW3o+D/n2zzCG07Zoe9VOX3aiKpj+BN0pjg==" + }, + "pg-int8": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/pg-int8/-/pg-int8-1.0.1.tgz", + "integrity": "sha512-WCtabS6t3c8SkpDBUlb1kjOs7l66xsGdKpIPZsg4wR+B3+u9UAum2odSsF9tnvxg80h4ZxLWMy4pRjOsFIqQpw==" + }, + "pg-pool": { + "version": "3.6.1", + "resolved": "https://registry.npmjs.org/pg-pool/-/pg-pool-3.6.1.tgz", + "integrity": "sha512-jizsIzhkIitxCGfPRzJn1ZdcosIt3pz9Sh3V01fm1vZnbnCMgmGl5wvGGdNN2EL9Rmb0EcFoCkixH4Pu+sP9Og==" + }, + "pg-protocol": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/pg-protocol/-/pg-protocol-1.6.0.tgz", + "integrity": "sha512-M+PDm637OY5WM307051+bsDia5Xej6d9IR4GwJse1qA1DIhiKlksvrneZOYQq42OM+spubpcNYEo2FcKQrDk+Q==" + }, + "pg-types": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/pg-types/-/pg-types-2.2.0.tgz", + "integrity": "sha512-qTAAlrEsl8s4OiEQY69wDvcMIdQN6wdz5ojQiOy6YRMuynxenON0O5oCpJI6lshc6scgAY8qvJ2On/p+CXY0GA==", + "requires": { + "pg-int8": "1.0.1", + "postgres-array": "~2.0.0", + "postgres-bytea": "~1.0.0", + "postgres-date": "~1.0.4", + "postgres-interval": "^1.1.0" + } + }, + "pgpass": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/pgpass/-/pgpass-1.0.5.tgz", + "integrity": "sha512-FdW9r/jQZhSeohs1Z3sI1yxFQNFvMcnmfuj4WBMUTxOrAyLMaTcE1aAMBiTlbMNaXvBCQuVi0R7hd8udDSP7ug==", + "requires": { + "split2": "^4.1.0" + }, + "dependencies": { + "split2": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/split2/-/split2-4.1.0.tgz", + "integrity": "sha512-VBiJxFkxiXRlUIeyMQi8s4hgvKCSjtknJv/LVYbrgALPwf5zSKmEwV9Lst25AkvMDnvxODugjdl6KZgwKM1WYQ==" + } + } + }, + "picocolors": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.0.0.tgz", + "integrity": "sha512-1fygroTLlHu66zi26VoTDv8yRgm0Fccecssto+MhsZ0D/DGW2sm8E8AjW7NU5VVTRt5GxbeZ5qBuJr+HyLYkjQ==", + "dev": true + }, + "picomatch": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", + "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", + "dev": true + }, + "pirates": { + "version": "4.0.6", + "resolved": "https://registry.npmjs.org/pirates/-/pirates-4.0.6.tgz", + "integrity": "sha512-saLsH7WeYYPiD25LDuLRRY/i+6HaPYr6G1OUlN39otzkSTxKnubR9RTxS3/Kk50s1g2JTgFwWQDQyplC5/SHZg==", + "dev": true + }, + "pkg-dir": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/pkg-dir/-/pkg-dir-2.0.0.tgz", + "integrity": "sha1-9tXREJ4Z1j7fQo4L1X4Sd3YVM0s=", + "dev": true, + "requires": { + "find-up": "^2.1.0" + } + }, + "postgres-array": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/postgres-array/-/postgres-array-2.0.0.tgz", + "integrity": "sha512-VpZrUqU5A69eQyW2c5CA1jtLecCsN2U/bD6VilrFDWq5+5UIEVO7nazS3TEcHf1zuPYO/sqGvUvW62g86RXZuA==" + }, + "postgres-bytea": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/postgres-bytea/-/postgres-bytea-1.0.0.tgz", + "integrity": "sha1-AntTPAqokOJtFy1Hz5zOzFIazTU=" + }, + "postgres-date": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/postgres-date/-/postgres-date-1.0.7.tgz", + "integrity": "sha512-suDmjLVQg78nMK2UZ454hAG+OAW+HQPZ6n++TNDUX+L0+uUlLywnoxJKDou51Zm+zTCjrCl0Nq6J9C5hP9vK/Q==" + }, + "postgres-interval": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/postgres-interval/-/postgres-interval-1.2.0.tgz", + "integrity": "sha512-9ZhXKM/rw350N1ovuWHbGxnGh/SNJ4cnxHiM0rxE4VN41wsg8P8zWn9hv/buK00RP4WvlOyr/RBDiptyxVbkZQ==", + "requires": { + "xtend": "^4.0.0" + } + }, + "prelude-ls": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz", + "integrity": "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==", + "dev": true + }, + "prettier-linter-helpers": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/prettier-linter-helpers/-/prettier-linter-helpers-1.0.0.tgz", + "integrity": "sha512-GbK2cP9nraSSUF9N2XwUwqfzlAFlMNYYl+ShE/V+H8a9uNl/oUqB1w2EL54Jh0OlyRSd8RfWYJ3coVS4TROP2w==", + "dev": true, + "requires": { + "fast-diff": "^1.1.2" + } + }, + "pretty-format": { + "version": "28.1.3", + "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-28.1.3.tgz", + "integrity": "sha512-8gFb/To0OmxHR9+ZTb14Df2vNxdGCX8g1xWGUTqUw5TiZvcQf5sHKObd5UcPyLLyowNwDAMTF3XWOG1B6mxl1Q==", + "dev": true, + "requires": { + "@jest/schemas": "^28.1.3", + "ansi-regex": "^5.0.1", + "ansi-styles": "^5.0.0", + "react-is": "^18.0.0" + }, + "dependencies": { + "ansi-styles": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz", + "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==", + "dev": true + }, + "react-is": { + "version": "18.2.0", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-18.2.0.tgz", + "integrity": "sha512-xWGDIW6x921xtzPkhiULtthJHoJvBbF3q26fzloPCK0hsvxtPVelvftw3zjbHWSkR2km9Z+4uxbDDK/6Zw9B8w==", + "dev": true + } + } + }, + "progress": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/progress/-/progress-2.0.3.tgz", + "integrity": "sha512-7PiHtLll5LdnKIMw100I+8xJXR5gW2QwWYkT6iJva0bXitZKa/XMrSbdmg3r2Xnaidz9Qumd0VPaMrZlF9V9sA==", + "dev": true + }, + "prompts": { + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/prompts/-/prompts-2.4.2.tgz", + "integrity": "sha512-NxNv/kLguCA7p3jE8oL2aEBsrJWgAakBpgmgK6lpPWV+WuOmY6r2/zbAVnP+T8bQlA0nzHXSJSJW0Hq7ylaD2Q==", + "dev": true, + "requires": { + "kleur": "^3.0.3", + "sisteransi": "^1.0.5" + } + }, + "prop-types": { + "version": "15.7.2", + "resolved": "https://registry.npmjs.org/prop-types/-/prop-types-15.7.2.tgz", + "integrity": "sha512-8QQikdH7//R2vurIJSutZ1smHYTcLpRWEOlHnzcWHmBYrOGUysKwSsrC89BCiFj3CbrfJ/nXFdJepOVrY1GCHQ==", + "dev": true, + "requires": { + "loose-envify": "^1.4.0", + "object-assign": "^4.1.1", + "react-is": "^16.8.1" + } + }, + "proxy-addr": { + "version": "2.0.7", + "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz", + "integrity": "sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==", + "requires": { + "forwarded": "0.2.0", + "ipaddr.js": "1.9.1" + } + }, + "pstree.remy": { + "version": "1.1.8", + "resolved": "https://registry.npmjs.org/pstree.remy/-/pstree.remy-1.1.8.tgz", + "integrity": "sha512-77DZwxQmxKnu3aR542U+X8FypNzbfJ+C5XQDk3uWjWxn6151aIMGthWYRXTqT1E5oJvg+ljaa2OJi+VfvCOQ8w==", + "dev": true + }, + "punycode": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.1.1.tgz", + "integrity": "sha512-XRsRjdf+j5ml+y/6GKHPZbrF/8p2Yga0JPtdqTIY2Xe5ohJPD9saDJJLPvp9+NSBprVvevdXZybnj2cv8OEd0A==" + }, + "pure-rand": { + "version": "6.0.4", + "resolved": "https://registry.npmjs.org/pure-rand/-/pure-rand-6.0.4.tgz", + "integrity": "sha512-LA0Y9kxMYv47GIPJy6MI84fqTd2HmYZI83W/kM/SkKfDlajnZYfmXFTxkbY+xSBPkLJxltMa9hIkmdc29eguMA==", + "dev": true + }, + "qs": { + "version": "6.11.0", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.11.0.tgz", + "integrity": "sha512-MvjoMCJwEarSbUYk5O+nmoSzSutSsTwF85zcHPQ9OrlFoZOYIjaqBAJIqIXjptyD5vThxGq52Xu/MaJzRkIk4Q==", + "requires": { + "side-channel": "^1.0.4" + } + }, + "queue-microtask": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz", + "integrity": "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==", + "dev": true + }, + "range-parser": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz", + "integrity": "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==" + }, + "raw-body": { + "version": "2.5.1", + "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.5.1.tgz", + "integrity": "sha512-qqJBtEyVgS0ZmPGdCFPWJ3FreoqvG4MVQln/kCgF7Olq95IbOp0/BWyMwbdtn4VTvkM8Y7khCQ2Xgk/tcrCXig==", + "requires": { + "bytes": "3.1.2", + "http-errors": "2.0.0", + "iconv-lite": "0.4.24", + "unpipe": "1.0.0" + } + }, + "react-is": { + "version": "16.13.1", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz", + "integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==", + "dev": true + }, + "readable-stream": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.0.tgz", + "integrity": "sha512-BViHy7LKeTz4oNnkcLJ+lVSL6vpiFeX6/d3oSH8zCW7UxP2onchk+vTGB143xuFjHS3deTgkKoXXymXqymiIdA==", + "requires": { + "inherits": "^2.0.3", + "string_decoder": "^1.1.1", + "util-deprecate": "^1.0.1" + } + }, + "readdirp": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz", + "integrity": "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==", + "dev": true, + "requires": { + "picomatch": "^2.2.1" + } + }, + "rechoir": { + "version": "0.8.0", + "resolved": "https://registry.npmjs.org/rechoir/-/rechoir-0.8.0.tgz", + "integrity": "sha512-/vxpCXddiX8NGfGO/mTafwjq4aFa/71pvamip0++IQk3zG8cbCj0fifNPrjjF1XMXUne91jL9OoxmdykoEtifQ==", + "requires": { + "resolve": "^1.20.0" + } + }, + "regenerator-runtime": { + "version": "0.13.11", + "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.13.11.tgz", + "integrity": "sha512-kY1AZVr2Ra+t+piVaJ4gxaFaReZVH40AKNo7UCX6W+dEwBo/2oZJzqfuN1qLq1oL45o56cPaTXELwrTh8Fpggg==" + }, + "regexp.prototype.flags": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/regexp.prototype.flags/-/regexp.prototype.flags-1.3.1.tgz", + "integrity": "sha512-JiBdRBq91WlY7uRJ0ds7R+dU02i6LKi8r3BuQhNXn+kmeLN+EfHhfjqMRis1zJxnlu88hq/4dx0P2OP3APRTOA==", + "dev": true, + "requires": { + "call-bind": "^1.0.2", + "define-properties": "^1.1.3" + } + }, + "require-directory": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", + "integrity": "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==", + "dev": true + }, + "require-from-string": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/require-from-string/-/require-from-string-2.0.2.tgz", + "integrity": "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==" + }, + "resolve": { + "version": "1.20.0", + "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.20.0.tgz", + "integrity": "sha512-wENBPt4ySzg4ybFQW2TT1zMQucPK95HSh/nq2CFTZVOGut2+pQvSsgtda4d26YrYcr067wjbmzOG8byDPBX63A==", + "requires": { + "is-core-module": "^2.2.0", + "path-parse": "^1.0.6" + } + }, + "resolve-cwd": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/resolve-cwd/-/resolve-cwd-3.0.0.tgz", + "integrity": "sha512-OrZaX2Mb+rJCpH/6CpSqt9xFVpN++x01XnN2ie9g6P5/3xelLAkXWVADpdz1IHD/KFfEXyE6V0U01OQ3UO2rEg==", + "dev": true, + "requires": { + "resolve-from": "^5.0.0" + }, + "dependencies": { + "resolve-from": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-5.0.0.tgz", + "integrity": "sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw==", + "dev": true + } + } + }, + "resolve-from": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz", + "integrity": "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==", + "dev": true + }, + "resolve.exports": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/resolve.exports/-/resolve.exports-2.0.2.tgz", + "integrity": "sha512-X2UW6Nw3n/aMgDVy+0rSqgHlv39WZAlZrXCdnbyEiKm17DSqHX4MmQMaST3FbeWR5FTuRcUwYAziZajji0Y7mg==", + "dev": true + }, + "restore-cursor": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/restore-cursor/-/restore-cursor-3.1.0.tgz", + "integrity": "sha512-l+sSefzHpj5qimhFSE5a8nufZYAM3sBSVMAPtYkmC+4EH2anSGaEMXSD0izRQbu9nfyQ9y5JrVmp7E8oZrUjvA==", + "dev": true, + "requires": { + "onetime": "^5.1.0", + "signal-exit": "^3.0.2" + } + }, + "reusify": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.0.4.tgz", + "integrity": "sha512-U9nH88a3fc/ekCF1l0/UP1IosiuIjyTh7hBvXVMHYgVcfGvt897Xguj2UOLDeI5BG2m7/uwyaLVT6fbtCwTyzw==", + "dev": true + }, + "rimraf": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz", + "integrity": "sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==", + "dev": true, + "requires": { + "glob": "^7.1.3" + } + }, + "run-applescript": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/run-applescript/-/run-applescript-5.0.0.tgz", + "integrity": "sha512-XcT5rBksx1QdIhlFOCtgZkB99ZEouFZ1E2Kc2LHqNW13U3/74YGdkQRmThTwxy4QIyookibDKYZOPqX//6BlAg==", + "dev": true, + "requires": { + "execa": "^5.0.0" + } + }, + "run-async": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/run-async/-/run-async-2.4.1.tgz", + "integrity": "sha512-tvVnVv01b8c1RrA6Ep7JkStj85Guv/YrMcwqYQnwjsAS2cTmmPGBBjAjpCW7RrSodNSoE2/qg9O4bceNvUuDgQ==", + "dev": true + }, + "run-parallel": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz", + "integrity": "sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==", + "dev": true, + "requires": { + "queue-microtask": "^1.2.2" + } + }, + "rxjs": { + "version": "6.6.0", + "resolved": "https://registry.npmjs.org/rxjs/-/rxjs-6.6.0.tgz", + "integrity": "sha512-3HMA8z/Oz61DUHe+SdOiQyzIf4tOx5oQHmMir7IZEu6TMqCLHT4LRcmNaUS0NwOz8VLvmmBduMsoaUvMaIiqzg==", + "dev": true, + "requires": { + "tslib": "^1.9.0" + } + }, + "safe-buffer": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", + "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==" + }, + "safe-stable-stringify": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/safe-stable-stringify/-/safe-stable-stringify-2.3.1.tgz", + "integrity": "sha512-kYBSfT+troD9cDA85VDnHZ1rpHC50O0g1e6WlGHVCz/g+JS+9WKLj+XwFYyR8UbrZN8ll9HUpDAAddY58MGisg==" + }, + "safer-buffer": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", + "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==" + }, + "semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "dev": true + }, + "send": { + "version": "0.18.0", + "resolved": "https://registry.npmjs.org/send/-/send-0.18.0.tgz", + "integrity": "sha512-qqWzuOjSFOuqPjFe4NOsMLafToQQwBSOEpS+FwEt3A2V3vKubTquT3vmLTQpFgMXp8AlFWFuP1qKaJZOtPpVXg==", + "requires": { + "debug": "2.6.9", + "depd": "2.0.0", + "destroy": "1.2.0", + "encodeurl": "~1.0.2", + "escape-html": "~1.0.3", + "etag": "~1.8.1", + "fresh": "0.5.2", + "http-errors": "2.0.0", + "mime": "1.6.0", + "ms": "2.1.3", + "on-finished": "2.4.1", + "range-parser": "~1.2.1", + "statuses": "2.0.1" + }, + "dependencies": { + "debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "requires": { + "ms": "2.0.0" + }, + "dependencies": { + "ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==" + } + } + }, + "ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==" + } + } + }, + "serve-static": { + "version": "1.15.0", + "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.15.0.tgz", + "integrity": "sha512-XGuRDNjXUijsUL0vl6nSD7cwURuzEgglbOaFuZM9g3kwDXOWVTck0jLzjPzGD+TazWbboZYu52/9/XPdUgne9g==", + "requires": { + "encodeurl": "~1.0.2", + "escape-html": "~1.0.3", + "parseurl": "~1.3.3", + "send": "0.18.0" + } + }, + "setprototypeof": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz", + "integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==" + }, + "shebang-command": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", + "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", + "dev": true, + "requires": { + "shebang-regex": "^3.0.0" + } + }, + "shebang-regex": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", + "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", + "dev": true + }, + "side-channel": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.0.4.tgz", + "integrity": "sha512-q5XPytqFEIKHkGdiMIrY10mvLRvnQh42/+GoBlFW3b2LXLE2xxJpZFdm94we0BaoV3RwJyGqg5wS7epxTv0Zvw==", + "requires": { + "call-bind": "^1.0.0", + "get-intrinsic": "^1.0.2", + "object-inspect": "^1.9.0" + } + }, + "signal-exit": { + "version": "3.0.7", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.7.tgz", + "integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==", + "dev": true + }, + "simple-swizzle": { + "version": "0.2.2", + "resolved": "https://registry.npmjs.org/simple-swizzle/-/simple-swizzle-0.2.2.tgz", + "integrity": "sha1-pNprY1/8zMoz9w0Xy5JZLeleVXo=", + "requires": { + "is-arrayish": "^0.3.1" + }, + "dependencies": { + "is-arrayish": { + "version": "0.3.2", + "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.3.2.tgz", + "integrity": "sha512-eVRqCvVlZbuw3GrM63ovNSNAeA1K16kaR/LRY/92w0zxQ5/1YzwblUX652i4Xs9RwAGjW9d9y6X88t8OaAJfWQ==" + } + } + }, + "simple-update-notifier": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/simple-update-notifier/-/simple-update-notifier-2.0.0.tgz", + "integrity": "sha512-a2B9Y0KlNXl9u/vsW6sTIu9vGEpfKu2wRV6l1H3XEas/0gUIzGzBoP/IouTcUQbm9JWZLH3COxyn03TYlFax6w==", + "dev": true, + "requires": { + "semver": "^7.5.3" + }, + "dependencies": { + "semver": { + "version": "7.5.4", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.5.4.tgz", + "integrity": "sha512-1bCSESV6Pv+i21Hvpxp3Dx+pSD8lIPt8uVjRrxAUt/nbswYc+tK6Y2btiULjd4+fnq15PX+nqQDC7Oft7WkwcA==", + "dev": true, + "requires": { + "lru-cache": "^6.0.0" + } + } + } + }, + "sinon": { + "version": "14.0.2", + "resolved": "https://registry.npmjs.org/sinon/-/sinon-14.0.2.tgz", + "integrity": "sha512-PDpV0ZI3ZCS3pEqx0vpNp6kzPhHrLx72wA0G+ZLaaJjLIYeE0n8INlgaohKuGy7hP0as5tbUd23QWu5U233t+w==", + "dev": true, + "requires": { + "@sinonjs/commons": "^2.0.0", + "@sinonjs/fake-timers": "^9.1.2", + "@sinonjs/samsam": "^7.0.1", + "diff": "^5.0.0", + "nise": "^5.1.2", + "supports-color": "^7.2.0" + }, + "dependencies": { + "@sinonjs/commons": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/@sinonjs/commons/-/commons-2.0.0.tgz", + "integrity": "sha512-uLa0j859mMrg2slwQYdO/AkrOfmH+X6LTVmNTS9CqexuE2IvVORIkSpJLqePAbEnKJ77aMmCwr1NUZ57120Xcg==", + "dev": true, + "requires": { + "type-detect": "4.0.8" + } + }, + "has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true + }, + "supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "requires": { + "has-flag": "^4.0.0" + } + } + } + }, + "sisteransi": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/sisteransi/-/sisteransi-1.0.5.tgz", + "integrity": "sha512-bLGGlR1QxBcynn2d5YmDX4MGjlZvy2MRBDRNHLJ8VI6l6+9FUiyTFNJ0IveOSP0bcXgVDPRcfGqA0pjaqUpfVg==", + "dev": true + }, + "slash": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/slash/-/slash-3.0.0.tgz", + "integrity": "sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==", + "dev": true + }, + "slice-ansi": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/slice-ansi/-/slice-ansi-2.1.0.tgz", + "integrity": "sha512-Qu+VC3EwYLldKa1fCxuuvULvSJOKEgk9pi8dZeCVK7TqBfUNTH4sFkk4joj8afVSfAYgJoSOetjx9QWOJ5mYoQ==", + "dev": true, + "requires": { + "ansi-styles": "^3.2.0", + "astral-regex": "^1.0.0", + "is-fullwidth-code-point": "^2.0.0" + }, + "dependencies": { + "is-fullwidth-code-point": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-2.0.0.tgz", + "integrity": "sha1-o7MKXE8ZkYMWeqq5O+764937ZU8=", + "dev": true + } + } + }, + "source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "dev": true + }, + "source-map-support": { + "version": "0.5.13", + "resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.13.tgz", + "integrity": "sha512-SHSKFHadjVA5oR4PPqhtAVdcBWwRYVd6g6cAXnIbRiIwc2EhPrTuKUBdSLvlEKyIP3GCf89fltvcZiP9MMFA1w==", + "dev": true, + "requires": { + "buffer-from": "^1.0.0", + "source-map": "^0.6.0" + } + }, + "sprintf-js": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.0.3.tgz", + "integrity": "sha1-BOaSb2YolTVPPdAVIDYzuFcpfiw=", + "dev": true + }, + "stack-trace": { + "version": "0.0.10", + "resolved": "https://registry.npmjs.org/stack-trace/-/stack-trace-0.0.10.tgz", + "integrity": "sha1-VHxws0fo0ytOEI6hoqFZ5f3eGcA=" + }, + "stack-utils": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/stack-utils/-/stack-utils-2.0.5.tgz", + "integrity": "sha512-xrQcmYhOsn/1kX+Vraq+7j4oE2j/6BFscZ0etmYg81xuM8Gq0022Pxb8+IqgOFUIaxHs0KaSb7T1+OegiNrNFA==", + "dev": true, + "requires": { + "escape-string-regexp": "^2.0.0" + }, + "dependencies": { + "escape-string-regexp": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-2.0.0.tgz", + "integrity": "sha512-UpzcLCXolUWcNu5HtVMHYdXJjArjsF9C0aNnquZYY4uW/Vu0miy5YoWvbV345HauVvcAUnpRuhMMcqTcGOY2+w==", + "dev": true + } + } + }, + "statuses": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.1.tgz", + "integrity": "sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ==" + }, + "stream-browserify": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/stream-browserify/-/stream-browserify-3.0.0.tgz", + "integrity": "sha512-H73RAHsVBapbim0tU2JwwOiXUj+fikfiaoYAKHF3VJfA0pe2BCzkhAHBlLG6REzE+2WNZcxOXjK7lkso+9euLA==", + "requires": { + "inherits": "~2.0.4", + "readable-stream": "^3.5.0" + } + }, + "string-length": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/string-length/-/string-length-4.0.2.tgz", + "integrity": "sha512-+l6rNN5fYHNhZZy41RXsYptCjA2Igmq4EG7kZAYFQI1E1VTXarr6ZPXBg6eq7Y6eK4FEhY6AJlyuFIb/v/S0VQ==", + "dev": true, + "requires": { + "char-regex": "^1.0.2", + "strip-ansi": "^6.0.0" + } + }, + "string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "dev": true, + "requires": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "dependencies": { + "emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "dev": true + } + } + }, + "string.prototype.matchall": { + "version": "4.0.6", + "resolved": "https://registry.npmjs.org/string.prototype.matchall/-/string.prototype.matchall-4.0.6.tgz", + "integrity": "sha512-6WgDX8HmQqvEd7J+G6VtAahhsQIssiZ8zl7zKh1VDMFyL3hRTJP4FTNA3RbIp2TOQ9AYNDcc7e3fH0Qbup+DBg==", + "dev": true, + "requires": { + "call-bind": "^1.0.2", + "define-properties": "^1.1.3", + "es-abstract": "^1.19.1", + "get-intrinsic": "^1.1.1", + "has-symbols": "^1.0.2", + "internal-slot": "^1.0.3", + "regexp.prototype.flags": "^1.3.1", + "side-channel": "^1.0.4" + } + }, + "string.prototype.trimend": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/string.prototype.trimend/-/string.prototype.trimend-1.0.4.tgz", + "integrity": "sha512-y9xCjw1P23Awk8EvTpcyL2NIr1j7wJ39f+k6lvRnSMz+mz9CGz9NYPelDk42kOz6+ql8xjfK8oYzy3jAP5QU5A==", + "dev": true, + "requires": { + "call-bind": "^1.0.2", + "define-properties": "^1.1.3" + } + }, + "string.prototype.trimstart": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/string.prototype.trimstart/-/string.prototype.trimstart-1.0.4.tgz", + "integrity": "sha512-jh6e984OBfvxS50tdY2nRZnoC5/mLFKOREQfw8t5yytkoUsJRNxvI/E39qu1sD0OtWI3OC0XgKSmcWwziwYuZw==", + "dev": true, + "requires": { + "call-bind": "^1.0.2", + "define-properties": "^1.1.3" + } + }, + "string_decoder": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", + "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", + "requires": { + "safe-buffer": "~5.1.0" + } + }, + "strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, + "requires": { + "ansi-regex": "^5.0.1" + } + }, + "strip-bom": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-3.0.0.tgz", + "integrity": "sha1-IzTBjpx1n3vdVv3vfprj1YjmjtM=", + "dev": true + }, + "strip-final-newline": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/strip-final-newline/-/strip-final-newline-2.0.0.tgz", + "integrity": "sha512-BrpvfNAE3dcvq7ll3xVumzjKjZQ5tI1sEUIKr3Uoks0XUl45St3FlatVqef9prk4jRDzhW6WZg+3bk93y6pLjA==", + "dev": true + }, + "strip-json-comments": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz", + "integrity": "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==", + "dev": true + }, + "strnum": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/strnum/-/strnum-1.0.5.tgz", + "integrity": "sha512-J8bbNyKKXl5qYcR36TIO8W3mVGVHrmmxsd5PAItGkmyzwJvybiw2IVq5nqd0i4LSNSkB/sx9VHllbfFdr9k1JA==" + }, + "superagent": { + "version": "8.0.6", + "resolved": "https://registry.npmjs.org/superagent/-/superagent-8.0.6.tgz", + "integrity": "sha512-HqSe6DSIh3hEn6cJvCkaM1BLi466f1LHi4yubR0tpewlMpk4RUFFy35bKz8SsPBwYfIIJy5eclp+3tCYAuX0bw==", + "dev": true, + "requires": { + "component-emitter": "^1.3.0", + "cookiejar": "^2.1.3", + "debug": "^4.3.4", + "fast-safe-stringify": "^2.1.1", + "form-data": "^4.0.0", + "formidable": "^2.1.1", + "methods": "^1.1.2", + "mime": "2.6.0", + "qs": "^6.11.0", + "semver": "^7.3.8" + }, + "dependencies": { + "debug": { + "version": "4.3.4", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", + "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==", + "dev": true, + "requires": { + "ms": "2.1.2" + } + }, + "mime": { + "version": "2.6.0", + "resolved": "https://registry.npmjs.org/mime/-/mime-2.6.0.tgz", + "integrity": "sha512-USPkMeET31rOMiarsBNIHZKLGgvKc/LrjofAnBlOttf5ajRvqiRA8QsenbcooctK6d6Ts6aqZXBA+XbkKthiQg==", + "dev": true + }, + "ms": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", + "dev": true + }, + "semver": { + "version": "7.5.4", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.5.4.tgz", + "integrity": "sha512-1bCSESV6Pv+i21Hvpxp3Dx+pSD8lIPt8uVjRrxAUt/nbswYc+tK6Y2btiULjd4+fnq15PX+nqQDC7Oft7WkwcA==", + "dev": true, + "requires": { + "lru-cache": "^6.0.0" + } + } + } + }, + "supertest": { + "version": "6.3.3", + "resolved": "https://registry.npmjs.org/supertest/-/supertest-6.3.3.tgz", + "integrity": "sha512-EMCG6G8gDu5qEqRQ3JjjPs6+FYT1a7Hv5ApHvtSghmOFJYtsU5S+pSb6Y2EUeCEY3CmEL3mmQ8YWlPOzQomabA==", + "dev": true, + "requires": { + "methods": "^1.1.2", + "superagent": "^8.0.5" + } + }, + "supports-color": { + "version": "5.5.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", + "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", + "requires": { + "has-flag": "^3.0.0" + } + }, + "synckit": { + "version": "0.8.5", + "resolved": "https://registry.npmjs.org/synckit/-/synckit-0.8.5.tgz", + "integrity": "sha512-L1dapNV6vu2s/4Sputv8xGsCdAVlb5nRDMFU/E27D44l5U6cw1g0dGd45uLc+OXjNMmF4ntiMdCimzcjFKQI8Q==", + "dev": true, + "requires": { + "@pkgr/utils": "^2.3.1", + "tslib": "^2.5.0" + }, + "dependencies": { + "tslib": { + "version": "2.6.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.6.1.tgz", + "integrity": "sha512-t0hLfiEKfMUoqhG+U1oid7Pva4bbDPHYfJNiB7BiIjRkj1pyC++4N3huJfqY6aRH6VTB0rvtzQwjM4K6qpfOig==", + "dev": true + } + } + }, + "table": { + "version": "5.4.6", + "resolved": "https://registry.npmjs.org/table/-/table-5.4.6.tgz", + "integrity": "sha512-wmEc8m4fjnob4gt5riFRtTu/6+4rSe12TpAELNSqHMfF3IqnA+CH37USM6/YR3qRZv7e56kAEAtd6nKZaxe0Ug==", + "dev": true, + "requires": { + "ajv": "^6.10.2", + "lodash": "^4.17.14", + "slice-ansi": "^2.1.0", + "string-width": "^3.0.0" + }, + "dependencies": { + "ansi-regex": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-4.1.1.tgz", + "integrity": "sha512-ILlv4k/3f6vfQ4OoP2AGvirOktlQ98ZEL1k9FaQjxa3L1abBgbuTDAdPOpvbGncC0BTVQrl+OM8xZGK6tWXt7g==", + "dev": true + }, + "is-fullwidth-code-point": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-2.0.0.tgz", + "integrity": "sha1-o7MKXE8ZkYMWeqq5O+764937ZU8=", + "dev": true + }, + "string-width": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-3.1.0.tgz", + "integrity": "sha512-vafcv6KjVZKSgz06oM/H6GDBrAtz8vdhQakGjFIvNrHA6y3HCF1CInLy+QLq8dTJPQ1b+KDUqDFctkdRW44e1w==", + "dev": true, + "requires": { + "emoji-regex": "^7.0.1", + "is-fullwidth-code-point": "^2.0.0", + "strip-ansi": "^5.1.0" + } + }, + "strip-ansi": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-5.2.0.tgz", + "integrity": "sha512-DuRs1gKbBqsMKIZlrffwlug8MHkcnpjs5VPmL1PAh+mA30U0DTotfDZ0d2UUsXpPmPmMMJ6W773MaA3J+lbiWA==", + "dev": true, + "requires": { + "ansi-regex": "^4.1.0" + } + } + } + }, + "tarn": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/tarn/-/tarn-3.0.2.tgz", + "integrity": "sha512-51LAVKUSZSVfI05vjPESNc5vwqqZpbXCsU+/+wxlOrUjk2SnFTt97v9ZgQrD4YmxYW1Px6w2KjaDitCfkvgxMQ==" + }, + "test-exclude": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/test-exclude/-/test-exclude-6.0.0.tgz", + "integrity": "sha512-cAGWPIyOHU6zlmg88jwm7VRyXnMN7iV68OGAbYDk/Mh/xC/pzVPlQtY6ngoIH/5/tciuhGfvESU8GrHrcxD56w==", + "dev": true, + "requires": { + "@istanbuljs/schema": "^0.1.2", + "glob": "^7.1.4", + "minimatch": "^3.0.4" + } + }, + "text-hex": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/text-hex/-/text-hex-1.0.0.tgz", + "integrity": "sha512-uuVGNWzgJ4yhRaNSiubPY7OjISw4sw4E5Uv0wbjp+OzcbmVU/rsT8ujgcXJhn9ypzsgr5vlzpPqP+MBBKcGvbg==" + }, + "text-table": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/text-table/-/text-table-0.2.0.tgz", + "integrity": "sha1-f17oI66AUgfACvLfSoTsP8+lcLQ=", + "dev": true + }, + "through": { + "version": "2.3.8", + "resolved": "https://registry.npmjs.org/through/-/through-2.3.8.tgz", + "integrity": "sha1-DdTJ/6q8NXlgsbckEV1+Doai4fU=", + "dev": true + }, + "tildify": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/tildify/-/tildify-2.0.0.tgz", + "integrity": "sha512-Cc+OraorugtXNfs50hU9KS369rFXCfgGLpfCfvlc+Ud5u6VWmUQsOAa9HbTvheQdYnrdJqqv1e5oIqXppMYnSw==" + }, + "titleize": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/titleize/-/titleize-3.0.0.tgz", + "integrity": "sha512-KxVu8EYHDPBdUYdKZdKtU2aj2XfEx9AfjXxE/Aj0vT06w2icA09Vus1rh6eSu1y01akYg6BjIK/hxyLJINoMLQ==", + "dev": true + }, + "tmp": { + "version": "0.0.33", + "resolved": "https://registry.npmjs.org/tmp/-/tmp-0.0.33.tgz", + "integrity": "sha512-jRCJlojKnZ3addtTOjdIqoRuPEKBvNXcGYqzO6zWZX8KfKEpnGY5jfggJQ3EjKuu8D4bJRr0y+cYJFmYbImXGw==", + "dev": true, + "requires": { + "os-tmpdir": "~1.0.2" + } + }, + "tmpl": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/tmpl/-/tmpl-1.0.5.tgz", + "integrity": "sha512-3f0uOEAQwIqGuWW2MVzYg8fV/QNnc/IpuJNG837rLuczAaLVHslWHZQj4IGiEl5Hs3kkbhwL9Ab7Hrsmuj+Smw==", + "dev": true + }, + "to-fast-properties": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/to-fast-properties/-/to-fast-properties-2.0.0.tgz", + "integrity": "sha1-3F5pjL0HkmW8c+A3doGk5Og/YW4=", + "dev": true + }, + "to-regex-range": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", + "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", + "dev": true, + "requires": { + "is-number": "^7.0.0" + } + }, + "toidentifier": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz", + "integrity": "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==" + }, + "touch": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/touch/-/touch-3.1.0.tgz", + "integrity": "sha512-WBx8Uy5TLtOSRtIq+M03/sKDrXCLHxwDcquSP2c43Le03/9serjQBIztjRz6FkJez9D/hleyAXTBGLwwZUw9lA==", + "dev": true, + "requires": { + "nopt": "~1.0.10" + } + }, + "triple-beam": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/triple-beam/-/triple-beam-1.3.0.tgz", + "integrity": "sha512-XrHUvV5HpdLmIj4uVMxHggLbFSZYIn7HEWsqePZcI50pco+MPqJ50wMGY794X7AOOhxOBAjbkqfAbEe/QMp2Lw==" + }, + "tsconfig-paths": { + "version": "3.12.0", + "resolved": "https://registry.npmjs.org/tsconfig-paths/-/tsconfig-paths-3.12.0.tgz", + "integrity": "sha512-e5adrnOYT6zqVnWqZu7i/BQ3BnhzvGbjEjejFXO20lKIKpwTaupkCPgEfv4GZK1IBciJUEhYs3J3p75FdaTFVg==", + "dev": true, + "requires": { + "@types/json5": "^0.0.29", + "json5": "^1.0.1", + "minimist": "^1.2.0", + "strip-bom": "^3.0.0" + }, + "dependencies": { + "json5": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/json5/-/json5-1.0.2.tgz", + "integrity": "sha512-g1MWMLBiz8FKi1e4w0UyVL3w+iJceWAFBAaBnnGKOpNa5f8TLktkbre1+s6oICydWAm+HRUGTmI+//xv2hvXYA==", + "dev": true, + "requires": { + "minimist": "^1.2.0" + } + } + } + }, + "tslib": { + "version": "1.13.0", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.13.0.tgz", + "integrity": "sha512-i/6DQjL8Xf3be4K/E6Wgpekn5Qasl1usyw++dAA35Ue5orEn65VIxOA+YvNNl9HV3qv70T7CNwjODHZrLwvd1Q==" + }, + "type-check": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz", + "integrity": "sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==", + "dev": true, + "requires": { + "prelude-ls": "^1.2.1" + } + }, + "type-detect": { + "version": "4.0.8", + "resolved": "https://registry.npmjs.org/type-detect/-/type-detect-4.0.8.tgz", + "integrity": "sha512-0fr/mIH1dlO+x7TlcMy+bIDqKPsw/70tVyeHW787goQjhmqaZe10uwLujubK9q9Lg6Fiho1KUKDYz0Z7k7g5/g==", + "dev": true + }, + "type-fest": { + "version": "0.8.1", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.8.1.tgz", + "integrity": "sha512-4dbzIzqvjtgiM5rw1k5rEHtBANKmdudhGyBEajN01fEyhaAIhsoKNy6y7+IN93IfpFtwY9iqi7kD+xwKhQsNJA==", + "dev": true + }, + "type-is": { + "version": "1.6.18", + "resolved": "https://registry.npmjs.org/type-is/-/type-is-1.6.18.tgz", + "integrity": "sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g==", + "requires": { + "media-typer": "0.3.0", + "mime-types": "~2.1.24" + } + }, + "unbox-primitive": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/unbox-primitive/-/unbox-primitive-1.0.1.tgz", + "integrity": "sha512-tZU/3NqK3dA5gpE1KtyiJUrEB0lxnGkMFHptJ7q6ewdZ8s12QrODwNbhIJStmJkd1QDXa1NRA8aF2A1zk/Ypyw==", + "dev": true, + "requires": { + "function-bind": "^1.1.1", + "has-bigints": "^1.0.1", + "has-symbols": "^1.0.2", + "which-boxed-primitive": "^1.0.2" + } + }, + "undefsafe": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/undefsafe/-/undefsafe-2.0.5.tgz", + "integrity": "sha512-WxONCrssBM8TSPRqN5EmsjVrsv4A8X12J4ArBiiayv3DyyG3ZlIg6yysuuSYdZsVz3TKcTg2fd//Ujd4CHV1iA==", + "dev": true + }, + "unpipe": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz", + "integrity": "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==" + }, + "untildify": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/untildify/-/untildify-4.0.0.tgz", + "integrity": "sha512-KK8xQ1mkzZeg9inewmFVDNkg3l5LUhoq9kN6iWYB/CC9YMG8HA+c1Q8HwDe6dEX7kErrEVNVBO3fWsVq5iDgtw==", + "dev": true + }, + "update-browserslist-db": { + "version": "1.0.13", + "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.0.13.tgz", + "integrity": "sha512-xebP81SNcPuNpPP3uzeW1NYXxI3rxyJzF3pD6sH4jE7o/IX+WtSpwnVU+qIsDPyk0d3hmFQ7mjqc6AtV604hbg==", + "dev": true, + "requires": { + "escalade": "^3.1.1", + "picocolors": "^1.0.0" + } + }, + "uri-js": { + "version": "4.2.2", + "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.2.2.tgz", + "integrity": "sha512-KY9Frmirql91X2Qgjry0Wd4Y+YTdrdZheS8TFwvkbLWf/G5KNJDCh6pKL5OZctEW4+0Baa5idK2ZQuELRwPznQ==", + "requires": { + "punycode": "^2.1.0" + } + }, + "util-deprecate": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", + "integrity": "sha1-RQ1Nyfpw3nMnYvvS1KKJgUGaDM8=" + }, + "utils-merge": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/utils-merge/-/utils-merge-1.0.1.tgz", + "integrity": "sha1-n5VxD1CiZ5R7LMwSR0HBAoQn5xM=" + }, + "uuid": { + "version": "8.3.2", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-8.3.2.tgz", + "integrity": "sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==" + }, + "v8-compile-cache": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/v8-compile-cache/-/v8-compile-cache-2.1.1.tgz", + "integrity": "sha512-8OQ9CL+VWyt3JStj7HX7/ciTL2V3Rl1Wf5OL+SNTm0yK1KvtReVulksyeRnCANHHuUxHlQig+JJDlUhBt1NQDQ==", + "dev": true + }, + "v8-to-istanbul": { + "version": "9.1.2", + "resolved": "https://registry.npmjs.org/v8-to-istanbul/-/v8-to-istanbul-9.1.2.tgz", + "integrity": "sha512-ZGBe7VAivuuoQXTeckpbYKTdtjXGcm3ZUHXC0PAk0CzFyuYvwi73a58iEKI3GkGD1c3EHc+EgfR1w5pgbfzJlQ==", + "dev": true, + "requires": { + "@jridgewell/trace-mapping": "^0.3.12", + "@types/istanbul-lib-coverage": "^2.0.1", + "convert-source-map": "^2.0.0" + } + }, + "vary": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz", + "integrity": "sha1-IpnwLG3tMNSllhsLn3RSShj2NPw=" + }, + "walker": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/walker/-/walker-1.0.8.tgz", + "integrity": "sha512-ts/8E8l5b7kY0vlWLewOkDXMmPdLcVV4GmOQLyxuSswIJsweeFZtAsMF7k1Nszz+TYBQrlYRmzOnr398y1JemQ==", + "dev": true, + "requires": { + "makeerror": "1.0.12" + } + }, + "which": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", + "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", + "dev": true, + "requires": { + "isexe": "^2.0.0" + } + }, + "which-boxed-primitive": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/which-boxed-primitive/-/which-boxed-primitive-1.0.2.tgz", + "integrity": "sha512-bwZdv0AKLpplFY2KZRX6TvyuN7ojjr7lwkg6ml0roIy9YeuSr7JS372qlNW18UQYzgYK9ziGcerWqZOmEn9VNg==", + "dev": true, + "requires": { + "is-bigint": "^1.0.1", + "is-boolean-object": "^1.1.0", + "is-number-object": "^1.0.4", + "is-string": "^1.0.5", + "is-symbol": "^1.0.3" + } + }, + "winston": { + "version": "3.10.0", + "resolved": "https://registry.npmjs.org/winston/-/winston-3.10.0.tgz", + "integrity": "sha512-nT6SIDaE9B7ZRO0u3UvdrimG0HkB7dSTAgInQnNR2SOPJ4bvq5q79+pXLftKmP52lJGW15+H5MCK0nM9D3KB/g==", + "requires": { + "@colors/colors": "1.5.0", + "@dabh/diagnostics": "^2.0.2", + "async": "^3.2.3", + "is-stream": "^2.0.0", + "logform": "^2.4.0", + "one-time": "^1.0.0", + "readable-stream": "^3.4.0", + "safe-stable-stringify": "^2.3.1", + "stack-trace": "0.0.x", + "triple-beam": "^1.3.0", + "winston-transport": "^4.5.0" + } + }, + "winston-transport": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/winston-transport/-/winston-transport-4.5.0.tgz", + "integrity": "sha512-YpZzcUzBedhlTAfJg6vJDlyEai/IFMIVcaEZZyl3UXIl4gmqRpU7AE89AHLkbzLUsv0NVmw7ts+iztqKxxPW1Q==", + "requires": { + "logform": "^2.3.2", + "readable-stream": "^3.6.0", + "triple-beam": "^1.3.0" + } + }, + "word-wrap": { + "version": "1.2.5", + "resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.5.tgz", + "integrity": "sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==", + "dev": true + }, + "wrap-ansi": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", + "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", + "dev": true, + "requires": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "dependencies": { + "ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "requires": { + "color-convert": "^2.0.1" + } + }, + "color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "requires": { + "color-name": "~1.1.4" + } + }, + "color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true + } + } + }, + "wrappy": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", + "integrity": "sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8=", + "dev": true + }, + "write": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/write/-/write-1.0.3.tgz", + "integrity": "sha512-/lg70HAjtkUgWPVZhZcm+T4hkL8Zbtp1nFNOn3lRrxnlv50SRBv7cR7RqR+GMsd3hUXy9hWBo4CHTbFTcOYwig==", + "dev": true, + "requires": { + "mkdirp": "^0.5.1" + }, + "dependencies": { + "mkdirp": { + "version": "0.5.5", + "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.5.tgz", + "integrity": "sha512-NKmAlESf6jMGym1++R0Ra7wvhV+wFW63FaSOFPwRahvea0gMUcGUhVeAg/0BC0wiv9ih5NYPB1Wn1UEI1/L+xQ==", + "dev": true, + "requires": { + "minimist": "^1.2.5" + } + } + } + }, + "write-file-atomic": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/write-file-atomic/-/write-file-atomic-4.0.2.tgz", + "integrity": "sha512-7KxauUdBmSdWnmpaGFg+ppNjKF8uNLry8LyzjauQDOVONfFLNKrKvQOxZ/VuTIcS/gge/YNahf5RIIQWTSarlg==", + "dev": true, + "requires": { + "imurmurhash": "^0.1.4", + "signal-exit": "^3.0.7" + } + }, + "xtend": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.2.tgz", + "integrity": "sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ==" + }, + "y18n": { + "version": "5.0.8", + "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz", + "integrity": "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==", + "dev": true + }, + "yallist": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", + "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==" + }, + "yargs": { + "version": "17.7.2", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-17.7.2.tgz", + "integrity": "sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w==", + "dev": true, + "requires": { + "cliui": "^8.0.1", + "escalade": "^3.1.1", + "get-caller-file": "^2.0.5", + "require-directory": "^2.1.1", + "string-width": "^4.2.3", + "y18n": "^5.0.5", + "yargs-parser": "^21.1.1" + } + }, + "yargs-parser": { + "version": "21.1.1", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-21.1.1.tgz", + "integrity": "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==", + "dev": true + }, + "yocto-queue": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", + "integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==", + "dev": true + } + } +} diff --git a/comsapi/app/package.json b/comsapi/app/package.json new file mode 100644 index 00000000..a274e46b --- /dev/null +++ b/comsapi/app/package.json @@ -0,0 +1,64 @@ +{ + "name": "common-object-management-service", + "version": "0.7.0", + "private": true, + "description": "", + "author": "NR Common Service Showcase ", + "license": "Apache-2.0", + "scripts": { + "build": "echo Nothing to build", + "serve": "nodemon ./bin/www", + "start": "node ./bin/www", + "debug": "nodemon --inspect ./bin/www", + "test": "jest --verbose --forceExit --detectOpenHandles", + "lint": "eslint . **/www --no-fix --ignore-pattern 'node_modules' --ext .js", + "lint:fix": "eslint . **/www --fix --ignore-pattern 'node_modules' --ext .js", + "pretest": "npm run lint", + "posttest": "node ./lcov-fix.js", + "clean": "rm -rf coverage dist", + "purge": "rm -rf node_modules", + "rebuild": "npm run clean && npm run build", + "reinstall": "npm run purge && npm install", + "migrate": "npm run migrate:latest", + "migrate:down": "knex migrate:down", + "migrate:latest": "knex migrate:latest", + "migrate:make": "knex migrate:make", + "migrate:rollback": "knex migrate:rollback", + "migrate:rollback:all": "knex migrate:rollback --all", + "migrate:up": "knex migrate:up", + "seed": "knex seed:run" + }, + "dependencies": { + "@aws-sdk/client-s3": "^3.423.0", + "@aws-sdk/lib-storage": "^3.423.0", + "@aws-sdk/s3-request-presigner": "^3.423.0", + "api-problem": "^9.0.2", + "compression": "^1.7.4", + "config": "^3.3.9", + "content-disposition": "^0.5.4", + "cors": "^2.8.5", + "date-fns": "^2.30.0", + "express": "^4.18.2", + "express-basic-auth": "^1.2.1", + "express-winston": "^4.2.0", + "joi": "^17.11.0", + "js-yaml": "^4.1.0", + "jsonwebtoken": "^9.0.2", + "knex": "^2.5.1", + "objection": "^3.1.2", + "pg": "^8.11.3", + "winston": "^3.10.0", + "winston-transport": "^4.5.0" + }, + "devDependencies": { + "aws-sdk-client-mock": "^3.0.0", + "aws-sdk-client-mock-jest": "^3.0.0", + "eslint": "^8.50.0", + "eslint-config-recommended": "^4.1.0", + "eslint-plugin-prettier": "^5.0.0", + "jest": "^29.7.0", + "jest-joi": "^1.1.17", + "nodemon": "^3.0.1", + "supertest": "^6.3.3" + } +} diff --git a/comsapi/app/src/components/constants.js b/comsapi/app/src/components/constants.js new file mode 100644 index 00000000..e7d1bf5c --- /dev/null +++ b/comsapi/app/src/components/constants.js @@ -0,0 +1,90 @@ +module.exports = Object.freeze({ + /** Application authentication mode */ + AuthMode: { + /** Only Basic Authentication */ + BASICAUTH: 'BASICAUTH', + /** Both Basic and OIDC Authentication */ + FULLAUTH: 'FULLAUTH', + /** Public mode */ + NOAUTH: 'NOAUTH', + /** Only OIDC Authentication */ + OIDCAUTH: 'OIDCAUTH', + }, + + /** Current user authentication type */ + AuthType: { + /** Basic Authentication credential header provided */ + BASIC: 'BASIC', + /** OIDC JWT Authentication header provided */ + BEARER: 'BEARER', + /** No Authentication header provided */ + NONE: 'NONE' + }, + + /** Default CORS settings used across the entire application */ + DEFAULTCORS: { + /** Tells browsers to cache preflight requests for Access-Control-Max-Age seconds */ + maxAge: 600, + /** Set true to dynamically set Access-Control-Allow-Origin based on Origin */ + origin: true + }, + + /** Need to specify valid AWS region or it'll explode ('us-east-1' is default, 'ca-central-1' for Canada) */ + DEFAULTREGION: 'us-east-1', + + /** Download mode behavior overrides */ + DownloadMode: { + /** Proxies payload data through COMS */ + PROXY: 'proxy', + /** Returns only a pre-signed S3 url */ + URL: 'url' + }, + + /** + * Generic email regex modified to require domain of at least 2 characters + * @see {@link https://emailregex.com/} + */ + EMAILREGEX: '^[a-zA-Z0-9.!#$%&’*+/=?^_`{|}~-]+@[a-zA-Z0-9-]+(?:\\.[a-zA-Z0-9-]{2,})+$', + + /** Maximum number of parts supported by lib-storage upload */ + MAXPARTCOUNT: 10000, + + /** Maximum Content Length supported by S3 CopyObjectCommand */ + MAXCOPYOBJECTLENGTH: 5 * 1024 * 1024 * 1024, // 5 GB + + /** Maximum Content Length supported by S3 CopyObjectCommand */ + MAXFILEOBJECTLENGTH: 5 * 1024 * 1024 * 1024 * 1024, // 5 TB + + /** Allowable values for the Metadata Directive parameter */ + MetadataDirective: { + /** The original metadata is copied to the new version as-is where applicable. */ + COPY: 'COPY', + /** All original metadata is replaced by the metadata you specify. */ + REPLACE: 'REPLACE' + }, + + /** Minimum part size supported by lib-storage upload */ + MINPARTSIZE: 5 * 1024 * 1024, // 5 MB + + /** Allowable values for the Tagging Directive parameter */ + TaggingDirective: { + /** The original tags are copied to the new version as-is where applicable. */ + COPY: 'COPY', + /** All original tags are replaced by the tags you specify. */ + REPLACE: 'REPLACE' + }, + + /** Resource permissions */ + Permissions: { + /** Grants resource creation permission */ + CREATE: 'CREATE', + /** Grants resource read permission */ + READ: 'READ', + /** Grants resource update permission */ + UPDATE: 'UPDATE', + /** Grants resource deletion permission */ + DELETE: 'DELETE', + /** Grants resource permission management */ + MANAGE: 'MANAGE' + } +}); diff --git a/comsapi/app/src/components/crypt.js b/comsapi/app/src/components/crypt.js new file mode 100644 index 00000000..de0c3859 --- /dev/null +++ b/comsapi/app/src/components/crypt.js @@ -0,0 +1,84 @@ +const config = require('config'); +const crypto = require('crypto'); + +// GCM mode is good for situations with random access and authenticity requirements +// CBC mode is older, but is sufficiently secure with high performance for short payloads +const algorithm = 'aes-256-cbc'; +const encoding = 'base64'; +const encodingCheck = /^(?:[A-Za-z0-9+/]{4})*(?:[A-Za-z0-9+/]{4}|[A-Za-z0-9+/]{3}=|[A-Za-z0-9+/]{2}={2})$/; +const hashAlgorithm = 'sha256'; + +const crypt = { + /** + * @function encrypt + * Yields an encrypted string containing the iv and ciphertext, separated by a colon. + * If no key is provided, ciphertext will be the plaintext in base64 encoding. + * @param {string} text The input string contents + * @returns {string} The encrypted base64 formatted string in the format `iv:ciphertext`. + */ + encrypt(text) { + if (crypt.isEncrypted(text)) return text; + + const passphrase = config.has('server.passphrase') ? config.get('server.passphrase') : undefined; + if (passphrase && passphrase.length) { + let content = Buffer.from(text); + const iv = crypto.randomBytes(16); + const hash = crypto.createHash(hashAlgorithm); + // AES-256 key length must be exactly 32 bytes + const key = hash.update(passphrase).digest().subarray(0, 32); + const cipher = crypto.createCipheriv(algorithm, key, iv); + content = Buffer.concat([cipher.update(text), cipher.final()]); + return `${iv.toString(encoding)}:${content.toString(encoding)}`; + } else { + return text; + } + }, + + /** + * @function decrypt + * Yields the plaintext by accepting an encrypted string containing the iv and + * ciphertext, separated by a colon. If no key is provided, the plaintext will be + * the ciphertext. + * @param {string} text The input encrypted string contents + * @returns {string} The decrypted plaintext string, usually in utf-8 + */ + decrypt(text) { + if (!crypt.isEncrypted(text)) return text; + + const passphrase = config.has('server.passphrase') ? config.get('server.passphrase') : undefined; + if (passphrase && passphrase.length) { + const [iv, encrypted] = text.split(':').map(p => Buffer.from(p, encoding)); + let content = encrypted; + const hash = crypto.createHash(hashAlgorithm); + // AES-256 key length must be exactly 32 bytes + const key = hash.update(passphrase).digest().subarray(0, 32); + const decipher = crypto.createDecipheriv(algorithm, key, iv); + content = Buffer.concat([decipher.update(encrypted), decipher.final()]); + return Buffer.from(content, encoding).toString(); + } else { + return text; + } + }, + + /** + * @function isEncrypted + * A predicate function for determining if the input text is encrypted + * @param {string} text The input string contents + * @returns {boolean} True if encrypted, false if not + */ + isEncrypted(text) { + if (!text) return false; + if (typeof text !== 'string') return false; + const textParts = text.split(':'); + return ( + textParts.length == 2 && + textParts[0] && + textParts[1] && + textParts[0].length === 24 && // Base64 encoding of a 16 byte IV should be 24 + encodingCheck.test(textParts[0]) && + encodingCheck.test(textParts[1]) + ); + } +}; + +module.exports = crypt; diff --git a/comsapi/app/src/components/errorToProblem.js b/comsapi/app/src/components/errorToProblem.js new file mode 100644 index 00000000..8aed7c92 --- /dev/null +++ b/comsapi/app/src/components/errorToProblem.js @@ -0,0 +1,50 @@ +const Problem = require('api-problem'); + +const log = require('./log')(module.filename); + +/** + * @function errorToProblem + * Attempts to interpret and infer the type of Problem to respond with + * @param {string} service A string representing which service the error occured at + * @param {Error} e The raw error exception object + * @returns {Problem} A problem error type + */ +function errorToProblem(service, e) { + // If already problem type, just return as is + if (e instanceof Problem) { + return e; + } else if (e.response) { + // Handle raw data + let data; + if (typeof e.response.data === 'string' || e.response.data instanceof String) { + data = JSON.parse(e.response.data); + } else { + data = e.response.data; + } + + log.error(`Error from ${service}`, { function: 'errorToProblem', status: e.response.status, data: data }); + // Validation Error + if (e.response.status === 422) { + return new Problem(e.response.status, { + detail: data.detail, + errors: data.errors + }); + } + // Something else happened but there's a response + return new Problem(e.response.status, { detail: e.response.data }); + } else if (e.statusCode) { + // Handle errors with Status Codes + return new Problem(e.statusCode, { detail: e.message }); + } else if (e.$metadata && e.$metadata.httpStatusCode) { + // Handle S3 promise rejections + if (e.$response && e.$response.body) delete e.$response.body; + return new Problem(e.$metadata.httpStatusCode, { detail: e }); + } else { + // Handle all other errors + const message = `${service} Error: ${e.message}`; + log.error(message, { error: e, function: 'errorToProblem', status: 500 }); + return new Problem(500, { detail: message }); + } +} + +module.exports = errorToProblem; diff --git a/comsapi/app/src/components/log.js b/comsapi/app/src/components/log.js new file mode 100644 index 00000000..3cdb79ff --- /dev/null +++ b/comsapi/app/src/components/log.js @@ -0,0 +1,102 @@ +const config = require('config'); +const jwt = require('jsonwebtoken'); +const { parse } = require('path'); +const Transport = require('winston-transport'); +const { createLogger, format, transports } = require('winston'); +const { logger } = require('express-winston'); + +/** + * Class representing a winston transport writing to null + * @extends Transport + */ +class NullTransport extends Transport { + /** + * Constructor + * @param {object} opts Winston Transport options + */ + constructor(opts) { + super(opts); + } + + /** + * The transport logger + * @param {object} _info Object to log + * @param {function} callback Callback function + */ + log(_info, callback) { + callback(); + } +} + +/** + * Main Winston Logger + * @returns {object} Winston Logger + */ +const log = createLogger({ + exitOnError: false, + format: format.combine( + format.errors({ stack: true }), // Force errors to show stacktrace + format.timestamp(), // Add ISO timestamp to each entry + format.json(), // Force output to be in JSON format + ), + level: config.get('server.logLevel') +}); + +if (process.env.NODE_ENV !== 'test') { + log.add(new transports.Console({ handleExceptions: true })); +} else { + log.add(new NullTransport()); +} + +if (config.has('server.logFile')) { + log.add(new transports.File({ + filename: config.get('server.logFile'), + handleExceptions: true + })); +} + +/** + * Returns a Winston Logger or Child Winston Logger + * @param {string} [filename] Optional module filename path to annotate logs with + * @returns {object} A child logger with appropriate metadata if `filename` is defined. Otherwise returns a standard logger. + */ +const getLogger = (filename) => { + return filename ? log.child({ component: parse(filename).name }) : log; +}; + +/** + * Returns an express-winston middleware function for http logging + * @returns {function} An express-winston middleware function + */ +const httpLogger = logger({ + colorize: false, + // Parses express information to insert into log output + dynamicMeta: (req, res) => { + const token = jwt.decode((req.get('authorization') || '').slice(7)); + return { + azp: token && token.azp || undefined, + contentLength: res.get('content-length'), + httpVersion: req.httpVersion, + ip: req.ip, + method: req.method, + path: req.path, + query: Object.keys(req.query).length ? req.query : undefined, + responseTime: res.responseTime, + statusCode: res.statusCode, + userAgent: req.get('user-agent') + }; + }, + expressFormat: true, // Use express style message strings + level: 'http', + meta: true, // Must be true for dynamicMeta to execute + metaField: null, // Set to null for all attributes to be at top level object + requestWhitelist: [], // Suppress default value output + responseWhitelist: [], // Suppress default value output + // Skip logging kube-probe requests + skip: (req) => req.get('user-agent') && req.get('user-agent').includes('kube-probe'), + winstonInstance: log, +}); + +module.exports = getLogger; +module.exports.httpLogger = httpLogger; +module.exports.NullTransport = NullTransport; diff --git a/comsapi/app/src/components/queueManager.js b/comsapi/app/src/components/queueManager.js new file mode 100644 index 00000000..b8585bbe --- /dev/null +++ b/comsapi/app/src/components/queueManager.js @@ -0,0 +1,130 @@ +const config = require('config'); + +const log = require('./log')(module.filename); +const { objectQueueService, syncService } = require('../services'); + +/** + * @class QueueManager + * A singleton wrapper for managing the queue worker thread + */ +class QueueManager { + /** + * @constructor + */ + constructor() { + if (!QueueManager._instance) { + this.isBusy = false; + this._toClose = false; + QueueManager._instance = this; + } + + return QueueManager._instance; + } + + /** + * @function isBusy + * Gets the isBusy state + */ + get isBusy() { + return this._isBusy; + } + + /** + * @function isBusy + * @param {boolean} v The new state + * Sets the isBusy state + */ + set isBusy(v) { + this._isBusy = v; + if (!v && this.toClose) { + log.info('No longer processing jobs', { function: 'isBusy' }); + if (this._cb) this._cb(); + } + } + + /** + * @function toClose + * Gets the toClose state + */ + get toClose() { + return this._toClose; + } + + /** + * @function checkQueue + * Checks the queue for any unprocessed jobs + */ + checkQueue() { + if (!this.isBusy && !this.toClose) { + objectQueueService.queueSize().then(size => { + if (size > 0) this.processNextJob(); + }).catch((err) => { + log.error(`Error encountered while checking queue: ${err.message}`, { function: 'checkQueue', error: err }); + }); + } + } + + /** + * @function close + * Stalls the callback until any remaining jobs are completed + * @param {function} [cb] Optional callback + */ + close(cb = undefined) { + this._toClose = true; + this._cb = cb; + if (!this.isBusy) { + log.info('No longer processing jobs', { function: 'close' }); + if (cb) cb(); + } + } + + /** + * @function processNextJob + * Attempts to process the next job if available + * @param {object} message A message object + */ + async processNextJob() { + let job; + + try { + const response = await objectQueueService.dequeue(); + + if (response.length) { + this.isBusy = true; + job = response[0]; + + log.verbose(`Started processing job id ${job.id}`, { function: 'processNextJob', job: job }); + + const objectId = await syncService.syncJob(job.path, job.bucketId, job.full, job.createdBy); + + log.verbose(`Finished processing job id ${job.id}`, { function: 'processNextJob', job: job, objectId: objectId }); + + this.isBusy = false; + // If job is completed, check if there are more jobs + if (!this.toClose) this.checkQueue(); + } + } catch (err) { + log.error(`Error encountered on job id ${job.id}: ${err.message}`, { function: 'processNextJob', job: job, error: err }); + + const maxRetries = parseInt(config.get('server.maxRetries')); + if (job.retries + 1 > maxRetries) { + log.warn(`Job has exceeded the ${maxRetries} maximum retries permitted`, { function: 'processNextJob', job: job, maxRetries: maxRetries }); + } else { + objectQueueService.enqueue({ + jobs: [{ bucketId: job.bucketId, path: job.path }], + full: job.full, + retries: job.retries + 1, + createdBy: job.createdBy + }).then(() => { + log.verbose('Job has been reenqueued', { function: 'processNextJob', job: job }); + }).catch((e) => { + log.error(`Failed to reenqueue job id ${job.id}: ${e.message}`, { function: 'processNextJob', job: job }); + }); + } + + this.isBusy = false; + } + } +} + +module.exports = QueueManager; diff --git a/comsapi/app/src/components/utils.js b/comsapi/app/src/components/utils.js new file mode 100644 index 00000000..ece6e41b --- /dev/null +++ b/comsapi/app/src/components/utils.js @@ -0,0 +1,442 @@ +const Problem = require('api-problem'); +const config = require('config'); +const { existsSync, readFileSync } = require('fs'); +const { join } = require('path'); + +const { AuthMode, AuthType, DEFAULTREGION, MAXPARTCOUNT, MINPARTSIZE } = require('./constants'); +const log = require('./log')(module.filename); + +const DELIMITER = '/'; + +const utils = { + /** + * @function addDashesToUuid + * Yields a lowercase uuid `str` that has dashes inserted, or `str` if not a string. + * @param {string} str The input string uuid + * @returns {string} The string `str` but with dashes inserted, or `str` if not a string. + */ + addDashesToUuid(str) { + if ((typeof str === 'string' || str instanceof String) && str.length === 32) { + return `${str.slice(0, 8)}-${str.slice(8, 12)}-${str.slice(12, 16)}-${str.slice(16, 20)}-${str.slice(20)}` + .toLowerCase(); + } + else return str; + }, + + /** + * @function calculatePartSize + * Calculates the smallest feasible part size rounded to the nearest 5MB boundary + * @param {number} length The incoming file length + * @returns {number | undefined} The part size to use for this file length + */ + calculatePartSize(length) { + if (!length || typeof length !== 'number') return undefined; + return Math.ceil(length / (MAXPARTCOUNT * MINPARTSIZE)) * MINPARTSIZE; + }, + + /** + * @function delimit + * Yields a string `s` that will always have a trailing delimiter. Returns an empty string if falsy. + * @param {string} s The input string + * @returns {string} The string `s` with the trailing delimiter, or an empty string. + */ + delimit(s) { + if (s) return s.endsWith(DELIMITER) ? s : `${s}${DELIMITER}`; + else return ''; + }, + + /** + * @function getAppAuthMode + * Yields the current `AuthMode` this application is operating under. + * @returns {string} The application AuthMode + */ + getAppAuthMode() { + const basicAuth = config.has('basicAuth.enabled'); + const oidcAuth = config.has('keycloak.enabled'); + + if (!basicAuth && !oidcAuth) return AuthMode.NOAUTH; + else if (basicAuth && !oidcAuth) return AuthMode.BASICAUTH; + else if (!basicAuth && oidcAuth) return AuthMode.OIDCAUTH; + else return AuthMode.FULLAUTH; // basicAuth && oidcAuth + }, + + /** + * @function getBucket + * Acquire core S3 bucket credential information from database or configuration + * @param {string} [bucketId=undefined] An optional bucket ID to query database for bucket + * @returns {object} An object containing accessKeyId, bucket, endpoint, key, + * region and secretAccessKey attributes + * @throws If there are no records found with `bucketId` or, if `bucketId` is undefined, + * no bucket details exist in the configuration + */ + async getBucket(bucketId = undefined) { + try { + const data = { region: DEFAULTREGION }; + if (bucketId) { + // Function scoped import to avoid circular dependencies + const { read } = require('../services/bucket'); + const bucketData = await read(bucketId); + + data.accessKeyId = bucketData.accessKeyId; + data.bucket = bucketData.bucket; + data.endpoint = bucketData.endpoint; + data.key = bucketData.key; + data.secretAccessKey = bucketData.secretAccessKey; + if (bucketData.region) data.region = bucketData.region; + } else if (config.has('objectStorage') && config.has('objectStorage.enabled')) { + data.accessKeyId = config.get('objectStorage.accessKeyId'); + data.bucket = config.get('objectStorage.bucket'); + data.endpoint = config.get('objectStorage.endpoint'); + data.key = config.get('objectStorage.key'); + data.secretAccessKey = config.get('objectStorage.secretAccessKey'); + if (config.has('objectStorage.region')) { + data.region = config.get('objectStorage.region'); + } + } else { + throw new Error('Unable to get bucket'); + } + return data; + } catch (err) { + log.error(err.message, { function: 'getBucket' }); + throw new Problem(404, { detail: err.message }); + } + }, + + /** + * @function getBucketId + * Gets the bucketId if object is in database + * @param {string} objId The object id + * @returns {Promise} The bucketId + */ + async getBucketId(objId) { + let bucketId = undefined; + // Function scoped import to avoid circular dependencies + const { objectService } = require('../services'); + try { + bucketId = (await objectService.read(objId)).bucketId; + } catch (err) { + log.verbose(`${err.message}. Using default bucketId instead.`, { + function: 'getBucketId', objId: objId + }); + } + return bucketId; + }, + + /** + * @function getCurrentIdentity + * Attempts to acquire current identity value. + * Always takes first non-default value available. Yields `defaultValue` otherwise. + * @param {object} currentUser The express request currentUser object + * @param {string} [defaultValue=undefined] An optional default return value + * @returns {string} The current user identifier if applicable, or `defaultValue` + */ + getCurrentIdentity(currentUser, defaultValue = undefined) { + return utils.parseIdentityKeyClaims() + .map(claim => utils.getCurrentTokenClaim(currentUser, claim, undefined)) + .filter(value => value) // Drop falsy values from array + .concat(defaultValue)[0]; // Add defaultValue as last element of array + }, + + /** + * @function getCurrentSubject + * Attempts to acquire current subject id. Yields `defaultValue` otherwise + * @param {object} currentUser The express request currentUser object + * @param {string} [defaultValue=undefined] An optional default return value + * @returns {string} The current subject id if applicable, or `defaultValue` + */ + getCurrentSubject(currentUser, defaultValue = undefined) { + return utils.getCurrentTokenClaim(currentUser, 'sub', defaultValue); + }, + + /** + * @function getCurrentTokenClaim + * Attempts to acquire a specific current token claim. Yields `defaultValue` otherwise + * @param {object} currentUser The express request currentUser object + * @param {string} claim The requested token claim + * @param {string} [defaultValue=undefined] An optional default return value + * @returns {object} The requested current token claim if applicable, or `defaultValue` + */ + getCurrentTokenClaim(currentUser, claim, defaultValue = undefined) { + return (currentUser && currentUser.authType === AuthType.BEARER) + ? currentUser.tokenPayload[claim] + : defaultValue; + }, + + /** + * @function getGitRevision + * Gets the current git revision hash + * @see {@link https://stackoverflow.com/a/34518749} + * @returns {string} The git revision hash, or empty string + */ + getGitRevision() { + try { + const gitDir = (() => { + let dir = '.git', i = 0; + while (!existsSync(join(__dirname, dir)) && i < 5) { + dir = '../' + dir; + i++; + } + return dir; + })(); + + const head = readFileSync(join(__dirname, `${gitDir}/HEAD`)).toString().trim(); + return (head.indexOf(':') === -1) + ? head + : readFileSync(join(__dirname, `${gitDir}/${head.substring(5)}`)).toString().trim(); + } catch (err) { + log.warn(err.message, { function: 'getGitRevision' }); + return ''; + } + }, + + /** + * @function getKeyValue + * Transforms arbitrary {:} objects to {key: , value: } + * @param {object} input Arbitrary object containing key value attributes + * @returns {object[]} Array of objects in the form of `{key: , value: }` + */ + getKeyValue(input) { + return Object.entries({ ...input }).map(([k, v]) => ({ key: k, value: v })); + }, + + /** + * @function getMetadata + * Derives metadata from a request header object + * @param {object} obj The request headers to get key/value pairs from + * @returns {object | undefined} An object with metadata key/value pair attributes or undefined + */ + getMetadata(obj) { + const metadata = Object.fromEntries(Object.keys(obj) + .filter((key) => key.toLowerCase().startsWith('x-amz-meta-')) + .map((key) => ([key.toLowerCase().substring(11), obj[key]])) + ); + return Object.keys(metadata).length ? metadata : undefined; + }, + + /** + * @function getObjectsByKeyValue + * Get tag/metadata objects in array that have given key and value + * @param {object[]} array an array of objects (eg: [{ key: 'a', value: '1'}, { key: 'b', value: '1'}] + * @param {string} key the string to match in the objects's `key` property + * @param {string} value the string to match in the objects's `value` property + * @returns {object} the matching object, or undefined + */ + getObjectsByKeyValue(array, key, value) { + return array.find(obj => (obj.key === key && obj.value === value)); + }, + + /** + * @function getS3VersionId + * Gets the s3VersionId from database using given internal COMS version id + * or returns given s3VersionId + * @param {string} s3VersionId S3 Version id + * @param {string} versionId A COMS version id + * @param {string} objectId The related COMS object id + * @returns {Promise} s3 Version id as string type or undefined + */ + async getS3VersionId(s3VersionId, versionId, objectId) { + let result = undefined; + if (s3VersionId) { + result = s3VersionId.toString(); + } else if (versionId) { + const { versionService } = require('../services'); + const version = await versionService.get({ versionId: versionId, s3VersionId: undefined, objectId: objectId }); + if (version.s3VersionId) { + result = version.s3VersionId; + } + } + return result; + }, + + /** + * @function getUniqueObjects + * @param {object[]} arr array of objects + * @param {string} key key of object property whose value we are comparing + * @returns array of unique objects based on value of a given property + */ + getUniqueObjects(array, key) { + return [...new Map(array.map(item => [item[key], item])).values()]; + }, + + /** + * @function groupByObject + * Re-structure array of nested objects + * @see {@link https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/reduce#grouping_objects_by_a_property} + * @param {string} property key (or property accessor) to group by + * @param {string} group attribute name for nested group + * @param {object[]} objectArray array of objects + * @returns {object[]} returns an array of Objects, each with nested group of objects + */ + groupByObject(property, group, objectArray) { + return objectArray.reduce((acc, obj) => { + // value of the 'property' attribute of obj + const val = obj[property]; + // does accumulator array have element with nested array containing current obj + const el = acc.find((ob) => { + return ob[group].some((p) => p[property] === val); + }); + if (el) { + // add to current element's nested array + el[group].push(obj); + } else { + // add to a new top level element in accumulator array + acc.push({ [property]: val, [group]: [obj] }); + } + return acc; + }, []); + }, + + /** + * @function isAtPath + * Predicate function determining if the `path` is a non-directory member of the `prefix` path + * @param {string} prefix The base "folder" + * @param {string} path The "file" to check + * @returns {boolean} True if path is member of prefix. False in all other cases. + */ + isAtPath(prefix, path) { + if (typeof prefix !== 'string' || typeof path !== 'string') return false; + if (prefix === path) return true; // Matching strings are always at the at the path + if (path.endsWith(DELIMITER)) return false; // Trailing slashes references the folder + + const pathParts = path.split(DELIMITER).filter(part => part); + const prefixParts = prefix.split(DELIMITER).filter(part => part); + return prefixParts.every((part, i) => pathParts[i] === part) + && pathParts.filter(part => !prefixParts.includes(part)).length === 1; + }, + + /** + * @function isTruthy + * Returns true if the element name in the object contains a truthy value + * @param {object} value The object to evaluate + * @returns {boolean} True if truthy, false if not, and undefined if undefined + */ + isTruthy(value) { + if (value === undefined) return value; + + const isStr = typeof value === 'string' || value instanceof String; + const trueStrings = ['true', 't', 'yes', 'y', '1']; + return value === true || value === 1 || isStr && trueStrings.includes(value.toLowerCase()); + }, + + /** + * @function joinPath + * Joins a set of string arguments to yield a string path + * @param {...string} items The strings to join on + * @returns {string} A path string with the specified delimiter + */ + joinPath(...items) { + if (items && items.length) { + const parts = []; + items.forEach(p => { + if (p) p.split(DELIMITER).forEach(x => { + if (x && x.trim().length) parts.push(x); + }); + }); + return parts.join(DELIMITER); + } + else return ''; + }, + + /** + * @function mixedQueryToArray + * Standardizes query params to yield an array of unique string values + * @param {string|string[]} param The query param to process + * @returns {string[]} A unique, non-empty array of string values, or undefined if empty + */ + mixedQueryToArray(param) { + // Short circuit undefined if param is falsy + if (!param) return undefined; + + const parsed = (Array.isArray(param)) + ? param.flatMap(p => utils.parseCSV(p)) + : utils.parseCSV(param); + const unique = [...new Set(parsed)]; + + return unique.length ? unique : undefined; + }, + + /** + * @function parseCSV + * Converts a comma separated value string into an array of string values + * @param {string} value The CSV string to parse + * @returns {string[]} An array of string values, or `value` if it is not a string + */ + parseCSV(value) { + return (typeof value === 'string' || value instanceof String) + ? value.split(',').map(s => s.trim()) + : value; + }, + + /** + * @function parseIdentityKeyClaims + * Returns an array of strings representing potential identity key claims + * Array will always end with the last value as 'sub' + * @returns {string[]} An array of string values, or `value` if it is not a string + */ + parseIdentityKeyClaims() { + const claims = []; + if (config.has('keycloak.identityKey')) { + claims.push(...utils.parseCSV(config.get('keycloak.identityKey'))); + } + return claims.concat('sub'); + }, + + /** + * @function renameObjectProperty + * Rename a property in given object + * @param {object} obj The object with a property you are changing + * @param {string} oldKey The property to rename + * @param {string} newKey The new name for the property + * @returns {object} the given object with property renamed + */ + renameObjectProperty(obj, oldKey, newKey) { + delete Object.assign(obj, { [newKey]: obj[oldKey] })[oldKey]; + return obj; + }, + + /** + * @function streamToBuffer + * Reads a Readable stream, writes to and returns an array buffer + * @see {@link https://github.com/aws/aws-sdk-js-v3/issues/1877#issuecomment-755446927} + * @param {Readable} stream A readable stream object + * @returns {Buffer} A buffer usually formatted as an Uint8Array + */ + streamToBuffer(stream) { // Readable + return new Promise((resolve, reject) => { + const chunks = []; // Uint8Array[] + stream.on('data', (chunk) => chunks.push(chunk)); + stream.on('end', () => resolve(Buffer.concat(chunks))); + stream.on('error', reject); + }); + }, + + /** + * @function stripDelimit + * Yields a string `s` that will never have a trailing delimiter. Returns an empty string if falsy. + * @param {string} s The input string + * @returns {string} The string `s` without the trailing delimiter, or an empty string. + */ + stripDelimit(s) { + if (s) return s.endsWith(DELIMITER) ? utils.stripDelimit(s.slice(0, -1)) : s; + else return ''; + }, + + /** + * @function toLowerKeys + * Converts all key names for all objects in an array to lowercase + * @param {object[]} arr Array of tag objects (eg: [{Key: k1, Value: V1}]) + * @returns {object[]} Array of objects (eg: [{key: k1, value: V1}]) or undefined if empty + */ + toLowerKeys(arr) { + if (!arr || !Array.isArray(arr)) return undefined; + return arr.map(obj => { + return Object.fromEntries( + Object.entries(obj).map(([key, value]) => { + return [key.toLowerCase(), value]; + }), + ); + }); + }, +}; + +module.exports = utils; diff --git a/comsapi/app/src/controllers/bucket.js b/comsapi/app/src/controllers/bucket.js new file mode 100644 index 00000000..1cc31cea --- /dev/null +++ b/comsapi/app/src/controllers/bucket.js @@ -0,0 +1,257 @@ +const Problem = require('api-problem'); +const { UniqueViolationError } = require('objection'); +const { NIL: SYSTEM_USER } = require('uuid'); + +const { DEFAULTREGION } = require('../components/constants'); +const errorToProblem = require('../components/errorToProblem'); +const log = require('../components/log')(module.filename); +const { + addDashesToUuid, + getCurrentIdentity, + isTruthy, + joinPath, + mixedQueryToArray, + stripDelimit +} = require('../components/utils'); +const { redactSecrets } = require('../db/models/utils'); + +const { bucketService, storageService, userService } = require('../services'); + +const SERVICE = 'BucketService'; +const secretFields = ['accessKeyId', 'secretAccessKey']; + +/** + * The Bucket Controller + */ +const controller = { + /** + * @function _processS3Headers + * Accepts a typical S3 response object and inserts appropriate express response headers + * Returns an array of non-standard headers that need to be CORS exposed + * @param {object} s3Resp S3 response object + * @param {object} res Express response object + * @returns {string[]} An array of non-standard headers that need to be CORS exposed + */ + _processS3Headers(s3Resp, res) { + // TODO: Consider adding 'x-coms-public' and 'x-coms-path' headers into API spec? + const exposedHeaders = []; + + if (s3Resp.ContentLength) res.set('Content-Length', s3Resp.ContentLength); + if (s3Resp.ContentType) res.set('Content-Type', s3Resp.ContentType); + if (s3Resp.ETag) { + const etag = 'ETag'; + res.set(etag, s3Resp.ETag); + exposedHeaders.push(etag); + } + if (s3Resp.LastModified) res.set('Last-Modified', s3Resp.LastModified); + if (s3Resp.Metadata) { + Object.entries(s3Resp.Metadata).forEach(([key, value]) => { + const metadata = `x-amz-meta-${key}`; + res.set(metadata, value); + exposedHeaders.push(metadata); + }); + + if (s3Resp.Metadata.name) res.attachment(s3Resp.Metadata.name); + } + if (s3Resp.ServerSideEncryption) { + const sse = 'x-amz-server-side-encryption'; + res.set(sse, s3Resp.ServerSideEncryption); + exposedHeaders.push(sse); + } + if (s3Resp.VersionId) { + const s3VersionId = 'x-amz-version-id'; + res.set(s3VersionId, s3Resp.VersionId); + exposedHeaders.push(s3VersionId); + } + + return exposedHeaders; + }, + + /** + * @function _validateCredentials + * Guard against creating or update a bucket with invalid creds + * @param {object} credentials The body of the request + * @throws A conflict error problem if the bucket is not reachable + */ + async _validateCredentials(credentials) { + try { + const bucketSettings = { + accessKeyId: credentials.accessKeyId, + bucket: credentials.bucket, + endpoint: credentials.endpoint, + key: credentials.key ? credentials.key : '/', + region: credentials.region || DEFAULTREGION, + secretAccessKey: credentials.secretAccessKey, + }; + await storageService.headBucket(bucketSettings); + } catch (e) { + // If it's caught here it's unable to validate the supplied store/bucket and creds + log.warn(`Failure to validate bucket credentials: ${e.message}`, { + function: '_validateCredentials', + }); + throw new Problem(409, { + detail: 'Unable to validate supplied credentials for the bucket', + }); + } + }, + + /** + * @function createBucket + * Creates a bucket + * @param {object} req Express request object + * @param {object} res Express response object + * @param {function} next The next callback function + * @returns {function} Express middleware function + * @throws The error encountered upon failure + */ + async createBucket(req, res, next) { + const data = { + ...req.body, + endpoint: stripDelimit(req.body.endpoint), + key: req.body.key ? joinPath(stripDelimit(req.body.key)) : undefined + }; + let response = undefined; + + try { + // Check for credential accessibility/validity first + await controller._validateCredentials(data); + data.userId = await userService.getCurrentUserId(getCurrentIdentity(req.currentUser, SYSTEM_USER)); + + response = await bucketService.create(data); + } catch (e) { + // If bucket exists, check if credentials precisely match + if (e instanceof UniqueViolationError) { + // Grant all permissions if credentials precisely match + response = await bucketService.checkGrantPermissions(data).catch(permErr => { + next(new Problem(403, { detail: permErr.message, instance: req.originalUrl })); + }); + } else { + next(errorToProblem(SERVICE, e)); + } + } finally { + if (response) res.status(201).json(redactSecrets(response, secretFields)); + } + }, + + /** + * @function deleteBucket + * Deletes the bucket + * @param {object} req Express request object + * @param {object} res Express response object + * @param {function} next The next callback function + * @returns {function} Express middleware function + */ + async deleteBucket(req, res, next) { + try { + const bucketId = addDashesToUuid(req.params.bucketId); + await bucketService.delete(bucketId); + res.status(204).end(); + } catch (e) { + next(errorToProblem(SERVICE, e)); + } + }, + + /** + * @function headBucket + * Returns bucket headers + * @param {object} req Express request object + * @param {object} res Express response object + * @param {function} next The next callback function + * @returns {function} Express middleware function + */ + async headBucket(req, res, next) { + try { + const bucketId = addDashesToUuid(req.params.bucketId); + await storageService.headBucket({ bucketId }); + + res.status(204).end(); + } catch (e) { + next(errorToProblem(SERVICE, e)); + } + }, + + /** + * @function readBucket + * Returns a bucket + * @param {object} req Express request object + * @param {object} res Express response object + * @param {function} next The next callback function + * @returns {function} Express middleware function + */ + async readBucket(req, res, next) { + try { + const bucketId = addDashesToUuid(req.params.bucketId); + const response = await bucketService.read(bucketId); + res.status(200).json(redactSecrets(response, secretFields)); + } catch (e) { + next(errorToProblem(SERVICE, e)); + } + }, + + /** + * @function searchBuckets + * Search and filter for specific buckets + * @param {object} req Express request object + * @param {object} res Express response object + * @param {function} next The next callback function + * @returns {function} Express middleware function + */ + async searchBuckets(req, res, next) { + try { + const bucketIds = mixedQueryToArray(req.query.bucketId); + const params = { + bucketId: bucketIds ? bucketIds.map(id => addDashesToUuid(id)) : bucketIds, + bucketName: req.query.bucketName, + key: req.query.key, + active: isTruthy(req.query.active) + }; + + const response = await bucketService.searchBuckets(params); + res.status(200).json(response.map(bucket => redactSecrets(bucket, secretFields))); + } catch (error) { + next(error); + } + }, + + /** + * @function updateBucket + * Updates a bucket + * @param {object} req Express request object + * @param {object} res Express response object + * @param {function} next The next callback function + * @returns {function} Express middleware function + */ + async updateBucket(req, res, next) { + try { + const bucketId = addDashesToUuid(req.params.bucketId); + const currentBucket = await bucketService.read(bucketId); + + // Check for credential accessibility/validity first + // Need to cross reference with existing data when partial patch data is provided + await controller._validateCredentials({ + ...currentBucket, + ...req.body, + endpoint: req.body.endpoint ? stripDelimit(req.body.endpoint) : currentBucket.endpoint + }); + + const userId = await userService.getCurrentUserId(getCurrentIdentity(req.currentUser, SYSTEM_USER), SYSTEM_USER); + const response = await bucketService.update({ + bucketId: bucketId, + bucketName: req.body.bucketName, + accessKeyId: req.body.accessKeyId, + bucket: req.body.bucket, + endpoint: req.body.endpoint ? stripDelimit(req.body.endpoint) : undefined, + secretAccessKey: req.body.secretAccessKey, + region: req.body.region, + active: isTruthy(req.body.active), + userId: userId + }); + + res.status(200).json(redactSecrets(response, secretFields)); + } catch (e) { + next(errorToProblem(SERVICE, e)); + } + } +}; + +module.exports = controller; diff --git a/comsapi/app/src/controllers/bucketPermission.js b/comsapi/app/src/controllers/bucketPermission.js new file mode 100644 index 00000000..3bb2388e --- /dev/null +++ b/comsapi/app/src/controllers/bucketPermission.js @@ -0,0 +1,123 @@ +const errorToProblem = require('../components/errorToProblem'); +const { + addDashesToUuid, + mixedQueryToArray, + getCurrentIdentity, + groupByObject, + isTruthy +} = require('../components/utils'); +const { NIL: SYSTEM_USER } = require('uuid'); +const { bucketPermissionService, userService } = require('../services'); + +const SERVICE = 'BucketPermissionService'; + +/** + * The Permission Controller + */ +const controller = { + /** + * @function searchPermissions + * Searches for bucket permissions + * @param {object} req Express request object + * @param {object} res Express response object + * @param {function} next The next callback function + * @returns {function} Express middleware function + */ + async searchPermissions(req, res, next) { + try { + const bucketIds = mixedQueryToArray(req.query.bucketId); + const userIds = mixedQueryToArray(req.query.userId); + const bucketPermissions = await bucketPermissionService.searchPermissions({ + bucketId: bucketIds ? bucketIds.map(id => addDashesToUuid(id)) : bucketIds, + userId: userIds ? userIds.map(id => addDashesToUuid(id)) : userIds, + permCode: mixedQueryToArray(req.query.permCode) + }); + const response = groupByObject('bucketId', 'permissions', bucketPermissions); + + // if also returning buckets with implicit object permissions + if (isTruthy(req.query.objectPerms)) { + const buckets = await bucketPermissionService.listInheritedBucketIds(userIds); + + // merge list of bucket permissions + buckets.forEach(bucketId => { + if (!response.map(r => r.bucketId).includes(bucketId) && + // limit to to bucketId(s) request query parameter if given + (!bucketIds?.length || bucketIds?.includes(bucketId))) { + response.push({ + bucketId: bucketId, + permissions: [] + }); + } + }); + } + + res.status(200).json(response); + } catch (e) { + next(errorToProblem(SERVICE, e)); + } + }, + + /** + * @function listPermissions + * Returns the bucket permissions + * @param {object} req Express request object + * @param {object} res Express response object + * @param {function} next The next callback function + * @returns {function} Express middleware function + */ + async listPermissions(req, res, next) { + try { + const userIds = mixedQueryToArray(req.query.userId); + const response = await bucketPermissionService.searchPermissions({ + bucketId: addDashesToUuid(req.params.bucketId), + userId: userIds ? userIds.map(id => addDashesToUuid(id)) : userIds, + permCode: mixedQueryToArray(req.query.permCode) + }); + res.status(200).json(response); + } catch (e) { + next(errorToProblem(SERVICE, e)); + } + }, + + /** + * @function addPermissions + * Grants bucket permissions to users + * @param {object} req Express request object + * @param {object} res Express response object + * @param {function} next The next callback function + * @returns {function} Express middleware function + */ + async addPermissions(req, res, next) { + try { + const userId = await userService.getCurrentUserId(getCurrentIdentity(req.currentUser, SYSTEM_USER)); + const response = await bucketPermissionService.addPermissions(addDashesToUuid(req.params.bucketId), req.body, userId); + res.status(201).json(response); + } catch (e) { + next(errorToProblem(SERVICE, e)); + } + }, + + /** + * @function removePermissions + * Deletes bucket permissions for a user + * @param {object} req Express request object + * @param {object} res Express response object + * @param {function} next The next callback function + * @returns {function} Express middleware function + */ + async removePermissions(req, res, next) { + try { + const userArray = mixedQueryToArray(req.query.userId); + const userIds = userArray ? userArray.map(id => addDashesToUuid(id)) : userArray; + const permissions = mixedQueryToArray(req.query.permCode); + const response = await bucketPermissionService.removePermissions(req.params.bucketId, userIds, permissions); + res.status(200).json(response); + } catch (e) { + next(errorToProblem(SERVICE, e)); + } + }, + + +}; + +module.exports = controller; diff --git a/comsapi/app/src/controllers/index.js b/comsapi/app/src/controllers/index.js new file mode 100644 index 00000000..7e629bad --- /dev/null +++ b/comsapi/app/src/controllers/index.js @@ -0,0 +1,11 @@ +module.exports = { + bucketController: require('./bucket'), + bucketPermissionController: require('./bucketPermission'), + metadataController: require('./metadata'), + objectController: require('./object'), + objectPermissionController: require('./objectPermission'), + syncController: require('./sync'), + userController: require('./user'), + tagController: require('./tag'), + versionController: require('./version') +}; diff --git a/comsapi/app/src/controllers/metadata.js b/comsapi/app/src/controllers/metadata.js new file mode 100644 index 00000000..6df464c2 --- /dev/null +++ b/comsapi/app/src/controllers/metadata.js @@ -0,0 +1,36 @@ +const config = require('config'); +const errorToProblem = require('../components/errorToProblem'); +const { getMetadata } = require('../components/utils'); +const { metadataService } = require('../services'); + +const SERVICE = 'MetadataService'; + +/** + * The Metadata Controller + */ +const controller = { + /** + * @function searchMetadata + * Search and filter for specific metadata + * @param {object} req Express request object + * @param {object} res Express response object + * @param {function} next The next callback function + * @returns {function} Express middleware function + */ + async searchMetadata(req, res, next) { + try { + const metadata = getMetadata(req.headers); + const params = { + metadata: metadata && Object.keys(metadata).length ? metadata : undefined, + privacyMask : req.currentUser.authType !== 'BASIC' ? config.has('server.privacyMask') : false + }; + + const response = await metadataService.searchMetadata(params); + res.status(200).json(response); + } catch (e) { + next(errorToProblem(SERVICE, e)); + } + }, +}; + +module.exports = controller; diff --git a/comsapi/app/src/controllers/object.js b/comsapi/app/src/controllers/object.js new file mode 100644 index 00000000..49c0ee73 --- /dev/null +++ b/comsapi/app/src/controllers/object.js @@ -0,0 +1,1062 @@ +const Problem = require('api-problem'); +const config = require('config'); +const cors = require('cors'); +const { v4: uuidv4, NIL: SYSTEM_USER } = require('uuid'); + +const { + DEFAULTCORS, + DownloadMode, + MAXCOPYOBJECTLENGTH, + MAXFILEOBJECTLENGTH, + MetadataDirective, + TaggingDirective +} = require('../components/constants'); +const errorToProblem = require('../components/errorToProblem'); +const log = require('../components/log')(module.filename); +const { + addDashesToUuid, + getBucketId, + getCurrentIdentity, + getKeyValue, + getMetadata, + getS3VersionId, + joinPath, + isTruthy, + mixedQueryToArray, + toLowerKeys, + getBucket, + renameObjectProperty +} = require('../components/utils'); +const utils = require('../db/models/utils'); + +const { + bucketPermissionService, + metadataService, + objectService, + storageService, + tagService, + userService, + versionService, +} = require('../services'); + +const SERVICE = 'ObjectService'; + +/** + * The Object Controller + */ +const controller = { + /** + * @function _processS3Headers + * Accepts a typical S3 response object and inserts appropriate express response headers + * Returns an array of non-standard headers that need to be CORS exposed + * @param {object} s3Resp S3 response object + * @param {object} res Express response object + * @returns {string[]} An array of non-standard headers that need to be CORS exposed + */ + _processS3Headers(s3Resp, res) { + // TODO: Consider adding 'x-coms-public' and 'x-coms-path' headers into API spec? + const exposedHeaders = []; + + if (s3Resp.ContentLength) res.set('Content-Length', s3Resp.ContentLength); + if (s3Resp.ContentType) res.set('Content-Type', s3Resp.ContentType); + if (s3Resp.ETag) { + const etag = 'ETag'; + res.set(etag, s3Resp.ETag); + exposedHeaders.push(etag); + } + if (s3Resp.LastModified) res.set('Last-Modified', s3Resp.LastModified); + if (s3Resp.Metadata) { + Object.entries(s3Resp.Metadata).forEach(([key, value]) => { + const metadata = `x-amz-meta-${key}`; + res.set(metadata, value); + exposedHeaders.push(metadata); + }); + + } + if (s3Resp.ServerSideEncryption) { + const sse = 'x-amz-server-side-encryption'; + res.set(sse, s3Resp.ServerSideEncryption); + exposedHeaders.push(sse); + } + if (s3Resp.VersionId) { + const s3VersionId = 'x-amz-version-id'; + res.set(s3VersionId, s3Resp.VersionId); + exposedHeaders.push(s3VersionId); + } + + return exposedHeaders; + }, + + /** + * @function addMetadata + * Creates a new version of the object via copy with the new metadata added + * @param {object} req Express request object + * @param {object} res Express response object + * @param {function} next The next callback function + * @returns {function} Express middleware function + */ + async addMetadata(req, res, next) { + try { + const bucketId = req.currentObject?.bucketId; + const objId = addDashesToUuid(req.params.objectId); + const objPath = req.currentObject?.path; + const userId = await userService.getCurrentUserId(getCurrentIdentity(req.currentUser, SYSTEM_USER)); + + // get source S3 VersionId + const sourceS3VersionId = await getS3VersionId( + req.query.s3VersionId, + addDashesToUuid(req.query.versionId), + objId + ); + + // get version from S3 + const source = await storageService.headObject({ + filePath: objPath, + s3VersionId: sourceS3VersionId, + bucketId: bucketId + }); + if (source.ContentLength > MAXCOPYOBJECTLENGTH) { + throw new Error('Cannot copy an object larger than 5GB'); + } + // get existing tags on source object, eg: { 'animal': 'bear', colour': 'black' } + const sourceObject = await storageService.getObjectTagging({ + filePath: objPath, + s3VersionId: sourceS3VersionId, + bucketId: bucketId + }); + const sourceTags = Object.assign({}, + ...(sourceObject.TagSet?.map(item => ({ [item.Key]: item.Value })) ?? []) + ); + + const metadataToAppend = getMetadata(req.headers); + const data = { + bucketId: bucketId, + copySource: objPath, + filePath: objPath, + metadata: { + ...source.Metadata, // Take existing metadata first + ...metadataToAppend, // Append new metadata + }, + metadataDirective: MetadataDirective.REPLACE, + // copy existing tags from source object + tags: { ...sourceTags, 'coms-id': objId }, + taggingDirective: TaggingDirective.REPLACE, + s3VersionId: sourceS3VersionId, + }; + // create new version with metadata in S3 + const s3Response = await storageService.copyObject(data); + + await utils.trxWrapper(async (trx) => { + // create or update version in DB (if a non-versioned object) + const version = s3Response.VersionId ? + await versionService.copy(sourceS3VersionId, s3Response.VersionId, objId, s3Response.CopyObjectResult?.ETag, userId, trx) : + await versionService.update({ ...data, id: objId, etag: s3Response.CopyObjectResult?.ETag, isLatest: true }, userId, trx); + + // add metadata for version in DB + await metadataService.associateMetadata(version.id, getKeyValue(data.metadata), userId, trx); + + // add tags to new version in DB + await tagService.associateTags(version.id, getKeyValue(data.tags), userId, trx); + }); + + res.status(204).end(); + } catch (e) { + next(errorToProblem(SERVICE, e)); + } + }, + + /** + * @function addTags + * Adds the tag set to the requested object + * @param {object} req Express request object + * @param {object} res Express response object + * @param {function} next The next callback function + * @returns {function} Express middleware function + * @throws The error encountered upon failure + */ + async addTags(req, res, next) { + try { + const bucketId = req.currentObject?.bucketId; + const objId = addDashesToUuid(req.params.objectId); + const objPath = req.currentObject?.path; + const userId = await userService.getCurrentUserId(getCurrentIdentity(req.currentUser, SYSTEM_USER)); + // format new tags to array of objects + const newTags = Object.entries({ ...req.query.tagset }).map(([k, v]) => ({ Key: k, Value: v })); + // get source version that we are adding tags to + const sourceS3VersionId = await getS3VersionId(req.query.s3VersionId, addDashesToUuid(req.query.versionId), objId); + // get existing tags on source version + const { TagSet: existingTags } = await storageService.getObjectTagging({ filePath: objPath, s3VersionId: sourceS3VersionId, bucketId: bucketId }); + + const newSet = newTags + // Join new tags and existing tags + .concat(existingTags ?? []) + // remove existing 'coms-id' tag if it exists + .filter(x => x.Key !== 'coms-id') + // filter duplicates + .filter((element, idx, arr) => arr.findIndex(element2 => (element2.Key === element.Key)) === idx) + // add 'coms-id' tag + .concat([{ Key: 'coms-id', Value: objId }]); + + if (newSet.length > 10) { + // 409 when total tag limit exceeded + throw new Problem(409, { + detail: 'Request exceeds maximum of 9 user-defined tag sets allowed', + instance: req.originalUrl + }); + } + + const data = { + bucketId, + filePath: objPath, + tags: newSet, + s3VersionId: sourceS3VersionId ? sourceS3VersionId.toString() : undefined + }; + + // Add tags to the version in S3 + await storageService.putObjectTagging(data); + + // Add tags to version in DB + await utils.trxWrapper(async (trx) => { + const version = await versionService.get({ s3VersionId: data.s3VersionId, objectId: objId }, trx); + // use replaceTags() in case they are replacing an existing tag with same key which we need to dissociate + await tagService.replaceTags(version.id, toLowerKeys(data.tags), userId, trx); + }); + + res.status(204).end(); + } catch (e) { + next(errorToProblem(SERVICE, e)); + } + }, + + /** + * @function createObject + * Creates a new object + * @param {object} req Express request object + * @param {object} res Express response object + * @param {function} next The next callback function + * @returns {function} Express middleware function + */ + async createObject(req, res, next) { + try { + const userId = await userService.getCurrentUserId(getCurrentIdentity(req.currentUser, SYSTEM_USER)); + + // Preflight CREATE permission check if bucket scoped and OIDC authenticated + const bucketId = req.query.bucketId ? addDashesToUuid(req.query.bucketId) : undefined; + if (bucketId && userId) { + const permission = await bucketPermissionService.searchPermissions({ userId: userId, bucketId: bucketId, permCode: 'CREATE' }); + if (!permission.length) { + throw new Problem(403, { + detail: 'User lacks permission to complete this action', + instance: req.originalUrl + }); + } + } + + // Preflight existence check for bucketId + const { key: bucketKey } = await getBucket(bucketId); + + const objId = uuidv4(); + const data = { + id: objId, + bucketId: bucketId, + length: req.currentUpload.contentLength, + name: req.currentUpload.filename, + mimeType: req.currentUpload.mimeType, + metadata: getMetadata(req.headers), + tags: { + ...req.query.tagset, + 'coms-id': objId // Enforce `coms-id:` tag + } + }; + + let s3Response; + try { + // Preflight S3 Object check + await storageService.headObject({ + filePath: joinPath(bucketKey, req.currentUpload.filename), + bucketId: bucketId + }); + + // Hard short circuit skip file as the object already exists on bucket + throw new Problem(409, { + detail: 'Bucket already contains object', + instance: req.originalUrl + }); + } catch (err) { + if (err instanceof Problem) throw err; // Rethrow Problem type errors + + // Object is soft deleted from the bucket + if (err.$response?.headers['x-amz-delete-marker']) { + throw new Problem(409, { + detail: 'Bucket already contains object', + instance: req.originalUrl + }); + } + + // Skip upload in the unlikely event we get an unexpected error from headObject + if (err.$metadata?.httpStatusCode !== 404) { + throw new Problem(502, { + detail: 'Bucket communication error', + instance: req.originalUrl + }); + } + + // Object does not exist on bucket + if (req.currentUpload.contentLength < MAXCOPYOBJECTLENGTH) { + log.debug('Uploading with putObject', { + contentLength: req.currentUpload.contentLength, + function: 'createObject', + uploadMethod: 'putObject' + }); + s3Response = await storageService.putObject({ ...data, stream: req }); + } else if (req.currentUpload.contentLength < MAXFILEOBJECTLENGTH) { + log.debug('Uploading with lib-storage', { + contentLength: req.currentUpload.contentLength, + function: 'createObject', + uploadMethod: 'lib-storage' + }); + s3Response = await storageService.upload({ ...data, stream: req }); + } else { + throw new Problem(413, { + detail: 'File exceeds maximum 50GB limit', + instance: req.originalUrl + }); + } + } + + const dbResponse = await utils.trxWrapper(async (trx) => { + // Create Object + const object = await objectService.create({ + ...data, + userId: userId, + path: joinPath(bucketKey, data.name) + }, trx); + + // Create Version + const s3VersionId = s3Response.VersionId ?? null; + const version = await versionService.create({ + ...data, + etag: s3Response.ETag, + s3VersionId: s3VersionId, + isLatest: true + }, userId, trx); + object.versionId = version.id; + + // Create Metadata + if (data.metadata && Object.keys(data.metadata).length) { + await metadataService.associateMetadata(version.id, getKeyValue(data.metadata), userId, trx); + } + + // Create Tags + if (data.tags && Object.keys(data.tags).length) { + await tagService.associateTags(version.id, getKeyValue(data.tags), userId, trx); + } + + return object; + }); + + res.status(200).json({ + ...data, + ...dbResponse, + ...renameObjectProperty(s3Response, 'VersionId', 's3VersionId') + }); + } catch (e) { + next(errorToProblem(SERVICE, e)); + } + }, + + /** + * @function deleteMetadata + * Creates a new version of the object via copy with the given metadata removed + * @param {object} req Express request object + * @param {object} res Express response object + * @param {function} next The next callback function + * @returns {function} Express middleware function + */ + async deleteMetadata(req, res, next) { + try { + const bucketId = req.currentObject?.bucketId; + const objId = addDashesToUuid(req.params.objectId); + const objPath = req.currentObject?.path; + const userId = await userService.getCurrentUserId(getCurrentIdentity(req.currentUser, SYSTEM_USER)); + + // Source S3 Version to copy from + const sourceS3VersionId = await getS3VersionId(req.query.s3VersionId, addDashesToUuid(req.query.versionId), objId); + + const source = await storageService.headObject({ filePath: objPath, s3VersionId: sourceS3VersionId, bucketId }); + if (source.ContentLength > MAXCOPYOBJECTLENGTH) { + throw new Error('Cannot copy an object larger than 5GB'); + } + + // Generate object subset by subtracting/omitting defined keys via filter/inclusion + const keysToRemove = Object.keys({ ...getMetadata(req.headers) }); + let metadata = undefined; + if (keysToRemove.length) { + metadata = Object.fromEntries( + Object.entries(source.Metadata) + .filter(([key]) => !keysToRemove.includes(key)) + ); + } + + // get existing tags on source object + const sourceObject = await storageService.getObjectTagging({ + filePath: objPath, + s3VersionId: sourceS3VersionId, + bucketId: bucketId + }); + const sourceTags = Object.assign({}, + ...(sourceObject.TagSet?.map(item => ({ [item.Key]: item.Value })) ?? []) + ); + + const data = { + bucketId: bucketId, + copySource: objPath, + filePath: objPath, + metadata: metadata, + metadataDirective: MetadataDirective.REPLACE, + // copy existing tags from source object + tags: { + ...sourceTags, + 'coms-id': objId + }, + taggingDirective: TaggingDirective.REPLACE, + s3VersionId: sourceS3VersionId + }; + // create new version with metadata in S3 + const s3Response = await storageService.copyObject(data); + + await utils.trxWrapper(async (trx) => { + // create or update version in DB(if a non-versioned object) + const version = s3Response.VersionId ? + await versionService.copy(sourceS3VersionId, s3Response.VersionId, objId, s3Response.CopyObjectResult?.ETag, userId, trx) : + await versionService.update({ ...data, id: objId, etag: s3Response.CopyObjectResult?.ETag, isLatest: true }, userId, trx); + // add metadata to version in DB + await metadataService.associateMetadata(version.id, getKeyValue(data.metadata), userId, trx); + + // add tags to new version in DB + await tagService.associateTags(version.id, getKeyValue(data.tags), userId, trx); + }); + + res.status(204).end(); + } catch (e) { + next(errorToProblem(SERVICE, e)); + } + }, + + /** + * @function deleteObject + * Deletes the object + * @param {object} req Express request object + * @param {object} res Express response object + * @param {function} next The next callback function + * @returns {function} Express middleware function + */ + async deleteObject(req, res, next) { + try { + const objId = addDashesToUuid(req.params.objectId); + const userId = await userService.getCurrentUserId(getCurrentIdentity(req.currentUser, SYSTEM_USER)); + + // target S3 version to delete + const targetS3VersionId = await getS3VersionId(req.query.s3VersionId, addDashesToUuid(req.query.versionId), objId); + + const data = { + bucketId: req.currentObject?.bucketId, + filePath: req.currentObject?.path, + s3VersionId: targetS3VersionId + }; + + // delete version on S3 + const s3Response = await storageService.deleteObject(data); + + // if request is to delete a version + if (data.s3VersionId) { + // delete version in DB + await versionService.delete(objId, s3Response.VersionId); + // prune tags amd metadata + await metadataService.pruneOrphanedMetadata(); + await tagService.pruneOrphanedTags(); + // if no other versions in DB, delete object record + const remainingVersions = await versionService.list(objId); + if (remainingVersions.length === 0) await objectService.delete(objId); + } else { // else deleting the object + // if versioning enabled s3Response will contain DeleteMarker: true + if (s3Response.DeleteMarker) { + // create DeleteMarker version in DB + const deleteMarker = { + id: objId, + deleteMarker: true, + s3VersionId: s3Response.VersionId, + isLatest: true + }; + await versionService.create(deleteMarker, userId); + } else { // else object in bucket is not versioned + // delete object record from DB + await objectService.delete(objId); + // prune tags amd metadata + await metadataService.pruneOrphanedMetadata(); + await tagService.pruneOrphanedTags(); + } + } + + res.status(200).json(renameObjectProperty(s3Response, 'VersionId', 's3VersionId')); + } catch (e) { + next(errorToProblem(SERVICE, e)); + } + }, + + /** + * @function deleteTags + * Deletes the tag set on the requested object + * @param {object} req Express request object + * @param {object} res Express response object + * @param {function} next The next callback function + * @returns {function} Express middleware function + */ + async deleteTags(req, res, next) { + try { + const bucketId = req.currentObject?.bucketId; + const objId = addDashesToUuid(req.params.objectId); + const objPath = req.currentObject?.path; + + // Target S3 version + const targetS3VersionId = await getS3VersionId(req.query.s3VersionId, addDashesToUuid(req.query.versionId), objId); + + const sourceObject = await storageService.getObjectTagging({ filePath: objPath, s3VersionId: targetS3VersionId, bucketId: bucketId }); + + // Generate object subset by subtracting/omitting defined keys via filter/inclusion + const keysToRemove = req.query.tagset ? Object.keys(req.query.tagset) : []; + + let newTags = []; + if (keysToRemove.length && sourceObject.TagSet) { + newTags = sourceObject.TagSet.filter(x => !keysToRemove.includes(x.Key) && x.Key != 'coms-id'); + } + + const data = { + bucketId, + filePath: objPath, + tags: newTags.concat([{ 'Key': 'coms-id', 'Value': objId }]), + s3VersionId: targetS3VersionId + }; + + // Update tags for version in S3 + await storageService.putObjectTagging(data); + + // dissociate provided tags or all tags if no tagset passed in query parameter + await utils.trxWrapper(async (trx) => { + const version = await versionService.get({ s3VersionId: data.s3VersionId, objectId: objId }, trx); + + let dissociateTags = []; + if (req.query.tagset) { // remove specified tags + dissociateTags = getKeyValue(req.query.tagset); + } + else if (sourceObject.TagSet && sourceObject.TagSet.length) { // remove all existing except coms-id + dissociateTags = toLowerKeys(sourceObject.TagSet).filter(x => x.key != 'coms-id'); + } + + if (dissociateTags.length) await tagService.dissociateTags(version.id, dissociateTags, trx); + }); + + res.status(204).end(); + } catch (e) { + next(errorToProblem(SERVICE, e)); + } + }, + + /** + * @function fetchMetadata + * Fetch metadata for specific objects + * @param {object} req Express request object + * @param {object} res Express response object + * @param {function} next The next callback function + * @returns {function} Express middleware function + */ + async fetchMetadata(req, res, next) { + try { + const bucketIds = mixedQueryToArray(req.query.bucketId); + const objIds = mixedQueryToArray(req.query.objectId); + const metadata = getMetadata(req.headers); + const params = { + bucketIds: bucketIds ? bucketIds.map(id => addDashesToUuid(id)) : bucketIds, + objId: objIds ? objIds.map(id => addDashesToUuid(id)) : objIds, + metadata: metadata && Object.keys(metadata).length ? metadata : undefined + }; + // if scoping to current user permissions on objects + if (config.has('server.privacyMask')) { + params.userId = await userService.getCurrentUserId(getCurrentIdentity(req.currentUser, SYSTEM_USER)); + } + const response = await metadataService.fetchMetadataForObject(params); + res.status(200).json(response); + } catch (error) { + next(error); + } + }, + + /** + * @function fetchTags + * Fetch tags for specific objects + * @param {object} req Express request object + * @param {object} res Express response object + * @param {function} next The next callback function + * @returns {function} Express middleware function + */ + async fetchTags(req, res, next) { + try { + const bucketIds = mixedQueryToArray(req.query.bucketId); + const objIds = mixedQueryToArray(req.query.objectId); + const tagset = req.query.tagset; + const params = { + bucketIds: bucketIds ? bucketIds.map(id => addDashesToUuid(id)) : bucketIds, + objectIds: objIds ? objIds.map(id => addDashesToUuid(id)) : objIds, + tagset: tagset && Object.keys(tagset).length ? tagset : undefined, + }; + // if scoping to current user permissions on objects + if (config.has('server.privacyMask')) { + params.userId = await userService.getCurrentUserId(getCurrentIdentity(req.currentUser, SYSTEM_USER)); + } + const response = await tagService.fetchTagsForObject(params); + res.status(200).json(response); + } catch (error) { + next(error); + } + }, + + /** + * @function headObject + * Returns object headers + * @param {object} req Express request object + * @param {object} res Express response object + * @param {function} next The next callback function + * @returns {function} Express middleware function + */ + async headObject(req, res, next) { + try { + const objId = addDashesToUuid(req.params.objectId); + + // target S3 version + const targetS3VersionId = await getS3VersionId(req.query.s3VersionId, addDashesToUuid(req.query.versionId), objId); + + const data = { + bucketId: req.currentObject?.bucketId, + filePath: req.currentObject?.path, + s3VersionId: targetS3VersionId + }; + const response = await storageService.headObject(data); + + // TODO: Proper 304 caching logic (with If-Modified-Since header support) + // Consider looking around for express-based caching middleware + // Perhaps npm express-preconditions is sufficient? + + // Set Headers via CORS library + cors({ + exposedHeaders: controller._processS3Headers(response, res), + ...DEFAULTCORS + })(req, res, () => { }); + res.status(204).end(); + } catch (e) { + next(errorToProblem(SERVICE, e)); + } + }, + + /** + * @function listObjectVersion + * List all versions of the object + * @param {object} req Express request object + * @param {object} res Express response object + * @param {function} next The next callback function + * @returns {function} Express middleware function + */ + async listObjectVersion(req, res, next) { + try { + const objId = addDashesToUuid(req.params.objectId); + + const response = await versionService.list(objId); + // TODO: sync with current versions in S3 + // const s3Versions = await storageService.listObjectVersion(data); + + res.status(200).json(response); + } catch (e) { + next(errorToProblem(SERVICE, e)); + } + }, + + /** + * @function readObject + * Reads via streaming or returns a presigned URL for the object + * @param {object} req Express request object + * @param {object} res Express response object + * @param {function} next The next callback function + * @returns {function} Express middleware function + */ + async readObject(req, res, next) { + try { + const objId = addDashesToUuid(req.params.objectId); + + // target S3 version + const targetS3VersionId = await getS3VersionId(req.query.s3VersionId, addDashesToUuid(req.query.versionId), objId); + + const data = { + // TODO: use req.currentObject.bucketId + bucketId: await getBucketId(objId), + filePath: req.currentObject?.path, + s3VersionId: targetS3VersionId + }; + + // Download via service proxy + if (req.query.download && req.query.download === DownloadMode.PROXY) { + // TODO: Consider if we need a HEAD operation first before doing the actual read on large files for pre-flight caching behavior? + const response = await storageService.readObject(data); + + // Set Headers via CORS library + cors({ + exposedHeaders: controller._processS3Headers(response, res), + ...DEFAULTCORS + })(req, res, () => { }); + + // TODO: Proper 304 caching logic (with If-Modified-Since header support) + // Consider looking around for express-based caching middleware + // Perhaps npm express-preconditions is sufficient? + if (req.get('If-None-Match') === response.ETag) res.status(304).end(); + else { + response.Body.pipe(res); // Stream body content directly to response + res.status(200); + } + } else { + const signedUrl = await storageService.readSignedUrl({ + expiresIn: req.query.expiresIn, + ...data + }); + + // Present download url link + if (req.query.download && req.query.download === DownloadMode.URL) { + res.status(201).json(signedUrl); + // Download via HTTP redirect + } else { + res.status(302).set('Location', signedUrl).end(); + } + } + + } catch (e) { + next(errorToProblem(SERVICE, e)); + } + }, + + /** + * @function replaceMetadata + * Creates a new version of the object via copy with the new metadata replacing the previous + * @param {object} req Express request object + * @param {object} res Express response object + * @param {function} next The next callback function + * @returns {function} Express middleware function + */ + async replaceMetadata(req, res, next) { + try { + const bucketId = req.currentObject?.bucketId; + const objId = addDashesToUuid(req.params.objectId); + const objPath = req.currentObject?.path; + const userId = await userService.getCurrentUserId(getCurrentIdentity(req.currentUser, SYSTEM_USER)); + + // source S3 version + const sourceS3VersionId = await getS3VersionId(req.query.s3VersionId, addDashesToUuid(req.query.versionId), objId); + + // get metadata for source version + const source = await storageService.headObject({ filePath: objPath, s3VersionId: sourceS3VersionId, bucketId }); + if (source.ContentLength > MAXCOPYOBJECTLENGTH) { + throw new Error('Cannot copy an object larger than 5GB'); + } + + // get existing tags on source object + const sourceObject = await storageService.getObjectTagging({ + filePath: objPath, + s3VersionId: sourceS3VersionId, + bucketId: bucketId + }); + const sourceTags = Object.assign({}, + ...(sourceObject.TagSet?.map(item => ({ [item.Key]: item.Value })) ?? []) + ); + + const newMetadata = getMetadata(req.headers); + + const data = { + bucketId, + copySource: objPath, + filePath: objPath, + metadata: newMetadata, + metadataDirective: MetadataDirective.REPLACE, + // copy existing tags from source object + tags: { + ...sourceTags, + 'coms-id': objId + }, + taggingDirective: TaggingDirective.REPLACE, + s3VersionId: sourceS3VersionId + }; + const s3Response = await storageService.copyObject(data); + + await utils.trxWrapper(async (trx) => { + // create or update version (if a non-versioned object) + const version = s3Response.VersionId ? + await versionService.copy(sourceS3VersionId, s3Response.VersionId, objId, s3Response.CopyObjectResult?.ETag, userId, trx) : + + await versionService.update({ ...data, id: objId, etag: s3Response.CopyObjectResult?.ETag, isLatest: true }, userId, trx); + + // add metadata + await metadataService.associateMetadata(version.id, getKeyValue(data.metadata), userId, trx); + + // add tags to new version in DB + await tagService.associateTags(version.id, getKeyValue(data.tags), userId, trx); + }); + + res.status(204).end(); + } catch (e) { + next(errorToProblem(SERVICE, e)); + } + }, + + /** + * @function replaceTags + * Replaces the tag set on the requested object + * @param {object} req Express request object + * @param {object} res Express response object + * @param {function} next The next callback function + * @returns {function} Express middleware function + */ + async replaceTags(req, res, next) { + try { + const objId = addDashesToUuid(req.params.objectId); + const objPath = req.currentObject?.path; + const userId = await userService.getCurrentUserId(getCurrentIdentity(req.currentUser, SYSTEM_USER)); + // format new tags to array of objects + const newTags = Object.entries({ ...req.query.tagset }).map(([k, v]) => ({ Key: k, Value: v })); + + // source S3 version + const sourceS3VersionId = await getS3VersionId(req.query.s3VersionId, addDashesToUuid(req.query.versionId), objId); + + const data = { + bucketId: req.currentObject?.bucketId, + filePath: objPath, + tags: newTags.concat([{ 'Key': 'coms-id', 'Value': objId }]), + s3VersionId: sourceS3VersionId + }; + + // Add tags to the object in S3 + await storageService.putObjectTagging(data); + + // update tags on version in DB + await utils.trxWrapper(async (trx) => { + const version = await versionService.get({ s3VersionId: data.s3VersionId, objectId: objId }, trx); + await tagService.replaceTags(version.id, toLowerKeys(data.tags), userId, trx); + }); + + res.status(204).end(); + } catch (e) { + next(errorToProblem(SERVICE, e)); + } + }, + + /** + * @function searchObjects + * Search and filter for specific objects + * @param {object} req Express request object + * @param {object} res Express response object + * @param {function} next The next callback function + * @returns {function} Express middleware function + */ + async searchObjects(req, res, next) { + // TODO: Consider support for filtering by set of permissions? + // TODO: handle additional parameters. Eg: deleteMarker, latest + try { + const bucketIds = mixedQueryToArray(req.query.bucketId); + const objIds = mixedQueryToArray(req.query.objectId); + const metadata = getMetadata(req.headers); + const tagging = req.query.tagset; + const params = { + id: objIds ? objIds.map(id => addDashesToUuid(id)) : objIds, + bucketId: bucketIds ? bucketIds.map(id => addDashesToUuid(id)) : bucketIds, + name: req.query.name, + path: req.query.path, + mimeType: req.query.mimeType, + metadata: metadata && Object.keys(metadata).length ? metadata : undefined, + tag: tagging && Object.keys(tagging).length ? tagging : undefined, + public: isTruthy(req.query.public), + active: isTruthy(req.query.active), + deleteMarker: isTruthy(req.query.deleteMarker), + latest: isTruthy(req.query.latest) + }; + // if scoping to current user permissions on objects + if (config.has('server.privacyMask')) { + params.userId = await userService.getCurrentUserId(getCurrentIdentity(req.currentUser, SYSTEM_USER)); + } + const response = await objectService.searchObjects(params); + res.status(200).json(response); + } catch (error) { + next(error); + } + }, + + /** + * @function togglePublic + * Sets the public flag of an object + * @param {object} req Express request object + * @param {object} res Express response object + * @param {function} next The next callback function + * @returns {function} Express middleware function + */ + async togglePublic(req, res, next) { + try { + const userId = await userService.getCurrentUserId(getCurrentIdentity(req.currentUser, SYSTEM_USER), SYSTEM_USER); + const data = { + id: addDashesToUuid(req.params.objectId), + public: isTruthy(req.query.public) ?? false, + userId: userId + }; + + const response = await objectService.update(data); + + res.status(200).json(response); + } catch (e) { + next(errorToProblem(SERVICE, e)); + } + }, + + /** + * @function updateObject + * Creates an updated version of the object via streaming + * @param {object} req Express request object + * @param {object} res Express response object + * @param {function} next The next callback function + * @returns {function} Express middleware function + */ + async updateObject(req, res, next) { + try { + const userId = await userService.getCurrentUserId(getCurrentIdentity(req.currentUser, SYSTEM_USER)); + + // Preflight existence check for bucketId + const bucketId = req.currentObject?.bucketId; + const { key: bucketKey } = await getBucket(bucketId); + + const filename = req.currentObject?.path.match(/(?!.*\/)(.*)$/)[0]; + const objId = addDashesToUuid(req.params.objectId); + const data = { + id: objId, + bucketId: bucketId, + length: req.currentUpload.contentLength, + name: filename, + mimeType: req.currentUpload.mimeType, + metadata: getMetadata(req.headers), + tags: { + ...req.query.tagset, + 'coms-id': objId // Enforce `coms-id:` tag + } + }; + + let s3Response; + try { + // Preflight S3 Object check + const headResponse = await storageService.headObject({ + filePath: joinPath(bucketKey, filename), + bucketId: bucketId + }); + + // Skip upload in the unlikely event we get an unexpected response from headObject + if (headResponse.$metadata?.httpStatusCode !== 200) { + throw new Problem(502, { + detail: 'Bucket communication error', + instance: req.originalUrl + }); + } + + // Object exists on bucket + if (req.currentUpload.contentLength < MAXCOPYOBJECTLENGTH) { + log.debug('Uploading with putObject', { + contentLength: req.currentUpload.contentLength, + function: 'createObject', + uploadMethod: 'putObject' + }); + s3Response = await storageService.putObject({ ...data, stream: req }); + } else if (req.currentUpload.contentLength < MAXFILEOBJECTLENGTH) { + log.debug('Uploading with lib-storage', { + contentLength: req.currentUpload.contentLength, + function: 'createObject', + uploadMethod: 'lib-storage' + }); + s3Response = await storageService.upload({ ...data, stream: req }); + } else { + throw new Problem(413, { + detail: 'File exceeds maximum 50GB limit', + instance: req.originalUrl + }); + } + } catch (err) { + if (err instanceof Problem) throw err; // Rethrow Problem type errors + else if (err.$metadata?.httpStatusCode !== 404) { + // An unexpected response from headObject + throw new Problem(502, { + detail: 'Bucket communication error', + instance: req.originalUrl + }); + } else { + if (err.$response?.headers['x-amz-delete-marker']) { + // Object is soft deleted from the bucket + throw new Problem(409, { + detail: 'Unable to update soft deleted object', + instance: req.originalUrl + }); + } else { + // Bucket is missing the existing object + throw new Problem(409, { + detail: 'Bucket does not contain existing object', + instance: req.originalUrl + }); + } + // TODO: Add in sync operation to update object record in COMS DB? + } + } + + const dbResponse = await utils.trxWrapper(async (trx) => { + // Update Object + const object = await objectService.update({ + ...data, + userId: userId, + path: joinPath(bucketKey, filename) + }, trx); + + // Update Version + let version = undefined; + if (s3Response.VersionId) { // Create new version if bucket versioning enabled + const s3VersionId = s3Response.VersionId; + version = await versionService.create({ + ...data, + etag: s3Response.ETag, + s3VersionId: s3VersionId, + isLatest: true + }, userId, trx); + } else { // Update existing version when bucket versioning not enabled + version = await versionService.update({ + ...data, + s3VersionId: null, + etag: s3Response.ETag + }, userId, trx); + } + object.versionId = version.id; + + // Update Metadata + if (data.metadata && Object.keys(data.metadata).length) await metadataService.associateMetadata(version.id, getKeyValue(data.metadata), userId, trx); + + // Update Tags + if (data.tags && Object.keys(data.tags).length) await tagService.replaceTags(version.id, getKeyValue(data.tags), userId, trx); + + return object; + }); + + res.status(200).json({ + ...data, + ...dbResponse, + ...renameObjectProperty(s3Response, 'VersionId', 's3VersionId') + }); + } catch (e) { + next(errorToProblem(SERVICE, e)); + } + } +}; + +module.exports = controller; diff --git a/comsapi/app/src/controllers/objectPermission.js b/comsapi/app/src/controllers/objectPermission.js new file mode 100644 index 00000000..dcb30a6c --- /dev/null +++ b/comsapi/app/src/controllers/objectPermission.js @@ -0,0 +1,120 @@ +const { NIL: SYSTEM_USER } = require('uuid'); +const errorToProblem = require('../components/errorToProblem'); +const utils = require('../components/utils'); + +const { objectPermissionService, userService } = require('../services'); + +const SERVICE = 'ObjectPermissionService'; + +/** + * The Permission Controller + */ +const controller = { + /** + * @function searchPermissions + * Searches for object permissions + * @param {object} req Express request object + * @param {object} res Express response object + * @param {function} next The next callback function + * @returns {function} Express middleware function + */ + async searchPermissions(req, res, next) { + try { + const bucketIds = utils.mixedQueryToArray(req.query.bucketId); + const objIds = utils.mixedQueryToArray(req.query.objectId); + const permCodes = utils.mixedQueryToArray(req.query.permCode); + const userIds = utils.mixedQueryToArray(req.query.userId); + const result = await objectPermissionService.searchPermissions({ + bucketId: bucketIds ? bucketIds.map(id => utils.addDashesToUuid(id)) : bucketIds, + objId: objIds ? objIds.map(id => utils.addDashesToUuid(id)) : objIds, + userId: userIds ? userIds.map(id => utils.addDashesToUuid(id)) : userIds, + permCode: permCodes + }); + const response = utils.groupByObject('objectId', 'permissions', result); + + // if also returning inheritied permissions + if (utils.isTruthy(req.query.bucketPerms)) { + const objectIds = await objectPermissionService.listInheritedObjectIds(userIds, bucketIds, permCodes); + + // merge list of object permissions + objectIds.forEach(objectId => { + if (!response.map(r => r.objectId).includes(objectId) && + // limit to objectId request query parameter if given + (!objIds?.length || objIds?.includes(objectId))) { + response.push({ + objectId: objectId, + permissions: [] + }); + } + }); + } + + res.status(200).json(response); + } catch (e) { + next(errorToProblem(SERVICE, e)); + } + }, + + /** + * @function listPermissions + * Returns the object permissions + * @param {object} req Express request object + * @param {object} res Express response object + * @param {function} next The next callback function + * @returns {function} Express middleware function + */ + async listPermissions(req, res, next) { + try { + const userIds = utils.mixedQueryToArray(req.query.userId); + const response = await objectPermissionService.searchPermissions({ + objId: utils.addDashesToUuid(req.params.objectId), + userId: userIds ? userIds.map(id => utils.addDashesToUuid(id)) : userIds, + permCode: utils.mixedQueryToArray(req.query.permCode) + }); + res.status(200).json(response); + } catch (e) { + next(errorToProblem(SERVICE, e)); + } + }, + + /** + * @function addPermissions + * Grants object permissions to users + * @param {object} req Express request object + * @param {object} res Express response object + * @param {function} next The next callback function + * @returns {function} Express middleware function + */ + async addPermissions(req, res, next) { + try { + const userId = await userService.getCurrentUserId(utils.getCurrentIdentity(req.currentUser, SYSTEM_USER)); + const response = await objectPermissionService.addPermissions(utils.addDashesToUuid(req.params.objectId), req.body, userId); + res.status(201).json(response); + } catch (e) { + next(errorToProblem(SERVICE, e)); + } + }, + + /** + * @function removePermissions + * Deletes object permissions for a user + * @param {object} req Express request object + * @param {object} res Express response object + * @param {function} next The next callback function + * @returns {function} Express middleware function + */ + async removePermissions(req, res, next) { + try { + const userArray = utils.mixedQueryToArray(req.query.userId); + const userIds = userArray ? userArray.map(id => utils.addDashesToUuid(id)) : userArray; + const permissions = utils.mixedQueryToArray(req.query.permCode); + const response = await objectPermissionService.removePermissions(req.params.objectId, userIds, permissions); + res.status(200).json(response); + } catch (e) { + next(errorToProblem(SERVICE, e)); + } + }, + +}; + +module.exports = controller; diff --git a/comsapi/app/src/controllers/sync.js b/comsapi/app/src/controllers/sync.js new file mode 100644 index 00000000..6ce4d7ca --- /dev/null +++ b/comsapi/app/src/controllers/sync.js @@ -0,0 +1,82 @@ +const errorToProblem = require('../components/errorToProblem'); +const { addDashesToUuid } = require('../components/utils'); +const { objectService, storageService, objectQueueService } = require('../services'); + +const SERVICE = 'ObjectQueueService'; + +/** + * The Sync Controller + */ +const controller = { + /** + * @function syncBucket + * Synchronizes a bucket + * @param {object} req Express request object + * @param {object} res Express response object + * @param {function} next The next callback function + * @returns {function} Express middleware function + */ + async syncBucket(req, res, next) { + try { + // TODO: Consider adding an "all" mode for checking through all known objects and buckets for job enumeration + // const allMode = isTruthy(req.query.all); + const bucketId = addDashesToUuid(req.params.bucketId); + + const [dbResponse, s3Response] = await Promise.all([ + objectService.searchObjects({ bucketId: bucketId }), + storageService.listAllObjectVersions({ bucketId: bucketId, filterLatest: true }) + ]); + + // Aggregate and dedupe all file paths to consider + const jobs = [...new Set([ + ...dbResponse.map(object => object.path), + ...s3Response.DeleteMarkers.map(object => object.Key), + ...s3Response.Versions.map(object => object.Key) + ])].map(path => ({ path: path, bucketId: bucketId })); + + const response = await objectQueueService.enqueue({ jobs: jobs }); + res.status(202).json(response); + } catch (e) { + next(errorToProblem(SERVICE, e)); + } + }, + + /** + * @function syncObject + * Synchronizes an object + * @param {object} req Express request object + * @param {object} res Express response object + * @param {function} next The next callback function + * @returns {function} Express middleware function + */ + async syncObject(req, res, next) { + try { + const bucketId = req.currentObject?.bucketId; + const path = req.currentObject?.path; + + const response = await objectQueueService.enqueue({ jobs: [{ path: path, bucketId: bucketId }] }); + res.status(202).json(response); + } catch (e) { + next(errorToProblem(SERVICE, e)); + } + }, + + /** + * @function syncStatus + * Reports on current sync queue size + * @param {object} req Express request object + * @param {object} res Express response object + * @param {function} next The next callback function + * @returns {function} Express middleware function + */ + async syncStatus(_req, res, next) { + try { + const response = await objectQueueService.queueSize(); + res.status(200).json(response); + } catch (e) { + next(errorToProblem(SERVICE, e)); + } + } +}; + +module.exports = controller; diff --git a/comsapi/app/src/controllers/tag.js b/comsapi/app/src/controllers/tag.js new file mode 100644 index 00000000..fb8d6537 --- /dev/null +++ b/comsapi/app/src/controllers/tag.js @@ -0,0 +1,37 @@ +const config = require('config'); +const errorToProblem = require('../components/errorToProblem'); +const { tagService } = require('../services'); + +const SERVICE = 'TagService'; + +/** + * The Tag Controller + */ +const controller = { + + /** + * @function searchTags + * Search and filter for specific tags + * @param {object} req Express request object + * @param {object} res Express response object + * @param {function} next The next callback function + * @returns {function} Express middleware function + */ + async searchTags(req, res, next) { + try { + const tagging = req.query.tagset; + const params = { + tag: tagging && Object.keys(tagging).length ? tagging : undefined, + privacyMask : req.currentUser.authType !== 'BASIC' ? config.has('server.privacyMask') : false + }; + + const response = await tagService.searchTags(params); + res.status(200).json(response); + } catch (e) { + next(errorToProblem(SERVICE, e)); + } + }, + +}; + +module.exports = controller; diff --git a/comsapi/app/src/controllers/user.js b/comsapi/app/src/controllers/user.js new file mode 100644 index 00000000..c40733ea --- /dev/null +++ b/comsapi/app/src/controllers/user.js @@ -0,0 +1,60 @@ +const errorToProblem = require('../components/errorToProblem'); +const { addDashesToUuid, isTruthy, mixedQueryToArray } = require('../components/utils'); +const { userService } = require('../services'); + +const SERVICE = 'UserService'; + +/** + * The User Controller + */ +const controller = { + /** + * @function listIdps + * Lists all known identity providers + * @param {object} req Express request object + * @param {object} res Express response object + * @param {function} next The next callback function + * @returns {function} Express middleware function + */ + async listIdps(req, res, next) { + try { + const response = await userService.listIdps({ + active: isTruthy(req.query.active) + }); + res.status(200).json(response); + } catch (e) { + next(errorToProblem(SERVICE, e)); + } + }, + + /** + * @function searchUsers + * Searches for users + * @param {object} req Express request object + * @param {object} res Express response object + * @param {function} next The next callback function + * @returns {function} Express middleware function + */ + async searchUsers(req, res, next) { + try { + const userIds = mixedQueryToArray(req.query.userId); + const response = await userService.searchUsers({ + userId: userIds ? userIds.map(id => addDashesToUuid(id)) : userIds, + identityId: mixedQueryToArray(req.query.identityId), + idp: mixedQueryToArray(req.query.idp), + username: req.query.username, + email: req.query.email, + firstName: req.query.firstName, + fullName: req.query.fullName, + lastName: req.query.lastName, + active: isTruthy(req.query.active), + search: req.query.search + }); + res.status(200).json(response); + } catch (e) { + next(errorToProblem(SERVICE, e)); + } + } +}; + +module.exports = controller; diff --git a/comsapi/app/src/controllers/version.js b/comsapi/app/src/controllers/version.js new file mode 100644 index 00000000..271dd93e --- /dev/null +++ b/comsapi/app/src/controllers/version.js @@ -0,0 +1,75 @@ +const config = require('config'); +const { NIL: SYSTEM_USER } = require('uuid'); +const errorToProblem = require('../components/errorToProblem'); +const { addDashesToUuid, getCurrentIdentity, getMetadata, mixedQueryToArray } = require('../components/utils'); +const { metadataService, tagService, userService } = require('../services'); + +const SERVICE = 'VersionService'; + +/** + * The Version Controller + */ +const controller = { + /** + * @function fetchMetadata + * Lists metadata for an array of versions + * @param {object} req Express request object + * @param {object} res Express response object + * @param {function} next The next callback function + * @returns {function} Express middleware function + */ + async fetchMetadata(req, res, next) { + try { + const versionIds = mixedQueryToArray(req.query.versionId); + const s3VersionIds = mixedQueryToArray(req.query.s3VersionId); + const metadata = getMetadata(req.headers); + + const params = { + versionIds: versionIds ? versionIds.map(id => addDashesToUuid(id)) : versionIds, + s3VersionIds: s3VersionIds ? s3VersionIds.map(id => id.toString()) : s3VersionIds, + metadata: metadata && Object.keys(metadata).length ? metadata : undefined, + }; + // if scoping to current user permissions on objects + if (config.has('server.privacyMask')) { + params.userId = await userService.getCurrentUserId(getCurrentIdentity(req.currentUser, SYSTEM_USER)); + } + const response = await metadataService.fetchMetadataForVersion(params); + res.status(200).json(response); + } catch (e) { + next(errorToProblem(SERVICE, e)); + } + }, + + /** + * @function fetchTags + * Lists tags for an array of versions + * @param {object} req Express request object + * @param {object} res Express response object + * @param {function} next The next callback function + * @returns {function} Express middleware function + */ + async fetchTags(req, res, next) { + try { + const versionIds = mixedQueryToArray(req.query.versionId); + const s3VersionIds = mixedQueryToArray(req.query.s3VersionId); + const tagging = req.query.tagset; + + const params = { + versionIds: versionIds ? versionIds.map(id => addDashesToUuid(id)) : versionIds, + s3VersionIds: s3VersionIds ? s3VersionIds.map(id => id.toString()) : s3VersionIds, + tags: tagging && Object.keys(tagging).length ? tagging : undefined, + }; + // if scoping to current user permissions on objects + if (config.has('server.privacyMask')) { + params.userId = await userService.getCurrentUserId(getCurrentIdentity(req.currentUser, SYSTEM_USER)); + } + const response = await tagService.fetchTagsForVersion(params); + res.status(200).json(response); + } catch (e) { + next(errorToProblem(SERVICE, e)); + } + }, + +}; + +module.exports = controller; diff --git a/comsapi/app/src/db/.editorconfig b/comsapi/app/src/db/.editorconfig new file mode 100644 index 00000000..fedd3a8c --- /dev/null +++ b/comsapi/app/src/db/.editorconfig @@ -0,0 +1,2 @@ +[*.js] +indent_size = unset diff --git a/comsapi/app/src/db/dataConnection.js b/comsapi/app/src/db/dataConnection.js new file mode 100644 index 00000000..8be34667 --- /dev/null +++ b/comsapi/app/src/db/dataConnection.js @@ -0,0 +1,170 @@ +const Knex = require('knex'); +const { Model } = require('objection'); + +const { searchPath: schemas } = require('../../knexfile'); +const log = require('../components/log')(module.filename); +const models = require('./models'); + +class DataConnection { + /** + * Creates a new DataConnection with default (Postgresql) Knex configuration. + * @class + */ + constructor() { + if (!DataConnection.instance) { + const knexfile = require('../../knexfile'); + this.knex = Knex(knexfile); + DataConnection.instance = this; + } + + return DataConnection.instance; + } + + /** + * @function connected + * True or false if connected. + */ + get connected() { + return this._connected; + } + + /** + * @function knex + * Gets the current knex binding + */ + get knex() { + return this._knex; + } + + /** + * @function knex + * Sets the current knex binding and forwards to Objection model + * @param {object} v - a Knex object. + */ + set knex(v) { + this._knex = v; + this._connected = false; + Model.knex(this.knex); + } + + /** + * @function checkAll + * Checks the Knex connection, the database schema, and Objection models + * @returns {boolean} True if successful, otherwise false + */ + async checkAll() { + const modelsOk = !!this.knex; + const [connectOk, schemaOk] = await Promise.all([ + this.checkConnection(), + this.checkSchema() + ]); + this._connected = connectOk && schemaOk && modelsOk; + log.verbose(`Connect OK: ${connectOk}, Schema OK: ${schemaOk}, Models OK: ${modelsOk}`, { function: 'checkAll' }); + + if (!connectOk) { + log.error('Could not connect to the database, check configuration and ensure database server is running', { function: 'checkAll' }); + } + if (!schemaOk) { + log.error('Connected to the database, could not verify the schema. Ensure proper migrations have been run.', { function: 'checkAll' }); + } + if (!modelsOk) { + log.error('Connected to the database, schema is ok, could not initialize Knex Models.', { function: 'checkAll' }); + } + + return this._connected; + } + + /** + * @function checkConnection + * Checks the current knex connection to Postgres + * If the connected DB is in read-only mode, transaction_read_only will not be off + * @returns {boolean} True if successful, otherwise false + */ + async checkConnection() { + try { + const data = await this.knex.raw('show transaction_read_only'); + const result = data?.rows[0]?.transaction_read_only === 'off'; + if (result) { + log.debug('Database connection ok', { function: 'checkConnection' }); + } else { + log.warn('Database connection is read-only', { function: 'checkConnection' }); + } + this._connected = result; + return result; + } catch (err) { + log.error(`Error with database connection: ${err.message}`, { function: 'checkConnection' }); + this._connected = false; + return false; + } + } + + /** + * @function checkSchema + * Queries the knex connection to check for the existence of the expected schema tables + * @returns {boolean} True if schema is ok, otherwise false + */ + checkSchema() { + try { + const tables = Object.values(models).map(model => model.tableName); + return Promise + .all(tables.map(table => Promise + .all(schemas.map(schema => this._knex.schema.withSchema(schema).hasTable(table))))) + .then(exists => exists.every(table => table.some(exist => exist))) + .then(result => { + if (result) log.debug('Database schema ok', { function: 'checkSchema' }); + return result; + }); + } catch (err) { + log.error(`Error with database schema: ${err.message}`, { function: 'checkSchema' }); + log.error(err); + return false; + } + } + + /** + * @function checkModel + * Attaches the Objection model to the existing knex connection + * @returns {boolean} True if successful, otherwise false + */ + checkModel() { + try { + Model.knex(this.knex); + log.debug('Database models ok', { function: 'checkModel' }); + return true; + } catch (err) { + log.error(`Error attaching Model to connection: ${err.message}`, { function: 'checkModel' }); + log.error(err); + return false; + } + } + + /** + * @function close + * Will close the DataConnection + * @param {function} [cb] Optional callback + */ + close(cb = undefined) { + if (this.knex) { + this.knex.destroy(() => { + this.knex = undefined; + log.info('Disconnected', { function: 'close' }); + if (cb) cb(); + }); + } else if (cb) cb(); + } + + /** + * @function resetConnection + * Invalidates and reconnects existing knex connection + */ + resetConnection() { + if (this.knex) { + log.warn('Attempting to reset database connection pool', { function: 'resetConnection' }); + this.knex.destroy(() => { + this.knex.initialize(); + }); + } + } +} + +module.exports = DataConnection; diff --git a/comsapi/app/src/db/migrations/20220130133615_000-init.js b/comsapi/app/src/db/migrations/20220130133615_000-init.js new file mode 100644 index 00000000..87c70b91 --- /dev/null +++ b/comsapi/app/src/db/migrations/20220130133615_000-init.js @@ -0,0 +1,169 @@ +const stamps = require('../stamps'); +const { NIL: SYSTEM_USER } = require('uuid'); + +exports.up = function (knex) { + return Promise.resolve() + // Create public schema COMS tables + .then(() => knex.schema.createTable('identity_provider', table => { + table.string('idp', 255).primary(); + table.boolean('active').notNullable().defaultTo(true); + stamps(knex, table); + })) + + .then(() => knex.schema.createTable('user', table => { + table.uuid('userId').primary(); + table.uuid('identityId').index(); + table.string('idp', 255).references('idp').inTable('identity_provider').onUpdate('CASCADE').onDelete('CASCADE'); + table.string('username', 255).notNullable().index(); + table.string('email', 255).index(); + table.string('firstName', 255); + table.string('fullName', 255); + table.string('lastName', 255); + table.boolean('active').notNullable().defaultTo(true); + stamps(knex, table); + })) + + .then(() => knex.schema.createTable('permission', table => { + table.string('permCode', 255).primary(); + table.boolean('active').notNullable().defaultTo(true); + stamps(knex, table); + })) + + .then(() => knex.schema.createTable('object', table => { + table.uuid('id').primary(); + table.string('originalName', 255).notNullable(); + table.string('path', 1024).notNullable(); + table.string('mimeType', 255).notNullable(); + table.boolean('public').notNullable().defaultTo(false); + table.boolean('active').notNullable().defaultTo(true); + stamps(knex, table); + })) + + .then(() => knex.schema.createTable('object_permission', table => { + table.uuid('id').primary(); + table.uuid('objectId').references('id').inTable('object').notNullable().onUpdate('CASCADE').onDelete('CASCADE'); + table.uuid('userId').references('userId').inTable('user').notNullable().onUpdate('CASCADE').onDelete('CASCADE'); + table.string('permCode').references('permCode').inTable('permission').notNullable().onUpdate('CASCADE').onDelete('CASCADE'); + stamps(knex, table); + })) + + // Create audit schema and logged_actions table + .then(() => knex.schema.raw('CREATE SCHEMA IF NOT EXISTS audit')) + + .then(() => knex.schema.withSchema('audit').createTable('logged_actions', table => { + table.specificType( + 'id', + 'integer GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY' + ); + table.string('schemaName', 255).notNullable().index(); + table.string('tableName', 255).notNullable().index(); + table.string('dbUser', 255).notNullable(); + table.string('updatedByUsername', 255); + table.timestamp('actionTimestamp', { useTz: true }).defaultTo(knex.fn.now()).index(); + table.string('action', 255).notNullable().index(); + table.json('originalData'); + table.json('newData'); + })) + + .then(() => knex.schema.raw(`CREATE OR REPLACE FUNCTION audit.if_modified_func() RETURNS trigger AS $body$ + DECLARE + v_old_data json; + v_new_data json; + + BEGIN + if (TG_OP = 'UPDATE') then + v_old_data := row_to_json(OLD); + v_new_data := row_to_json(NEW); + insert into audit.logged_actions ("schemaName", "tableName", "dbUser", "updatedByUsername", "actionTimestamp", "action", "originalData", "newData") + values (TG_TABLE_SCHEMA::TEXT, TG_TABLE_NAME::TEXT, SESSION_USER::TEXT, NEW."updatedBy", now(), TG_OP::TEXT, v_old_data, v_new_data); + RETURN NEW; + elsif (TG_OP = 'DELETE') then + v_old_data := row_to_json(OLD); + insert into audit.logged_actions ("schemaName", "tableName", "dbUser", "actionTimestamp", "action", "originalData") + values (TG_TABLE_SCHEMA::TEXT, TG_TABLE_NAME::TEXT, SESSION_USER::TEXT, now(), TG_OP::TEXT, v_old_data); + RETURN OLD; + else + RAISE WARNING '[AUDIT.IF_MODIFIED_FUNC] - Other action occurred: %, at %', TG_OP, now(); + RETURN NULL; + end if; + + EXCEPTION + WHEN data_exception THEN + RAISE WARNING '[AUDIT.IF_MODIFIED_FUNC] - UDF ERROR [DATA EXCEPTION] - SQLSTATE: %, SQLERRM: %',SQLSTATE,SQLERRM; + RETURN NULL; + WHEN unique_violation THEN + RAISE WARNING '[AUDIT.IF_MODIFIED_FUNC] - UDF ERROR [UNIQUE] - SQLSTATE: %, SQLERRM: %',SQLSTATE,SQLERRM; + RETURN NULL; + WHEN others THEN + RAISE WARNING '[AUDIT.IF_MODIFIED_FUNC] - UDF ERROR [OTHER] - SQLSTATE: %, SQLERRM: %',SQLSTATE,SQLERRM; + RETURN NULL; + + END; + $body$ + LANGUAGE plpgsql + SECURITY DEFINER + SET search_path = pg_catalog, audit;`)) + + // Create audit triggers + .then(() => knex.schema.raw(`CREATE TRIGGER audit_identity_provider_trigger + AFTER UPDATE OR DELETE ON identity_provider + FOR EACH ROW EXECUTE PROCEDURE audit.if_modified_func();`)) + + .then(() => knex.schema.raw(`CREATE TRIGGER audit_user_trigger + AFTER UPDATE OR DELETE ON "user" + FOR EACH ROW EXECUTE PROCEDURE audit.if_modified_func();`)) + + .then(() => knex.schema.raw(`CREATE TRIGGER audit_permission_trigger + AFTER UPDATE OR DELETE ON permission + FOR EACH ROW EXECUTE PROCEDURE audit.if_modified_func();`)) + + .then(() => knex.schema.raw(`CREATE TRIGGER audit_object_trigger + AFTER UPDATE OR DELETE ON object + FOR EACH ROW EXECUTE PROCEDURE audit.if_modified_func();`)) + + .then(() => knex.schema.raw(`CREATE TRIGGER audit_object_permission_trigger + AFTER UPDATE OR DELETE ON object_permission + FOR EACH ROW EXECUTE PROCEDURE audit.if_modified_func();`)) + + // Populate Baseline Data + .then(() => { + const users = ['system']; + const items = users.map((user) => ({ + userId: SYSTEM_USER, + username: user, + active: true, + createdBy: SYSTEM_USER, + })); + return knex('user').insert(items); + }) + + .then(() => { + const perms = ['CREATE', 'READ', 'UPDATE', 'DELETE', 'MANAGE' ]; + const items = perms.map((perm) => ({ + permCode: perm, + active: true, + createdBy: SYSTEM_USER, + })); + return knex('permission').insert(items); + }); +}; + +exports.down = function (knex) { + return Promise.resolve() + // Drop audit triggers + .then(() => knex.schema.raw('DROP TRIGGER IF EXISTS audit_object_permission_trigger ON object_permission')) + .then(() => knex.schema.raw('DROP TRIGGER IF EXISTS audit_object_trigger ON object')) + .then(() => knex.schema.raw('DROP TRIGGER IF EXISTS audit_permission_trigger ON permission')) + .then(() => knex.schema.raw('DROP TRIGGER IF EXISTS audit_user_trigger ON "user"')) + .then(() => knex.schema.raw('DROP TRIGGER IF EXISTS audit_identity_provider_trigger ON identity_provider')) + // Drop audit schema and logged_actions table + .then(() => knex.schema.raw('DROP FUNCTION IF EXISTS audit.if_modified_func()')) + .then(() => knex.schema.withSchema('audit').dropTableIfExists('logged_actions')) + .then(() => knex.schema.dropSchemaIfExists('audit')) + // Drop public schema COMS tables + .then(() => knex.schema.dropTableIfExists('object_permission')) + .then(() => knex.schema.dropTableIfExists('object')) + .then(() => knex.schema.dropTableIfExists('permission')) + .then(() => knex.schema.dropTableIfExists('user')) + .then(() => knex.schema.dropTableIfExists('identity_provider')); +}; diff --git a/comsapi/app/src/db/migrations/20220516000000_001-version.js b/comsapi/app/src/db/migrations/20220516000000_001-version.js new file mode 100644 index 00000000..345cdaf5 --- /dev/null +++ b/comsapi/app/src/db/migrations/20220516000000_001-version.js @@ -0,0 +1,90 @@ +const stamps = require('../stamps'); +const { v4: uuidv4, NIL: SYSTEM_USER } = require('uuid'); + +exports.up = function (knex) { + return Promise.resolve() + // create version table + .then(() => knex.schema.createTable('version', table => { + table.uuid('id').primary(); + table.string('versionId', 1024); + table.uuid('objectId').references('id').inTable('object').notNullable().onUpdate('CASCADE').onDelete('CASCADE'); + table.string('originalName', 255); + table.string('mimeType', 255); + table.boolean('deleteMarker').notNullable().defaultTo(false); + stamps(knex, table); + })) + + // Create audit trigger + .then(() => knex.schema.raw(`CREATE TRIGGER audit_version_trigger + AFTER UPDATE OR DELETE ON version + FOR EACH ROW EXECUTE PROCEDURE audit.if_modified_func();`)) + + // create a 'default' version for each object and move mimeType and originalName values + .then(() => knex('object')) + .then((rows) => { + const versions = rows.map(o => ({ + id: uuidv4(), + objectId: o.id, + originalName: o.originalName, + mimeType: o.mimeType, + createdBy: SYSTEM_USER, + createdAt: o.createdAt, + updatedAt: o.updatedAt + })); + + return versions.length ? knex('version').insert(versions) : Promise.resolve(); + }) + + // remove columns originalName and mimeType from object table + .then(() => knex.schema.alterTable('object', table => { + table.dropColumn('originalName'); + table.dropColumn('mimeType'); + })); +}; + +exports.down = function (knex) { + return Promise.resolve() + // re-add columns originalName and mimeType to object table + .then(() => knex.schema.alterTable('object', table => { + table.string('originalName', 255); + table.string('mimeType', 255); + })) + + // move originalName and mimeType values back to object table + .then(() => knex.select('*') + .distinctOn('objectId') + .from('version') + .orderBy([ + { column: 'objectId' }, + { column: 'createdAt', order: 'desc' } + ]) + ) + .then((rows) => { + const data = rows.map(row => ({ + id: row.objectId, + originalName: row.originalName, + mimeType: row.mimeType, + createdAt: row.createdAt, + updatedAt: row.updatedAt + })); + + if (data && data.length) { + return Promise.all(data.map((row) => { + return knex('object') + .where({ 'id': row.id }) + .update({ + 'originalName': row.originalName, + 'mimeType': row.mimeType, + 'createdAt': row.createdAt, + 'updatedAt': row.updatedAt + }); + })); + } + }) + + // Drop audit trigger + .then(() => knex.schema.raw('DROP TRIGGER IF EXISTS audit_version_trigger ON version')) + + // Drop tables + .then(() => knex.schema.dropTableIfExists('version')); +}; diff --git a/comsapi/app/src/db/migrations/20220627000000_002-metadata-tags.js b/comsapi/app/src/db/migrations/20220627000000_002-metadata-tags.js new file mode 100644 index 00000000..e95add78 --- /dev/null +++ b/comsapi/app/src/db/migrations/20220627000000_002-metadata-tags.js @@ -0,0 +1,134 @@ +const stamps = require('../stamps'); + +exports.up = function (knex) { + return Promise.resolve() + + // create metadata table + .then(() => knex.schema.createTable('metadata', table => { + table.specificType( + 'id', + 'integer GENERATED ALWAYS AS IDENTITY PRIMARY KEY' + ); + table.string('key', 255).index(); + table.string('value', 255).index(); + table.unique(['key', 'value']); + })) + + // create version_metadata table + .then(() => knex.schema.createTable('version_metadata', table => { + table.primary(['versionId', 'metadataId']); + table.uuid('versionId').notNullable().references('id').inTable('version').onDelete('CASCADE').onUpdate('CASCADE'); + table.integer('metadataId').notNullable().references('id').inTable('metadata').onDelete('CASCADE').onUpdate('CASCADE'); + stamps(knex, table); + })) + + // create tag table + .then(() => knex.schema.createTable('tag', table => { + table.specificType( + 'id', + 'integer GENERATED ALWAYS AS IDENTITY PRIMARY KEY' + ); + table.string('key', 255).index(); + table.string('value', 255).index(); + table.unique(['key', 'value']); + })) + + // create version_tag table + .then(() => knex.schema.createTable('version_tag', table => { + table.primary(['versionId', 'tagId']); + table.uuid('versionId').notNullable().references('id').inTable('version').onDelete('CASCADE').onUpdate('CASCADE'); + table.integer('tagId').notNullable().references('id').inTable('tag').onDelete('CASCADE').onUpdate('CASCADE'); + stamps(knex, table); + })) + + // Create metadata audit trigger + .then(() => knex.schema.raw(`CREATE TRIGGER audit_version_metadata_trigger + AFTER UPDATE OR DELETE ON version_metadata + FOR EACH ROW EXECUTE PROCEDURE audit.if_modified_func();`)) + // Create tag audit trigger + .then(() => knex.schema.raw(`CREATE TRIGGER audit_version_tag_trigger + AFTER UPDATE OR DELETE ON version_tag + FOR EACH ROW EXECUTE PROCEDURE audit.if_modified_func();`)) + + /** + * for each version, move originalName and objectId to records in metadata table + * and create joining version_metadata records + */ + .then(() => knex('version').where('deleteMarker', false)) + .then((rows) => { + const versions = rows.map(v => ({ + value: v.originalName, + versionId: v.id, + objectId: v.objectId + })); + + return Promise.all(versions.map((row) => { + // insert into metadata table + knex('metadata').insert([ + { key: 'name', value: row.value }, + { key: 'id', value: row.objectId } + ]).onConflict(['key', 'value']) + .merge() + // Return just id column + .returning('id') + // add joining records + .then((result) => { + return knex('version_metadata').insert(result.map((metadata) => ({ + metadataId: metadata.id, + versionId: row.versionId + }))); + }); + })); + }) + + // remove column originalName from version table + .then(() => knex.schema.alterTable('version', table => { + table.dropColumn('originalName'); + })) + + // additional DB update: change user.identityId field to data type `string` + .then(() => knex.schema.alterTable('user', table => { + table.string('identityId', 255).alter(); + })); + +}; + + +exports.down = function (knex) { + return Promise.resolve() + // additional DB update: change user.identityId field back + // NOTE: Destructive change - removes all data in identityId column + .then(() => knex('user').update({ identityId: null })) + .then(() => knex.schema.alterTable('user', table => { + table.uuid('identityId').alter(); + })) + + // re-add columns originalName version table + .then(() => knex.schema.alterTable('version', table => { + table.string('originalName', 255); + })) + + // move originalName back to version table + .then(() => knex('version_metadata') + .join('metadata', 'metadata.id', 'version_metadata.metadataId') + .select('version_metadata.versionId', 'metadata.value') + .where('metadata.key', 'name') + ) + .then((versionMetadata) => { + return Promise.all(versionMetadata.map((vm) => { + return knex('version') + .update({ originalName: vm.value }) + .where({ id: vm.versionId }); + })); + }) + + // Drop audit triggers + .then(() => knex.schema.raw('DROP TRIGGER IF EXISTS audit_version_tag_trigger ON version_tag')) + .then(() => knex.schema.raw('DROP TRIGGER IF EXISTS audit_version_metadata_trigger ON version_metadata')) + + // Drop tables + .then(() => knex.schema.dropTableIfExists('version_tag')) + .then(() => knex.schema.dropTableIfExists('tag')) + .then(() => knex.schema.dropTableIfExists('version_metadata')) + .then(() => knex.schema.dropTableIfExists('metadata')); +}; diff --git a/comsapi/app/src/db/migrations/20221014000000_003-multi-bucket.js b/comsapi/app/src/db/migrations/20221014000000_003-multi-bucket.js new file mode 100644 index 00000000..cc406c36 --- /dev/null +++ b/comsapi/app/src/db/migrations/20221014000000_003-multi-bucket.js @@ -0,0 +1,81 @@ +const stamps = require('../stamps'); + +exports.up = function (knex) { + return Promise.resolve() + // Create bucket tables + .then(() => knex.schema.createTable('bucket', table => { + table.uuid('bucketId').primary(); + table.string('bucketName', 255).notNullable().index(); + table.string('accessKeyId', 255).notNullable(); + table.string('bucket', 255).notNullable(); + table.string('endpoint', 255).notNullable(); + table.string('key', 255).notNullable(); + table.string('secretAccessKey', 255).notNullable(); + table.string('region', 255); + table.boolean('active').notNullable().defaultTo(true); + stamps(knex, table); + table.unique(['bucket', 'endpoint', 'key']); + })) + .then(() => knex.schema.createTable('bucket_permission', table => { + table.uuid('id').primary(); + table.uuid('bucketId').references('bucketId').inTable('bucket').notNullable().onUpdate('CASCADE').onDelete('CASCADE'); + table.uuid('userId').references('userId').inTable('user').notNullable().onUpdate('CASCADE').onDelete('CASCADE'); + table.string('permCode').references('permCode').inTable('permission').notNullable().onUpdate('CASCADE').onDelete('CASCADE'); + stamps(knex, table); + })) + + // Add object-bucket relation + .then(() => knex.schema.alterTable('object', table => { + table.uuid('bucketId').references('bucketId').inTable('bucket').onUpdate('CASCADE').onDelete('CASCADE'); + })) + + // Ensure appropriate columns are not nullable + .then(() => knex.schema.alterTable('metadata', table => { + table.string('key').notNullable().alter(); + table.string('value').notNullable().alter(); + })) + .then(() => knex.schema.alterTable('tag', table => { + table.string('key').notNullable().alter(); + table.string('value').notNullable().alter(); + })) + .then(() => knex.schema.alterTable('version', table => { + table.string('mimeType').notNullable().defaultTo('application/octet-stream').alter(); + })) + + // Create audit triggers + .then(() => knex.schema.raw(`CREATE TRIGGER audit_bucket_trigger + AFTER UPDATE OR DELETE ON bucket + FOR EACH ROW EXECUTE PROCEDURE audit.if_modified_func();`)) + .then(() => knex.schema.raw(`CREATE TRIGGER audit_bucket_permission_trigger + AFTER UPDATE OR DELETE ON bucket_permission + FOR EACH ROW EXECUTE PROCEDURE audit.if_modified_func();`)); +}; + +exports.down = function (knex) { + return Promise.resolve() + // Drop audit triggers + .then(() => knex.schema.raw('DROP TRIGGER IF EXISTS audit_bucket_permission_trigger ON object_permission')) + .then(() => knex.schema.raw('DROP TRIGGER IF EXISTS audit_bucket_trigger ON object')) + + // Revert not nullable column updates + .then(() => knex.schema.alterTable('version', table => { + table.string('mimeType').alter(); + })) + .then(() => knex.schema.alterTable('tag', table => { + table.string('key').alter(); + table.string('value').alter(); + })) + .then(() => knex.schema.alterTable('metadata', table => { + table.string('key').alter(); + table.string('value').alter(); + })) + + // Remove object-bucket relation + .then(() => knex.schema.alterTable('object', table => { + table.dropColumn('bucketId'); + })) + + // Drop bucket tables + .then(() => knex.schema.dropTableIfExists('bucket_permission')) + .then(() => knex.schema.dropTableIfExists('bucket')); +}; diff --git a/comsapi/app/src/db/migrations/20230306000000_004-s3-version-tag-expand.js b/comsapi/app/src/db/migrations/20230306000000_004-s3-version-tag-expand.js new file mode 100644 index 00000000..fda93da8 --- /dev/null +++ b/comsapi/app/src/db/migrations/20230306000000_004-s3-version-tag-expand.js @@ -0,0 +1,36 @@ +exports.up = function (knex) { + return Promise.resolve() + // Rename versionId to s3VersionId + .then(() => knex.schema.alterTable('version', table => { + table.renameColumn('versionId', 's3VersionId'); + })) + // Add indexes to version.s3VersionId and version.objectId + .then(() => knex.schema.alterTable('version', table => { + table.string('s3VersionId', 1024).index().alter(); + table.uuid('objectId').index().notNullable().alter(); + })) + // Change tag table lengths to align with AWS standards + .then(() => knex.schema.alterTable('tag', table => { + table.string('key', 128).alter(); + table.string('value', 256).alter(); + })); +}; + +exports.down = function (knex) { + return Promise.resolve() + // Revert tag table lengths + .then(() => knex.schema.alterTable('tag', table => { + table.string('key', 255).alter(); + table.string('value', 255).alter(); + })) + // Remove index on version.s3VersionId and version.objectId + .then(() => knex.schema.alterTable('version', table => { + table.dropIndex('s3VersionId'); + table.dropIndex('objectId'); + table.uuid('objectId').notNullable().alter(); + })) + // Revert s3VersionId to versionId + .then(() => knex.schema.alterTable('version', table => { + table.renameColumn('s3VersionId', 'versionId'); + })); +}; diff --git a/comsapi/app/src/db/migrations/20230420000000_005-permission-indexes.js b/comsapi/app/src/db/migrations/20230420000000_005-permission-indexes.js new file mode 100644 index 00000000..59b16c5a --- /dev/null +++ b/comsapi/app/src/db/migrations/20230420000000_005-permission-indexes.js @@ -0,0 +1,27 @@ +exports.up = function (knex) { + return Promise.resolve() + // Add indexes to bucket_permission.bucketId and bucket_permission.userId + .then(() => knex.schema.alterTable('bucket_permission', table => { + table.uuid('bucketId').index().notNullable().alter(); + table.uuid('userId').index().notNullable().alter(); + })) + // Add indexes to object_permission.objectId and object_permission.userId + .then(() => knex.schema.alterTable('object_permission', table => { + table.uuid('objectId').index().notNullable().alter(); + table.uuid('userId').index().notNullable().alter(); + })); +}; + +exports.down = function (knex) { + return Promise.resolve() + // Remove index on object_permission.userId and object_permission.objectId + .then(() => knex.schema.alterTable('object_permission', table => { + table.dropIndex('userId'); + table.dropIndex('objectId'); + })) + // Remove index on bucket_permission.userId and bucket_permission.bucketId + .then(() => knex.schema.alterTable('bucket_permission', table => { + table.dropIndex('userId'); + table.dropIndex('bucketId'); + })); +}; diff --git a/comsapi/app/src/db/migrations/20230427000000_006-metadata-fix.js b/comsapi/app/src/db/migrations/20230427000000_006-metadata-fix.js new file mode 100644 index 00000000..d5a4d991 --- /dev/null +++ b/comsapi/app/src/db/migrations/20230427000000_006-metadata-fix.js @@ -0,0 +1,23 @@ +exports.up = function (knex) { + return Promise.resolve() + .then(() => knex('metadata') + .where({ key: 'id' }) + .update({ key: 'coms-id' }) + ) + .then(() => knex('metadata') + .where({ key: 'name' }) + .update({ key: 'coms-name' }) + ); +}; + +exports.down = function (knex) { + return Promise.resolve() + .then(() => knex('metadata') + .where({ key: 'coms-id' }) + .update({ key: 'id' }) + ) + .then(() => knex('metadata') + .where({ key: 'coms-name' }) + .update({ key: 'name' }) + ); +}; diff --git a/comsapi/app/src/db/migrations/20230503000000_007-default-updatedat.js b/comsapi/app/src/db/migrations/20230503000000_007-default-updatedat.js new file mode 100644 index 00000000..a626dd37 --- /dev/null +++ b/comsapi/app/src/db/migrations/20230503000000_007-default-updatedat.js @@ -0,0 +1,69 @@ +// change updatedAt column to have no default value +// timestamps.js mixin will add CURRENT_TIMESTAMP on update +exports.up = function (knex) { + return Promise.resolve() + .then(() => knex.schema.alterTable('bucket', table => { + table.timestamp('updatedAt', { useTz: true }).alter(); + })) + .then(() => knex.schema.alterTable('bucket_permission', table => { + table.timestamp('updatedAt', { useTz: true }).alter(); + })) + .then(() => knex.schema.alterTable('identity_provider', table => { + table.timestamp('updatedAt', { useTz: true }).alter(); + })) + .then(() => knex.schema.alterTable('object', table => { + table.timestamp('updatedAt', { useTz: true }).alter(); + })) + .then(() => knex.schema.alterTable('object_permission', table => { + table.timestamp('updatedAt', { useTz: true }).alter(); + })) + .then(() => knex.schema.alterTable('permission', table => { + table.timestamp('updatedAt', { useTz: true }).alter(); + })) + .then(() => knex.schema.alterTable('user', table => { + table.timestamp('updatedAt', { useTz: true }).alter(); + })) + .then(() => knex.schema.alterTable('version', table => { + table.timestamp('updatedAt', { useTz: true }).alter(); + })) + .then(() => knex.schema.alterTable('version_metadata', table => { + table.timestamp('updatedAt', { useTz: true }).alter(); + })) + .then(() => knex.schema.alterTable('version_tag', table => { + table.timestamp('updatedAt', { useTz: true }).alter(); + })); +}; + +exports.down = function (knex) { + return Promise.resolve() + .then(() => knex.schema.alterTable('version_tag', table => { + table.timestamp('updatedAt', { useTz: true }).defaultTo(knex.fn.now()).alter(); + })) + .then(() => knex.schema.alterTable('version_metadata', table => { + table.timestamp('updatedAt', { useTz: true }).defaultTo(knex.fn.now()).alter(); + })) + .then(() => knex.schema.alterTable('version', table => { + table.timestamp('updatedAt', { useTz: true }).defaultTo(knex.fn.now()).alter(); + })) + .then(() => knex.schema.alterTable('user', table => { + table.timestamp('updatedAt', { useTz: true }).defaultTo(knex.fn.now()).alter(); + })) + .then(() => knex.schema.alterTable('permission', table => { + table.timestamp('updatedAt', { useTz: true }).defaultTo(knex.fn.now()).alter(); + })) + .then(() => knex.schema.alterTable('object_permission', table => { + table.timestamp('updatedAt', { useTz: true }).defaultTo(knex.fn.now()).alter(); + })) + .then(() => knex.schema.alterTable('object', table => { + table.timestamp('updatedAt', { useTz: true }).defaultTo(knex.fn.now()).alter(); + })) + .then(() => knex.schema.alterTable('identity_provider', table => { + table.timestamp('updatedAt', { useTz: true }).defaultTo(knex.fn.now()).alter(); + })) + .then(() => knex.schema.alterTable('bucket_permission', table => { + table.timestamp('updatedAt', { useTz: true }).defaultTo(knex.fn.now()).alter(); + })) + .then(() => knex.schema.alterTable('bucket', table => { + table.timestamp('updatedAt', { useTz: true }).defaultTo(knex.fn.now()).alter(); + })); +}; diff --git a/comsapi/app/src/db/migrations/20230518000000_008-filename-etag.js b/comsapi/app/src/db/migrations/20230518000000_008-filename-etag.js new file mode 100644 index 00000000..75e1e5c3 --- /dev/null +++ b/comsapi/app/src/db/migrations/20230518000000_008-filename-etag.js @@ -0,0 +1,32 @@ +exports.up = function (knex) { + return Promise.resolve() + // add name column to object table + .then(() => knex.schema.alterTable('object', table => { + table.string('name', 1024); + })) + // backfill name from path column data + .then(() => knex('object').select(['id', 'path'])) + .then((tuples) => { + return Promise.all(tuples.map(tuple => + knex('object') + .where('id', tuple.id) + .update({ 'name': tuple.path.match(/(?!.*\/)(.*)$/)[0] }) + )); + }) + // add etag column to version table + .then(() => knex.schema.alterTable('version', table => { + table.string('etag', 65536); + })); +}; + +exports.down = function (knex) { + return Promise.resolve() + // drop etag column in version table + .then(() => knex.schema.alterTable('version', table => { + table.dropColumn('etag'); + })) + // drop name column in object table + .then(() => knex.schema.alterTable('object', table => { + table.dropColumn('name'); + })); +}; diff --git a/comsapi/app/src/db/migrations/20230810000000_009-user-identity-constraint.js b/comsapi/app/src/db/migrations/20230810000000_009-user-identity-constraint.js new file mode 100644 index 00000000..1f2e71a9 --- /dev/null +++ b/comsapi/app/src/db/migrations/20230810000000_009-user-identity-constraint.js @@ -0,0 +1,28 @@ +exports.up = function (knex) { + return Promise.resolve() + // remove duplicate users - deletes all but oldest of users with matching IdentityId + // duplicates users may have existed in rare case where authentication of a new user was triggered concurrently + .then(() => knex.raw(`DELETE FROM "public"."user" AS u + WHERE u."userId" IN ( + SELECT DISTINCT ON ("public"."user"."identityId") "public"."user"."userId" + FROM "public"."user" + WHERE ( + SELECT count(*) + FROM "public"."user" AS user2 + WHERE user2."identityId" = "public"."user"."identityId") > 1 + ORDER BY "public"."user"."identityId", "public"."user"."createdAt" DESC + )`)) + + // prevent further duplicate users - add unique index over columns identityId and idp in user table + .then(() => knex.schema.alterTable('user', table => { + table.unique(['identityId', 'idp']); + })); +}; + +exports.down = function (knex) { + return Promise.resolve() + // remove unique index over columns identityId and idp in user table + .then(() => knex.schema.alterTable('user', table => { + table.dropUnique(['identityId', 'idp']); + })); +}; diff --git a/comsapi/app/src/db/migrations/20230814000000_010-sync-queue.js b/comsapi/app/src/db/migrations/20230814000000_010-sync-queue.js new file mode 100644 index 00000000..d902276e --- /dev/null +++ b/comsapi/app/src/db/migrations/20230814000000_010-sync-queue.js @@ -0,0 +1,63 @@ +const stamps = require('../stamps'); + +exports.up = function (knex) { + return Promise.resolve() + // Create queue schema and object_queue table + .then(() => knex.schema.raw('CREATE SCHEMA IF NOT EXISTS queue')) + + .then(() => knex.schema.withSchema('queue').createTable('object_queue', table => { + table.specificType( + 'id', + 'integer GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY' + ); + table.uuid('bucketId', 255); + table.string('path', 1024).notNullable(); + table.boolean('full').notNullable().defaultTo(false); + table.integer('retries').notNullable().defaultTo(0); + stamps(knex, table); + + // Add two partial indexes to ensure null bucketIds are also unique constrained + // @see {@link https://stackoverflow.com/a/8289253} + table.unique(['bucketId', 'path'], { predicate: knex.whereNotNull('bucketId') }); + table.unique('path', { predicate: knex.whereNull('bucketId') }); + })) + + // Add index to object path column + .then(() => knex.schema.alterTable('object', table => { + table.string('path', 1024).index().notNullable().alter(); + })) + + // Add isLatest column to version table + .then(() => knex.schema.alterTable('version', table => { + table.boolean('isLatest').notNullable().defaultTo(false); + })) + + // Add notNullable to tag columns + .then(() => knex.schema.alterTable('tag', table => { + table.string('key', 128).notNullable().alter(); + table.string('value', 256).notNullable().alter(); + })); +}; + +exports.down = function (knex) { + return Promise.resolve() + // Drop notNullable from tag columns + .then(() => knex.schema.alterTable('tag', table => { + table.string('key', 128).alter(); + table.string('value', 256).alter(); + })) + + // Drop isLatest column in version table + .then(() => knex.schema.alterTable('version', table => { + table.dropColumn('isLatest'); + })) + + // Drop index from object path column + .then(() => knex.schema.alterTable('object', table => { + table.dropIndex('path'); + })) + + // Drop queue schema and object_queue table + .then(() => knex.schema.withSchema('queue').dropTableIfExists('object_queue')) + .then(() => knex.schema.dropSchemaIfExists('queue')); +}; diff --git a/comsapi/app/src/db/migrations/20231010000000_011-metadata-text.js b/comsapi/app/src/db/migrations/20231010000000_011-metadata-text.js new file mode 100644 index 00000000..c4e06007 --- /dev/null +++ b/comsapi/app/src/db/migrations/20231010000000_011-metadata-text.js @@ -0,0 +1,17 @@ +exports.up = function (knex) { + return Promise.resolve() + // Change to text type + .then(() => knex.schema.alterTable('metadata', table => { + table.text('key').notNullable().alter(); + table.text('value').notNullable().alter(); + })); +}; + +exports.down = function (knex) { + return Promise.resolve() + // Revert back to varchar(255) type + .then(() => knex.schema.alterTable('metadata', table => { + table.string('key', 255).notNullable().alter(); + table.string('value', 255).notNullable().alter(); + })); +}; diff --git a/comsapi/app/src/db/models/index.js b/comsapi/app/src/db/models/index.js new file mode 100644 index 00000000..42cc11ee --- /dev/null +++ b/comsapi/app/src/db/models/index.js @@ -0,0 +1,20 @@ +const models = { + // Tables + Bucket: require('./tables/bucket'), + BucketPermission: require('./tables/bucketPermission'), + IdentityProvider: require('./tables/identityProvider'), + Metadata: require('./tables/metadata'), + ObjectModel: require('./tables/objectModel'), + ObjectPermission: require('./tables/objectPermission'), + ObjectQueue: require('./tables/objectQueue'), + Permission: require('./tables/permission'), + Tag: require('./tables/tag'), + User: require('./tables/user'), + Version: require('./tables/version'), + VersionMetadata: require('./tables/versionMetadata'), + VersionTag: require('./tables/versionTag'), + + // Views +}; + +module.exports = models; diff --git a/comsapi/app/src/db/models/jsonSchema.js b/comsapi/app/src/db/models/jsonSchema.js new file mode 100644 index 00000000..c3f69606 --- /dev/null +++ b/comsapi/app/src/db/models/jsonSchema.js @@ -0,0 +1,6 @@ +module.exports.stamps = { + createdBy: { type: ['string', 'null'], maxLength: 255 }, + createdAt: { type: ['string', 'null'] }, + updatedBy: { type: ['string', 'null'], maxLength: 255 }, + updatedAt: { type: ['string', 'null'] } +}; diff --git a/comsapi/app/src/db/models/mixins/encrypt.js b/comsapi/app/src/db/models/mixins/encrypt.js new file mode 100644 index 00000000..e09224c6 --- /dev/null +++ b/comsapi/app/src/db/models/mixins/encrypt.js @@ -0,0 +1,70 @@ +const { encrypt, decrypt } = require('../../../components/crypt'); + +/** + * Encrypt Objection Model Plugin + * Add column encryption handlers to an Objection Model + * + * This class will automatically encrypt and decrypt specified column fields + * during insert/update/get operations transparently. + * Inspired by @see {@link https://github.com/Dialogtrail/objection-encrypt} + * + * @see module:knex + * @see module:objection + */ +const Encrypt = opts => { + // Provide good default options if possible. + const options = Object.assign( + { + fields: [] + }, + opts + ); + + // Return the mixin + return Model => { + return class extends Model { + async $beforeInsert(context) { + await super.$beforeInsert(context); + this.encryptFields(); + } + async $afterInsert(context) { + await super.$afterInsert(context); + return this.decryptFields(); + } + async $beforeUpdate(queryOptions, context) { + await super.$beforeUpdate(queryOptions, context); + this.encryptFields(); + } + async $afterUpdate(queryOptions, context) { + await super.$afterUpdate(queryOptions, context); + return this.decryptFields(); + } + async $afterFind(context) { + await super.$afterFind(context); + return this.decryptFields(); + } + + /** + * Encrypts specified fields + */ + encryptFields() { + options.fields.forEach(field => { + const value = this[field]; + if (value) this[field] = encrypt(value); + }); + } + + /** + * Decrypts specified fields + */ + decryptFields() { + options.fields.forEach(field => { + const value = this[field]; + if (value) this[field] = decrypt(value); + }); + } + }; + }; +}; + +module.exports = Encrypt; diff --git a/comsapi/app/src/db/models/mixins/index.js b/comsapi/app/src/db/models/mixins/index.js new file mode 100644 index 00000000..432391f8 --- /dev/null +++ b/comsapi/app/src/db/models/mixins/index.js @@ -0,0 +1,6 @@ +const mixins = { + Encrypt: require('./encrypt'), + Timestamps: require('./timestamps') +}; + +module.exports = mixins; diff --git a/comsapi/app/src/db/models/mixins/timestamps.js b/comsapi/app/src/db/models/mixins/timestamps.js new file mode 100644 index 00000000..d1d9fab0 --- /dev/null +++ b/comsapi/app/src/db/models/mixins/timestamps.js @@ -0,0 +1,27 @@ +/** + * Timestamps Objection Model Plugin + * Add handlers to an Objection Model + * + * In order to use JSON Schema Validation, we need to treat Timestamps as strings. + * They still get stored as dates/timestamps, but in/out of the database they need to be strings. + * + * This class will set the createdAt timestamp/string before insert and updatedAt before update. + * The JSON Schema validation will pass as it goes through the marshalling, expecting createdAt and updateAt as strings. + * + * @see module:knex + * @see module:objection + */ +const Timestamps = (Model) => { + return class extends Model { + async $beforeInsert(queryContext) { + await super.$beforeInsert(queryContext); + this.createdAt = new Date().toISOString(); + } + async $beforeUpdate(opt, queryContext) { + await super.$beforeUpdate(opt, queryContext); + this.updatedAt = new Date().toISOString(); + } + }; +}; + +module.exports = Timestamps; diff --git a/comsapi/app/src/db/models/tables/bucket.js b/comsapi/app/src/db/models/tables/bucket.js new file mode 100644 index 00000000..5572e360 --- /dev/null +++ b/comsapi/app/src/db/models/tables/bucket.js @@ -0,0 +1,98 @@ +const { mixin, Model } = require('objection'); + +const { stamps } = require('../jsonSchema'); +const { Encrypt, Timestamps } = require('../mixins'); +const { filterOneOrMany, filterILike } = require('../utils'); + +class Bucket extends mixin(Model, [ + Encrypt({ fields: ['secretAccessKey'] }), + Timestamps +]) { + static get tableName() { + return 'bucket'; + } + + static get idColumn() { + return 'bucketId'; + } + + static get relationMappings() { + const BucketPermission = require('./bucketPermission'); + const ObjectModel = require('./objectModel'); + + return { + bucketPermission: { + relation: Model.HasManyRelation, + modelClass: BucketPermission, + join: { + from: 'bucket.bucketId', + to: 'bucket_permission.bucketId' + } + }, + object: { + relation: Model.HasManyRelation, + modelClass: ObjectModel, + join: { + from: 'bucket.bucketId', + to: 'object.bucketId', + } + }, + }; + } + + static get modifiers() { + return { + filterBucketIds(query, value) { + filterOneOrMany(query, value, 'bucket.bucketId'); + }, + filterBucketName(query, value) { + filterILike(query, value, 'bucket.bucketName'); + }, + filterKey(query, value) { + filterILike(query, value, 'bucket.key'); + }, + filterActive(query, value) { + if (value !== undefined) query.where('bucket.active', value); + }, + filterUserId(query, value) { + if (value) { + query + .withGraphJoined('bucketPermission') + .whereIn('bucketPermission.bucketId', builder => { + builder.distinct('bucketPermission.bucketId') + .where('bucketPermission.userId', value); + }); + } + }, + }; + } + + static get jsonSchema() { + return { + type: 'object', + required: [ + 'bucketId', + 'bucketName', + 'accessKeyId', + 'bucket', + 'endpoint', + 'secretAccessKey' + ], + properties: { + bucketId: { type: 'string', minLength: 1, maxLength: 255 }, + bucketName: { type: 'string', minLength: 1, maxLength: 255 }, + accessKeyId: { type: 'string', minLength: 1, maxLength: 255 }, + bucket: { type: 'string', minLength: 1, maxLength: 255 }, + endpoint: { type: 'string', minLength: 1, maxLength: 255 }, + key: { type: 'string', maxLength: 255 }, + secretAccessKey: { type: 'string', minLength: 1, maxLength: 255 }, + region: { type: 'string', maxLength: 255 }, + active: { type: 'boolean' }, + ...stamps + }, + additionalProperties: false + }; + } +} + +module.exports = Bucket; diff --git a/comsapi/app/src/db/models/tables/bucketPermission.js b/comsapi/app/src/db/models/tables/bucketPermission.js new file mode 100644 index 00000000..7c13e316 --- /dev/null +++ b/comsapi/app/src/db/models/tables/bucketPermission.js @@ -0,0 +1,75 @@ +const { Model } = require('objection'); + +const { stamps } = require('../jsonSchema'); +const { Timestamps } = require('../mixins'); +const { filterOneOrMany } = require('../utils'); + +class BucketPermission extends Timestamps(Model) { + static get tableName() { + return 'bucket_permission'; + } + + static get relationMappings() { + const Bucket = require('./bucket'); + const Permission = require('./permission'); + const User = require('./user'); + + return { + bucket: { + relation: Model.HasOneRelation, + modelClass: Bucket, + join: { + from: 'bucket_permission.bucketId', + to: 'bucket.bucketId' + } + }, + permission: { + relation: Model.HasOneRelation, + modelClass: Permission, + join: { + from: 'bucket_permission.permCode', + to: 'permission.permCode' + } + }, + user: { + relation: Model.HasOneRelation, + modelClass: User, + join: { + from: 'bucket_permission.userId', + to: 'user.userId' + } + } + }; + } + + static get modifiers() { + return { + filterUserId(query, value) { + filterOneOrMany(query, value, 'userId'); + }, + filterBucketId(query, value) { + filterOneOrMany(query, value, 'bucketId'); + }, + filterPermissionCode(query, value) { + filterOneOrMany(query, value, 'permCode'); + } + }; + } + + static get jsonSchema() { + return { + type: 'object', + required: ['id', 'userId', 'bucketId', 'permCode'], + properties: { + id: { type: 'string', maxLength: 255 }, + userId: { type: 'string', maxLength: 255 }, + bucketId: { type: 'string', maxLength: 255 }, + permCode: { type: 'string', maxLength: 255 }, + ...stamps + }, + additionalProperties: false + }; + } +} + +module.exports = BucketPermission; diff --git a/comsapi/app/src/db/models/tables/identityProvider.js b/comsapi/app/src/db/models/tables/identityProvider.js new file mode 100644 index 00000000..74510368 --- /dev/null +++ b/comsapi/app/src/db/models/tables/identityProvider.js @@ -0,0 +1,56 @@ +const { Model } = require('objection'); + +const { stamps } = require('../jsonSchema'); +const { Timestamps } = require('../mixins'); + +class IdentityProvider extends Timestamps(Model) { + static get tableName() { + return 'identity_provider'; + } + + static get idColumn() { + return 'idp'; + } + + static get relationMappings() { + const IdentityProvider = require('./identityProvider'); + + return { + identityProvider: { + relation: Model.BelongsToOneRelation, + modelClass: IdentityProvider, + join: { + from: 'user.idp', + to: 'identity_provider.idp' + } + } + }; + } + + static get modifiers() { + return { + filterActive(query, value) { + if (value !== undefined) query.where('active', value); + }, + orderDefault(builder) { + builder.orderByRaw('lower("identity_provider"."idp")'); + } + }; + } + + static get jsonSchema() { + return { + type: 'object', + required: ['idp'], + properties: { + idp: { type: 'string', minLength: 1, maxLength: 255 }, + active: { type: 'boolean' }, + ...stamps + }, + additionalProperties: false + }; + } + +} + +module.exports = IdentityProvider; diff --git a/comsapi/app/src/db/models/tables/metadata.js b/comsapi/app/src/db/models/tables/metadata.js new file mode 100644 index 00000000..6110f5a6 --- /dev/null +++ b/comsapi/app/src/db/models/tables/metadata.js @@ -0,0 +1,87 @@ +const { Model } = require('objection'); + +class Metadata extends Model { + static get tableName() { + return 'metadata'; + } + + static get relationMappings() { + const Version = require('./version'); + const VersionMetadata = require('./versionMetadata'); + + return { + version: { + relation: Model.ManyToManyRelation, + modelClass: Version, + join: { + from: 'metadata.id', + through: { + from: 'version_metadata.metadataId', + to: 'version_metadata.versionId' + }, + to: 'version.id' + } + }, + versionMetadata: { + relation: Model.HasManyRelation, + modelClass: VersionMetadata, + join: { + from: 'metadata.id', + to: 'version_metadata.metadataId' + } + }, + }; + } + + static get modifiers() { + return { + filterKey(query, value) { + const subqueries = []; + if (value.metadata && Object.keys(value.metadata).length) { + Object.entries(value.metadata).forEach(metadata => { + const q = Metadata.query().select('metadata.id').where('key', 'ilike', `%${metadata[0]}%`); + subqueries.push(q); + }); + } + if (subqueries.length) { + query + .whereIn('metadata.id', builder => { + builder.intersect(subqueries); + }); + } + }, + + filterKeyValue(query, value) { + const subqueries = []; + if (value.metadata && Object.keys(value.metadata).length) { + Object.entries(value.metadata).forEach(([key, val]) => { + const q = Metadata.query().select('metadata.id').where('key', 'ilike', `%${key}%`); + if (val.length) q.where('value', 'ilike', `%${val}%`); + subqueries.push(q); + }); + } + if (subqueries.length) { + query + .whereIn('metadata.id', builder => { + builder.intersect(subqueries); + }); + } + }, + }; + } + + static get jsonSchema() { + return { + type: 'object', + required: ['key', 'value'], + properties: { + id: { type: 'integer' }, + key: { type: 'string', minLength: 1 }, + value: { type: 'string', minLength: 1 } + }, + additionalProperties: false + }; + } +} + +module.exports = Metadata; diff --git a/comsapi/app/src/db/models/tables/objectModel.js b/comsapi/app/src/db/models/tables/objectModel.js new file mode 100644 index 00000000..e391d203 --- /dev/null +++ b/comsapi/app/src/db/models/tables/objectModel.js @@ -0,0 +1,201 @@ +const { Model } = require('objection'); + +const { stamps } = require('../jsonSchema'); +const { Timestamps } = require('../mixins'); +const { filterOneOrMany, filterILike } = require('../utils'); + +// The table is "object" but Object is a bit of a reserved word :) +class ObjectModel extends Timestamps(Model) { + static get tableName() { + return 'object'; + } + + static get relationMappings() { + const Bucket = require('./bucket'); + const ObjectPermission = require('./objectPermission'); + const BucketPermission = require('./bucketPermission'); + const Version = require('./version'); + + return { + bucket: { + relation: Model.HasOneRelation, + modelClass: Bucket, + join: { + from: 'object.bucketId', + to: 'bucket.bucketId' + } + }, + objectPermission: { + relation: Model.HasManyRelation, + modelClass: ObjectPermission, + join: { + from: 'object.id', + to: 'object_permission.objectId' + } + }, + version: { + relation: Model.HasManyRelation, + modelClass: Version, + join: { + from: 'object.id', + to: 'version.objectId', + } + }, + bucketPermission: { + relation: Model.HasManyRelation, + modelClass: BucketPermission, + join: { + from: 'object.bucketId', + to: 'bucket_permission.bucketId' + } + } + }; + } + + static get modifiers() { + const Version = require('./version'); + + return { + filterIds(query, value) { + filterOneOrMany(query, value, 'object.id'); + }, + filterBucketIds(query, value) { + if (value === null) { + query.whereNull('object.bucketId'); + } else { + filterOneOrMany(query, value, 'object.bucketId'); + } + }, + filterName(query, value) { + filterILike(query, value, 'object.name'); + }, + filterPath(query, value) { + filterILike(query, value, 'object.path'); + }, + filterPublic(query, value) { + if (value !== undefined) query.where('object.public', value); + }, + filterActive(query, value) { + if (value !== undefined) query.where('object.active', value); + }, + filterMimeType(query, value) { + if (value) { + query + .withGraphJoined('version') + .whereIn('version.id', builder => { + builder.select('version.id') + .where('version.mimeType', 'ilike', `%${value}%`); + }); + } + }, + filterDeleteMarker(query, value) { + if (value !== undefined) { + query + .withGraphJoined('version') + .where('version.deleteMarker', value); + } + }, + filterLatest(query, value) { + if (value !== undefined) { + + query.withGraphJoined('version'); + if (value) { + // join on version where isLatest = true + query.modifyGraph('version', builder => { + builder + .select('version.*') + .where('version.isLatest', true); + }); + } else { + // join on ALL versions where isLatest = false + const subquery = Version.query() + .select('version.id') + .where('version.isLatest', false); + query.whereIn('version.id', builder => { + builder.intersect(subquery); + }); + } + } + }, + filterMetadataTag(query, value) { + const subqueries = []; + + if (value.metadata && Object.keys(value.metadata).length) { + Object.entries(value.metadata).forEach(([key, val]) => { + const q = Version.query() + .select('version.id') + .joinRelated('metadata') + .where('metadata.key', key); + if (val.length) q.where('metadata.value', val); + subqueries.push(q); + }); + } + + if (value.tag && Object.keys(value.tag).length) { + Object.entries(value.tag).forEach(([key, val]) => { + const q = Version.query() + .select('version.id') + .joinRelated('tag') + .where('tag.key', key); + if (val.length) q.where('tag.value', val); + subqueries.push(q); + }); + } + + if (subqueries.length) { + query + .withGraphJoined('version') + .whereIn('version.id', builder => { + builder.intersect(subqueries); + }); + } + }, + findPath(query, value) { + if (value) query.where('object.path', value); + }, + hasPermission(query, userId, permCode) { + if (userId && permCode) { + query + .fullOuterJoinRelated('[objectPermission, bucketPermission]') + // wrap in WHERE to make contained clauses exclusive of root query + .where(query => { + query + .where(query => { + query + .where({ + 'objectPermission.permCode': permCode, + 'objectPermission.userId': userId + }); + }) + .orWhere(query => { + query + .where({ + 'bucketPermission.permCode': permCode, + 'bucketPermission.userId': userId + }); + }); + }); + } + } + }; + } + + static get jsonSchema() { + return { + type: 'object', + required: ['id', 'path'], + properties: { + id: { type: 'string', minLength: 1, maxLength: 255 }, + path: { type: 'string', minLength: 1, maxLength: 1024 }, + public: { type: 'boolean' }, + active: { type: 'boolean' }, + bucketId: { type: 'string', maxLength: 255, nullable: true }, + name: { type: 'string', maxLength: 1024 }, + ...stamps + }, + additionalProperties: false + }; + } +} + +module.exports = ObjectModel; diff --git a/comsapi/app/src/db/models/tables/objectPermission.js b/comsapi/app/src/db/models/tables/objectPermission.js new file mode 100644 index 00000000..970e1211 --- /dev/null +++ b/comsapi/app/src/db/models/tables/objectPermission.js @@ -0,0 +1,83 @@ +const { Model } = require('objection'); + +const { stamps } = require('../jsonSchema'); +const { Timestamps } = require('../mixins'); +const { filterOneOrMany } = require('../utils'); + +class ObjectPermission extends Timestamps(Model) { + static get tableName() { + return 'object_permission'; + } + + static get relationMappings() { + const ObjectModel = require('./objectModel'); + const Permission = require('./permission'); + const User = require('./user'); + + return { + object: { + relation: Model.HasOneRelation, + modelClass: ObjectModel, + join: { + from: 'object_permission.objectId', + to: 'object.id' + } + }, + permission: { + relation: Model.HasOneRelation, + modelClass: Permission, + join: { + from: 'object_permission.permCode', + to: 'permission.permCode' + } + }, + user: { + relation: Model.HasOneRelation, + modelClass: User, + join: { + from: 'object_permission.userId', + to: 'user.userId' + } + } + }; + } + + static get modifiers() { + return { + filterBucketId(query, value) { + if (value) { + query + .select('object_permission.*') + .joinRelated('object') + .whereIn('object.bucketId', value); + } + }, + filterUserId(query, value) { + filterOneOrMany(query, value, 'userId'); + }, + filterObjectId(query, value) { + filterOneOrMany(query, value, 'objectId'); + }, + filterPermissionCode(query, value) { + filterOneOrMany(query, value, 'permCode'); + } + }; + } + + static get jsonSchema() { + return { + type: 'object', + required: ['id', 'userId', 'objectId', 'permCode'], + properties: { + id: { type: 'string', maxLength: 255 }, + userId: { type: 'string', maxLength: 255 }, + objectId: { type: 'string', maxLength: 255 }, + permCode: { type: 'string', maxLength: 255 }, + ...stamps + }, + additionalProperties: false + }; + } +} + +module.exports = ObjectPermission; diff --git a/comsapi/app/src/db/models/tables/objectQueue.js b/comsapi/app/src/db/models/tables/objectQueue.js new file mode 100644 index 00000000..d0205eb5 --- /dev/null +++ b/comsapi/app/src/db/models/tables/objectQueue.js @@ -0,0 +1,53 @@ +const { Model } = require('objection'); + +const { stamps } = require('../jsonSchema'); +const { Timestamps } = require('../mixins'); +const { filterOneOrMany, filterILike } = require('../utils'); + +class ObjectModel extends Timestamps(Model) { + static get tableName() { + return 'object_queue'; + } + + static get modifiers() { + return { + filterBucketIds(query, value) { + filterOneOrMany(query, value, 'object_queue.bucketId'); + }, + filterPath(query, value) { + filterILike(query, value, 'object_queue.path'); + }, + findNextJob(query) { + query.where('object_queue.id', builder => { + builder + .select('object_queue.id') + .from('object_queue') + .orderBy('object_queue.id', 'asc') + // Need to ensure transactional integrity under concurrent loads + // https://www.2ndquadrant.com/en/blog/what-is-select-skip-locked-for-in-postgresql-9-5/ + .forUpdate() + .skipLocked() + .limit(1); + }); + } + }; + } + + static get jsonSchema() { + return { + type: 'object', + required: ['path', 'full', 'retries'], + properties: { + id: { type: 'integer' }, + bucketId: { type: 'string', maxLength: 255, nullable: true }, + path: { type: 'string', minLength: 1, maxLength: 1024 }, + full: { type: 'boolean' }, + retries: { type: 'integer' }, + ...stamps + }, + additionalProperties: false + }; + } +} + +module.exports = ObjectModel; diff --git a/comsapi/app/src/db/models/tables/permission.js b/comsapi/app/src/db/models/tables/permission.js new file mode 100644 index 00000000..d85ed5c4 --- /dev/null +++ b/comsapi/app/src/db/models/tables/permission.js @@ -0,0 +1,56 @@ +const { Model } = require('objection'); + +const { stamps } = require('../jsonSchema'); +const { Timestamps } = require('../mixins'); + +class Permission extends Timestamps(Model) { + static get tableName() { + return 'permission'; + } + + static get idColumn() { + return 'permCode'; + } + + static get relationMappings() { + const ObjectPermission = require('./objectPermission'); + + return { + identityProvider: { + relation: Model.BelongsToOneRelation, + modelClass: ObjectPermission, + join: { + from: 'permission.permCode', + to: 'object_permission.permCode' + } + } + }; + } + + static get modifiers() { + return { + filterActive(query, value) { + if (value !== undefined) query.where('active', value); + }, + orderDefault(builder) { + builder.orderByRaw('lower("permission"."permCode")'); + } + }; + } + + static get jsonSchema() { + return { + type: 'object', + required: ['permCode'], + properties: { + permCode: { type: 'string', minLength: 1, maxLength: 255 }, + active: { type: 'boolean' }, + ...stamps + }, + additionalProperties: false + }; + } + +} + +module.exports = Permission; diff --git a/comsapi/app/src/db/models/tables/tag.js b/comsapi/app/src/db/models/tables/tag.js new file mode 100644 index 00000000..d5464b7b --- /dev/null +++ b/comsapi/app/src/db/models/tables/tag.js @@ -0,0 +1,88 @@ +const { Model } = require('objection'); + +class Tag extends Model { + static get tableName() { + return 'tag'; + } + + static get relationMappings() { + const Version = require('./version'); + const VersionTag = require('./versionTag'); + + return { + versionTag: { + relation: Model.HasManyRelation, + modelClass: VersionTag, + join: { + from: 'tag.id', + to: 'version_tag.tagId' + } + }, + + version: { + relation: Model.ManyToManyRelation, + modelClass: Version, + join: { + from: 'tag.id', + through: { + from: 'version_tag.tagId', + to: 'version_tag.versionId' + }, + to: 'version.id' + } + } + }; + } + + static get modifiers() { + return { + filterKey(query, value) { + const subqueries = []; + if (value.tag && Object.keys(value.tag).length) { + Object.entries(value.tag).forEach(tag => { + const q = Tag.query().select('tag.id').where('key', 'ilike', `%${tag[0]}%`); + subqueries.push(q); + }); + } + if (subqueries.length) { + query + .whereIn('tag.id', builder => { + builder.intersect(subqueries); + }); + } + }, + + filterKeyValue(query, value) { + const subqueries = []; + if (value.tag && Object.keys(value.tag).length) { + Object.entries(value.tag).forEach(([key, val]) => { + const q = Tag.query().select('tag.id').where('key', 'ilike', `%${key}%`); + if (val.length) q.where('value', 'ilike', `%${val}%`); + subqueries.push(q); + }); + } + if (subqueries.length) { + query + .whereIn('tag.id', builder => { + builder.intersect(subqueries); + }); + } + }, + }; + } + + static get jsonSchema() { + return { + type: 'object', + required: ['key', 'value'], + properties: { + id: { type: 'integer' }, + key: { type: 'string', minLength: 1, maxLength: 128 }, + value: { type: 'string', maxLength: 256 } + }, + additionalProperties: false + }; + } +} + +module.exports = Tag; diff --git a/comsapi/app/src/db/models/tables/user.js b/comsapi/app/src/db/models/tables/user.js new file mode 100644 index 00000000..c7be1629 --- /dev/null +++ b/comsapi/app/src/db/models/tables/user.js @@ -0,0 +1,107 @@ +const { Model } = require('objection'); + +const { stamps } = require('../jsonSchema'); +const { Timestamps } = require('../mixins'); +const { filterOneOrMany, filterILike } = require('../utils'); + +class User extends Timestamps(Model) { + static get tableName() { + return 'user'; + } + + static get idColumn() { + return 'userId'; + } + + static get relationMappings() { + const IdentityProvider = require('./identityProvider'); + const ObjectPermission = require('./objectPermission'); + + return { + identityProvider: { + relation: Model.HasOneRelation, + modelClass: IdentityProvider, + join: { + from: 'user.idp', + to: 'identity_provider.idp' + } + }, + objectPermission: { + relation: Model.HasManyRelation, + modelClass: ObjectPermission, + join: { + from: 'user.userId', + to: 'object_permission.userId' + } + } + }; + } + + static get modifiers() { + return { + filterUserId(query, value) { + filterOneOrMany(query, value, 'userId'); + }, + filterIdentityId(query, value) { + filterOneOrMany(query, value, 'identityId'); + }, + filterIdp(query, value) { + filterOneOrMany(query, value, 'idp'); + }, + filterUsername(query, value) { + filterILike(query, value, 'username'); + }, + filterEmail(query, value) { + filterILike(query, value, 'email'); + }, + filterFirstName(query, value) { + filterILike(query, value, 'firstName'); + }, + filterFullName(query, value) { + filterILike(query, value, 'fullName'); + }, + filterLastName(query, value) { + filterILike(query, value, 'lastName'); + }, + filterActive(query, value) { + if (value !== undefined) query.where('active', value); + }, + /** General OR search across multiple fields */ + filterSearch(query, value) { + // Must be written as subquery function to force parentheses grouping + if (value) { + query.where(subquery => { + subquery.where('username', 'ilike', `%${value}%`) + .orWhere('email', 'ilike', `%${value}%`) + .orWhere('fullName', 'ilike', `%${value}%`); + }); + } + }, + orderLastFirstAscending(builder) { + builder.orderByRaw('lower("lastName"), lower("firstName")'); + } + }; + } + + static get jsonSchema() { + return { + type: 'object', + required: ['userId', 'username'], + properties: { + userId: { type: 'string', maxLength: 255 }, + identityId: { type: 'string', maxLength: 255 }, + idp: { type: 'string' }, + firstName: { type: 'string', maxLength: 255 }, + fullName: { type: 'string', maxLength: 255 }, + lastName: { type: 'string', maxLength: 255 }, + username: { type: 'string', maxLength: 255 }, + email: { type: 'string', maxLength: 255 }, + active: { type: 'boolean' }, + ...stamps + }, + additionalProperties: false + }; + } +} + +module.exports = User; diff --git a/comsapi/app/src/db/models/tables/version.js b/comsapi/app/src/db/models/tables/version.js new file mode 100644 index 00000000..dd9b8c32 --- /dev/null +++ b/comsapi/app/src/db/models/tables/version.js @@ -0,0 +1,100 @@ +const { Model } = require('objection'); + +const { stamps } = require('../jsonSchema'); +const { Timestamps } = require('../mixins'); +const { filterOneOrMany, filterILike } = require('../utils'); + +class Version extends Timestamps(Model) { + static get tableName() { + return 'version'; + } + + static get relationMappings() { + const ObjectModel = require('./objectModel'); + const Metadata = require('./metadata'); + const Tag = require('./tag'); + + return { + object: { + relation: Model.HasOneRelation, + modelClass: ObjectModel, + join: { + from: 'version.objectId', + to: 'object.id' + } + }, + + metadata: { + relation: Model.ManyToManyRelation, + modelClass: Metadata, + join: { + from: 'version.id', + through: { + from: 'version_metadata.versionId', + to: 'version_metadata.metadataId' + }, + to: 'metadata.id' + } + }, + + tag: { + relation: Model.ManyToManyRelation, + modelClass: Tag, + join: { + from: 'version.id', + through: { + from: 'version_tag.versionId', + to: 'version_tag.tagId' + }, + to: 'tag.id' + } + } + }; + } + + static get modifiers() { + return { + filterDeleteMarker(query, value) { + if (value !== undefined) query.where('version.deleteMarker', value); + }, + filterETag(query, value) { + if (value) query.where('version.etag', value); + }, + filterId(query, value) { + filterOneOrMany(query, value, 'version.id'); + }, + filterIsLatest(query, value) { + if (value !== undefined) query.where('version.isLatest', value); + }, + filterMimeType(query, value) { + filterILike(query, value, 'version.mimeType'); + }, + filterObjectId(query, value) { + filterOneOrMany(query, value, 'version.objectId'); + }, + filterS3VersionId(query, value) { + filterOneOrMany(query, value, 'version.s3VersionId'); + }, + }; + } + + static get jsonSchema() { + return { + type: 'object', + // required: ['objectId'], + properties: { + id: { type: 'string', minLength: 1, maxLength: 255 }, + s3VersionId: { type: ['string', 'null'], maxLength: 1024 }, + objectId: { type: 'string', minLength: 1, maxLength: 255 }, + mimeType: { type: 'string', maxLength: 255 }, + deleteMarker: { type: 'boolean' }, + etag: { type: 'string', maxLength: 65536 }, + isLatest: { type: 'boolean' }, + ...stamps + }, + additionalProperties: false + }; + } +} + +module.exports = Version; diff --git a/comsapi/app/src/db/models/tables/versionMetadata.js b/comsapi/app/src/db/models/tables/versionMetadata.js new file mode 100644 index 00000000..cf2f503a --- /dev/null +++ b/comsapi/app/src/db/models/tables/versionMetadata.js @@ -0,0 +1,65 @@ +const { Model } = require('objection'); + +const { stamps } = require('../jsonSchema'); +const { Timestamps } = require('../mixins'); +const { filterOneOrMany } = require('../utils'); + +class VersionMetadata extends Timestamps(Model) { + static get tableName() { + return 'version_metadata'; + } + + static get idColumn() { + return ['versionId', 'metadataId']; + } + + static get relationMappings() { + const Version = require('./version'); + const Metadata = require('./metadata'); + + return { + version: { + relation: Model.HasOneRelation, + modelClass: Version, + join: { + from: 'version_metadata.versionId', + to: 'version.id' + } + }, + metadata: { + relation: Model.HasOneRelation, + modelClass: Metadata, + join: { + from: 'version_metadata.metadataId', + to: 'metadata.id' + } + }, + }; + } + + static get modifiers() { + return { + filterMetadataId(query, value) { + filterOneOrMany(query, value, 'metadataId'); + }, + filterVersionId(query, value) { + filterOneOrMany(query, value, 'versionId'); + } + }; + } + + static get jsonSchema() { + return { + type: 'object', + required: ['metadataId', 'versionId'], + properties: { + metadataId: { type: 'integer' }, + versionId: { type: 'string', minLength: 1, maxLength: 255 }, + ...stamps + }, + additionalProperties: false + }; + } +} + +module.exports = VersionMetadata; diff --git a/comsapi/app/src/db/models/tables/versionTag.js b/comsapi/app/src/db/models/tables/versionTag.js new file mode 100644 index 00000000..7a97524b --- /dev/null +++ b/comsapi/app/src/db/models/tables/versionTag.js @@ -0,0 +1,65 @@ +const { Model } = require('objection'); + +const { stamps } = require('../jsonSchema'); +const { Timestamps } = require('../mixins'); +const { filterOneOrMany } = require('../utils'); + +class VersionTag extends Timestamps(Model) { + static get tableName() { + return 'version_tag'; + } + + static get idColumn() { + return ['versionId', 'tagId']; + } + + static get relationMappings() { + const Version = require('./version'); + const Tag = require('./tag'); + + return { + version: { + relation: Model.HasOneRelation, + modelClass: Version, + join: { + from: 'version_tag.versionId', + to: 'version.id' + } + }, + tag: { + relation: Model.HasOneRelation, + modelClass: Tag, + join: { + from: 'version_tag.tagId', + to: 'tag.id' + } + }, + }; + } + + static get modifiers() { + return { + filterTagId(query, value) { + filterOneOrMany(query, value, 'tagId'); + }, + filterVersionId(query, value) { + filterOneOrMany(query, value, 'versionId'); + } + }; + } + + static get jsonSchema() { + return { + type: 'object', + required: ['tagId', 'versionId'], + properties: { + tagId: { type: 'integer' }, + versionId: { type: 'string', minLength: 1, maxLength: 255 }, + ...stamps + }, + additionalProperties: false + }; + } +} + +module.exports = VersionTag; diff --git a/comsapi/app/src/db/models/utils.js b/comsapi/app/src/db/models/utils.js new file mode 100644 index 00000000..9f324514 --- /dev/null +++ b/comsapi/app/src/db/models/utils.js @@ -0,0 +1,88 @@ +const { Model } = require('objection'); + +const utils = { + /** + * @function filterILike + * Conditionally adds a where or where in clause to the `query` if `value` is a string + * or an array of strings respectively + * @param {object} query The Objection Query Builder + * @param {string|string[]} value The string or array of string values to match on + * @param {string} column The table column to match on + */ + filterOneOrMany(query, value, column) { + if (value) { + if (Array.isArray(value) && value.length) { + query.whereIn(column, value); + } else { + query.where(column, value); + } + } + }, + + /** + * @function filterILike + * Conditionally adds a where ilike clause to the `query` builder if `value` is not falsy + * ilike is a Postgres keyword for case-insensitive matching + * @see {@link https://www.postgresql.org/docs/current/functions-matching.html} + * @param {object} query The Objection Query Builder + * @param {string} value The string value to match on + * @param {string} column The table column to match on + */ + filterILike(query, value, column) { + if (value) query.where(column, 'ilike', `%${value}%`); + }, + + inArrayClause(column, values) { + return values.map(p => `'${p}' = ANY("${column}")`).join(' or '); + }, + + inArrayFilter(column, values) { + const clause = utils.inArrayClause(column, values); + return `(array_length("${column}", 1) > 0 and (${clause}))`; + }, + + /** + * @function redactSecrets + * Sanitizes objects by replacing sensitive data with a REDACTED string value + * @param {object} data An arbitrary object + * @param {string[]} fields An array of field strings to sanitize on + * @returns {object} An arbitrary object with specified secret fields marked as redacted + */ + redactSecrets(data, fields) { + if (fields && Array.isArray(fields) && fields.length) { + fields.forEach(field => { + if (data[field]) data[field] = 'REDACTED'; + }); + } + return data; + }, + + toArray(values) { + if (values) { + return Array.isArray(values) ? values.filter(p => p && p.trim().length > 0) : [values].filter(p => p && p.trim().length > 0); + } + return []; + }, + + /** + * @function trx + * Wraps Objection/Knex queries in an Objection Transaction object + * @param {*} callback The objection queries that we want to enclose in a transaction + * @returns {Promise { + table.string('createdBy').defaultTo(SYSTEM_USER); + table.timestamp('createdAt', {useTz: true}).defaultTo(knex.fn.now()); + table.string('updatedBy'); + table.timestamp('updatedAt', {useTz: true}); +}; diff --git a/comsapi/app/src/docs/docs.js b/comsapi/app/src/docs/docs.js new file mode 100644 index 00000000..f4067aa4 --- /dev/null +++ b/comsapi/app/src/docs/docs.js @@ -0,0 +1,23 @@ +const docs = { + getDocHTML: version => ` + + + Common Object Management Service API - Documentation ${version} + + + + + + + + + + + + + ` +}; + +module.exports = docs; diff --git a/comsapi/app/src/docs/v1.api-spec.yaml b/comsapi/app/src/docs/v1.api-spec.yaml new file mode 100644 index 00000000..9891bfef --- /dev/null +++ b/comsapi/app/src/docs/v1.api-spec.yaml @@ -0,0 +1,2805 @@ +--- +openapi: 3.0.2 +info: + version: 1.0.0 + title: Common Object Management Service (COMS) + description: A microservice for managing access control to S3 Objects + license: + name: Apache 2.0 + url: https://www.apache.org/licenses/LICENSE-2.0.html + contact: + name: NR Common Service Showcase + email: NR.CommonServiceShowcase@gov.bc.ca +servers: + - url: /api/v1 + description: This Server +security: + - BasicAuth: [] + - BearerAuth: [] + OpenID: [] +tags: + - name: Bucket + description: Operations for managing S3 Bucket(s). + externalDocs: + url: >- + https://github.com/bcgov/common-object-management-service/wiki/Endpoint-Notes#bucket + - name: Metadata + description: Operations for managing Metadata for S3 Objects. + externalDocs: + url: >- + https://github.com/bcgov/common-object-management-service/wiki/Endpoint-Notes#metadata + - name: Object + description: Operations directly influencing an S3 Object. + externalDocs: + url: >- + https://github.com/bcgov/common-object-management-service/wiki/Endpoint-Notes#object + - name: Permission + description: Operations for managing S3 permissions. + externalDocs: + url: >- + https://github.com/bcgov/common-object-management-service/wiki/Endpoint-Notes#permission + - name: Sync + description: >- + Operations for syncing existing buckets and objects. Ensures that external + S3 modifications are eventually correctly tracked by COMS. + externalDocs: + url: >- + https://github.com/bcgov/common-object-management-service/wiki/Endpoint-Notes#sync + - name: Tagging + description: Operations for managing Tags for S3 Objects. + externalDocs: + url: >- + https://github.com/bcgov/common-object-management-service/wiki/Endpoint-Notes#tag + - name: User + description: Operations to list valid queryable users and identity providers. + externalDocs: + url: >- + https://github.com/bcgov/common-object-management-service/wiki/Endpoint-Notes#user + - name: Version + description: Operations to list object versions. + externalDocs: + url: >- + https://github.com/bcgov/common-object-management-service/wiki/Endpoint-Notes +paths: + /bucket: + put: + summary: Creates a bucket + description: >- + Creates a bucket record. Bucket should exist in S3. If the set of + `bucket`, `endpoint` and `key` match an existing record, the user will + be added to that existing bucket with full permissions instead of + generating a new bucket record. + operationId: createBucket + tags: + - Bucket + requestBody: + content: + application/json: + schema: + $ref: "#/components/schemas/Request-CreateBucket" + responses: + "201": + description: Returns inserted DB bucket record + content: + application/json: + schema: + $ref: "#/components/schemas/DB-Bucket" + "401": + $ref: "#/components/responses/Unauthorized" + "403": + $ref: "#/components/responses/Forbidden" + "409": + $ref: "#/components/responses/Conflict" + default: + $ref: "#/components/responses/Error" + get: + summary: Search for buckets + description: >- + Returns a list of buckets matching all search criteria across all bucket + records + operationId: searchBuckets + tags: + - Bucket + parameters: + - $ref: "#/components/parameters/Query-BucketId" + - $ref: "#/components/parameters/Query-Active" + - $ref: "#/components/parameters/Query-Key" + - $ref: "#/components/parameters/Query-BucketName" + responses: + "200": + description: Returns an array of buckets + content: + application/json: + schema: + type: array + items: + $ref: "#/components/schemas/DB-Bucket" + "401": + $ref: "#/components/responses/Unauthorized" + "403": + $ref: "#/components/responses/Forbidden" + default: + $ref: "#/components/responses/Error" + /bucket/{bucketId}: + head: + summary: Checks if a bucket exists in S3 + description: Returns either a 204 or 403 if user lacks permissions + operationId: headBucket + tags: + - Bucket + parameters: + - $ref: "#/components/parameters/Path-BucketId" + responses: + "204": + description: Returns object headers + "403": + $ref: "#/components/responses/Forbidden" + default: + $ref: "#/components/responses/Error" + get: + summary: Returns a bucket + description: Returns a bucket record based on bucketId + operationId: readBucket + tags: + - Bucket + parameters: + - $ref: "#/components/parameters/Path-BucketId" + responses: + "200": + description: Returns bucket + content: + application/json: + schema: + $ref: "#/components/schemas/DB-Bucket" + "403": + $ref: "#/components/responses/Forbidden" + default: + $ref: "#/components/responses/Error" + patch: + summary: Updates a bucket + description: Updates the bucket record + operationId: updateBucket + tags: + - Bucket + requestBody: + content: + application/json: + schema: + type: array + items: + $ref: "#/components/schemas/Request-UpdateBucket" + parameters: + - $ref: "#/components/parameters/Path-BucketId" + responses: + "200": + description: Returns updated bucket + content: + application/json: + schema: + $ref: "#/components/schemas/DB-Bucket" + "403": + $ref: "#/components/responses/Forbidden" + "409": + $ref: "#/components/responses/Conflict" + default: + $ref: "#/components/responses/Error" + delete: + summary: Deletes a bucket + description: >- + Deletes the bucket record based on bucketId. This request does not + dispatch an S3 operation to request the deletion of the associated + bucket. + operationId: deleteBucket + tags: + - Bucket + parameters: + - $ref: "#/components/parameters/Path-BucketId" + responses: + "204": + description: Returns no content success + "403": + $ref: "#/components/responses/Forbidden" + default: + $ref: "#/components/responses/Error" + /bucket/{bucketId}/sync: + get: + summary: Synchronize a bucket + description: >- + Synchronize the contents of an existing bucket with COMS so that it can + track and manage the objects in it. This avoids the need to re-upload + preexisting files through the COMS API. This endpoint does not guarantee + immediately synchronized results. Synchronization latency will be + affected by the remaining number of objects awaiting synchronization. + tags: + - Sync + operationId: syncBucket + parameters: + - $ref: "#/components/parameters/Path-BucketId" + responses: + "202": + $ref: "#/components/responses/AddedQueueLength" + "401": + $ref: "#/components/responses/Unauthorized" + "403": + $ref: "#/components/responses/Forbidden" + "422": + $ref: "#/components/responses/UnprocessableEntity" + default: + $ref: "#/components/responses/Error" + /metadata: + get: + summary: Search all metadata + description: >- + Gets a list of metadata matching the given parameters. Multiple + Key/Value pairs can be provided in the headers to narrow down results. + If none are provided the full set of metadata will be returned. + operationId: searchMetadata + tags: + - Metadata + parameters: + - $ref: "#/components/parameters/Header-Metadata" + responses: + "200": + description: Returns an array of matching key/value pairs. + content: + application/json: + schema: + type: array + items: + $ref: "#/components/schemas/DB-MetadataKeyValue" + "422": + $ref: "#/components/responses/UnprocessableEntity" + default: + $ref: "#/components/responses/Error" + /object: + put: + summary: Creates an object + description: >- + Creates an object in the bucket specified with bucketId parameter or + default bucket configured. If COMS is running in either 'OIDC' or 'Full' + mode, any objects created with OIDC user authentication will have all + object permissions assigned to them by default. This endpoint will do a + best attempt effort to create the object as requested as long as it does + not already exist in the destination bucket and path. Existing objects + should be managed with the updateObject endpoint instead. + operationId: createObject + tags: + - Object + parameters: + - $ref: "#/components/parameters/Header-ContentDisposition" + - $ref: "#/components/parameters/Header-ContentLength" + - $ref: "#/components/parameters/Header-ContentType" + - $ref: "#/components/parameters/Header-Metadata" + - $ref: "#/components/parameters/Query-TagSet" + - in: query + name: bucketId + description: Uuid representing the bucket + schema: + type: string + format: uuid + example: ac246e31-c807-496c-bc93-cd8bc2f1b2b4 + requestBody: + description: Raw binary representation of the file + required: true + content: + "*/*": + schema: + type: string + description: >- + Contains the binary representation of the file to upload. + Maximum filesize of 5TB. + format: binary + maxLength: 5497558138880 + responses: + "200": + description: Returns the created object data + content: + application/json: + schema: + $ref: "#/components/schemas/Response-ObjectDetails" + "400": + $ref: "#/components/responses/BadRequest" + "401": + $ref: "#/components/responses/Unauthorized" + "403": + $ref: "#/components/responses/Forbidden" + "409": + description: Conflict. Bucket already contains the object + content: + application/json: + schema: + $ref: "#/components/schemas/Response-Conflict" + "411": + $ref: "#/components/responses/LengthRequired" + "413": + $ref: "#/components/responses/ContentTooLarge" + "415": + $ref: "#/components/responses/UnsupportedMediaType" + "422": + $ref: "#/components/responses/UnprocessableEntity" + default: + $ref: "#/components/responses/Error" + get: + summary: Search for objects + description: >- + Returns a list of objects matching all search criteria across all known + versions of objects. Search criteria on string attributes will match on + partial results and ignore case sensitivity. + operationId: searchObjects + tags: + - Object + parameters: + - $ref: "#/components/parameters/Header-Metadata" + - $ref: "#/components/parameters/Query-ObjectId" + - $ref: "#/components/parameters/Query-BucketId" + - $ref: "#/components/parameters/Query-Path" + - $ref: "#/components/parameters/Query-Active" + - $ref: "#/components/parameters/Query-DeleteMarker" + - $ref: "#/components/parameters/Query-Latest" + - $ref: "#/components/parameters/Query-Public" + - $ref: "#/components/parameters/Query-MimeType" + - $ref: "#/components/parameters/Query-Name" + - $ref: "#/components/parameters/Query-TagSet" + responses: + "200": + description: Returns and array of objects + content: + application/json: + schema: + type: array + items: + $ref: "#/components/schemas/DB-Object" + "401": + $ref: "#/components/responses/Unauthorized" + "403": + $ref: "#/components/responses/Forbidden" + default: + $ref: "#/components/responses/Error" + /object/{objectId}: + get: + summary: Returns the object + description: >- + Returns the object as either a direct binary stream, an HTTP 201 + containing a direct, temporary pre-signed S3 object URL location, or an + HTTP 302 redirect to a direct, temporary pre-signed S3 object URL + location. + operationId: readObject + tags: + - Object + parameters: + - $ref: "#/components/parameters/Path-ObjectId" + - $ref: "#/components/parameters/Query-Download" + - $ref: "#/components/parameters/Query-ExpiresIn" + - $ref: "#/components/parameters/Query-S3VersionId" + - $ref: "#/components/parameters/Query-VersionId" + responses: + "200": + description: Returns the object + headers: + Content-Disposition: + $ref: "#/components/headers/Content-Disposition" + Content-Length: + $ref: "#/components/headers/Content-Length" + Content-Type: + $ref: "#/components/headers/Content-Type" + ETag: + $ref: "#/components/headers/ETag" + Last-Modified: + $ref: "#/components/headers/Last-Modified" + x-amz-server-side-encryption: + $ref: "#/components/headers/x-amz-server-side-encryption" + x-amz-version-id: + $ref: "#/components/headers/x-amz-version-id" + x-amz-meta-id: + $ref: "#/components/headers/x-amz-meta-id" + x-amz-meta-name: + $ref: "#/components/headers/x-amz-meta-name" + x-amz-meta-*: + description: Any additional metadata key/value pairs added to the object + schema: + type: string + maximum: 255 + content: + application/octet-stream: + schema: + type: string + format: binary + "201": + description: Returns a Presigned S3 URL + content: + application/json: + schema: + $ref: "#/components/schemas/Response-PresignedURL" + "302": + $ref: "#/components/responses/S3Found" + "304": + $ref: "#/components/responses/NotModified" + "401": + $ref: "#/components/responses/Unauthorized" + "403": + $ref: "#/components/responses/Forbidden" + "404": + $ref: "#/components/responses/NotFound" + default: + $ref: "#/components/responses/Error" + head: + summary: Returns object headers + description: Returns S3 and COMS headers for a specific object + operationId: headObject + tags: + - Object + parameters: + - $ref: "#/components/parameters/Path-ObjectId" + - $ref: "#/components/parameters/Query-S3VersionId" + - $ref: "#/components/parameters/Query-VersionId" + responses: + "204": + description: Returns object headers + headers: + Content-Disposition: + $ref: "#/components/headers/Content-Disposition" + Content-Length: + $ref: "#/components/headers/Content-Length" + Content-Type: + $ref: "#/components/headers/Content-Type" + ETag: + $ref: "#/components/headers/ETag" + Last-Modified: + $ref: "#/components/headers/Last-Modified" + x-amz-server-side-encryption: + $ref: "#/components/headers/x-amz-server-side-encryption" + x-amz-version-id: + $ref: "#/components/headers/x-amz-version-id" + x-amz-meta-id: + $ref: "#/components/headers/x-amz-meta-id" + x-amz-meta-name: + $ref: "#/components/headers/x-amz-meta-name" + x-amz-meta-*: + description: Any additional metadata key/value pairs added to the object + schema: + type: string + maximum: 255 + "401": + $ref: "#/components/responses/Unauthorized" + "403": + $ref: "#/components/responses/Forbidden" + default: + $ref: "#/components/responses/Error" + put: + summary: Updates an object + description: >- + Updates the object in the configured object storage. If the object + storage supports versioning, a new version will be generated instead of + overwriting the existing contents. + operationId: updateObject + tags: + - Object + parameters: + - $ref: "#/components/parameters/Header-ContentLength" + - $ref: "#/components/parameters/Header-ContentType" + - $ref: "#/components/parameters/Header-Metadata" + - $ref: "#/components/parameters/Path-ObjectId" + - $ref: "#/components/parameters/Query-TagSet" + requestBody: + description: Raw binary representation of the file + required: true + content: + "*/*": + schema: + type: string + description: >- + Contains the binary representation of the file to upload. + Maximum filesize of 5TB. + format: binary + maxLength: 5497558138880 + responses: + "200": + description: Returns the updated object data + content: + application/json: + schema: + $ref: "#/components/schemas/Response-ObjectDetails" + "400": + $ref: "#/components/responses/BadRequest" + "401": + $ref: "#/components/responses/Unauthorized" + "403": + $ref: "#/components/responses/Forbidden" + "409": + description: Conflict. Bucket does not contain the existing object + content: + application/json: + schema: + $ref: "#/components/schemas/Response-Conflict" + "411": + $ref: "#/components/responses/LengthRequired" + "413": + $ref: "#/components/responses/ContentTooLarge" + "422": + $ref: "#/components/responses/UnprocessableEntity" + default: + $ref: "#/components/responses/Error" + delete: + summary: Deletes an object or a version of object + description: >- + Deletes the specified object (or version) from S3. If the object storage + supports versioning, precise S3 version stack manipulation is supported, + including soft-deletion and soft-restore. Hard-deletions on S3 are also + supported. For more details on general S3 version behavior, visit + https://docs.aws.amazon.com/AmazonS3/latest/userguide/DeletingObjectVersions.html + operationId: deleteObject + tags: + - Object + parameters: + - $ref: "#/components/parameters/Path-ObjectId" + - $ref: "#/components/parameters/Query-S3VersionId" + - $ref: "#/components/parameters/Query-VersionId" + responses: + "200": + description: Object or version was deleted from object storage and COMS database + content: + application/json: + schema: + oneOf: + - $ref: "#/components/schemas/Response-ObjectDeleted" + - $ref: "#/components/schemas/Response-VersionDeleted" + "401": + $ref: "#/components/responses/Unauthorized" + "403": + $ref: "#/components/responses/Forbidden" + default: + $ref: "#/components/responses/Error" + /object/{objectId}/version: + get: + summary: List versions for an object + description: >- + Returns an array of an object's versions, including delete-markers, + ordered by creation date. + operationId: listObjectVersion + tags: + - Version + parameters: + - $ref: "#/components/parameters/Path-ObjectId" + responses: + "200": + description: Returns an array of versions for a specific object + content: + application/json: + schema: + type: array + items: + $ref: "#/components/schemas/DB-Version" + "401": + $ref: "#/components/responses/Unauthorized" + "403": + $ref: "#/components/responses/Forbidden" + default: + $ref: "#/components/responses/Error" + /object/{objectId}/public: + patch: + summary: Sets the public flag of an object + description: >- + Toggles the public property for an object. Sets public to false if + public query parameter is not specified. + operationId: togglePublic + tags: + - Object + parameters: + - $ref: "#/components/parameters/Path-ObjectId" + - $ref: "#/components/parameters/Query-Public" + responses: + "200": + description: Returns the object information + content: + application/json: + schema: + $ref: "#/components/schemas/DB-Object" + "401": + $ref: "#/components/responses/Unauthorized" + "403": + $ref: "#/components/responses/Forbidden" + default: + $ref: "#/components/responses/Error" + /object/{objectId}/metadata: + patch: + summary: Adds metadata to an object + description: >- + Creates a copy and new version of the object with the given metadata + added to the object. Multiple Key/Value pairs can be provided in the + header for the metadata. + operationId: addMetadata + tags: + - Metadata + parameters: + - $ref: "#/components/parameters/Header-Metadata" + - $ref: "#/components/parameters/Path-ObjectId" + - $ref: "#/components/parameters/Query-S3VersionId" + - $ref: "#/components/parameters/Query-VersionId" + responses: + "204": + $ref: "#/components/responses/NoContent" + "401": + $ref: "#/components/responses/Unauthorized" + "403": + $ref: "#/components/responses/Forbidden" + "422": + $ref: "#/components/responses/UnprocessableEntity" + default: + $ref: "#/components/responses/Error" + put: + summary: Replaces metadata of an object + description: >- + Creates a copy and new version of the object with the given metadata + replacing the existing. Multiple Key/Value pairs can be provided in the + header for the metadata. + operationId: replaceMetadata + tags: + - Metadata + parameters: + - $ref: "#/components/parameters/Header-Metadata" + - $ref: "#/components/parameters/Path-ObjectId" + - $ref: "#/components/parameters/Query-S3VersionId" + - $ref: "#/components/parameters/Query-VersionId" + responses: + "204": + $ref: "#/components/responses/NoContent" + "401": + $ref: "#/components/responses/Unauthorized" + "403": + $ref: "#/components/responses/Forbidden" + "422": + $ref: "#/components/responses/UnprocessableEntity" + default: + $ref: "#/components/responses/Error" + delete: + summary: Delete metadata of an object. + description: >- + Creates a copy and new version of the object with the given metadata + removed. Multiple Key/Value pairs can be provided in the header for the + metadata. If no metadata headers are given then all metadata will be + removed. Metadata headers `name` and `id` are mandatory and will always + persist. + operationId: deleteMetadata + tags: + - Metadata + parameters: + - $ref: "#/components/parameters/Header-Metadata" + - $ref: "#/components/parameters/Path-ObjectId" + - $ref: "#/components/parameters/Query-S3VersionId" + - $ref: "#/components/parameters/Query-VersionId" + responses: + "204": + $ref: "#/components/responses/NoContent" + "401": + $ref: "#/components/responses/Unauthorized" + "403": + $ref: "#/components/responses/Forbidden" + default: + $ref: "#/components/responses/Error" + /object/{objectId}/sync: + get: + summary: Synchronize an existing object + description: >- + Synchronize an existing COMS object so that any changes made externally + (e.g. using an S3 client to upload a new version) are tracked by COMS. + This endpoint does not guarantee immediately synchronized results. + Synchronization latency will be affected by the remaining number of + objects awaiting synchronization. + tags: + - Sync + operationId: syncObject + parameters: + - $ref: "#/components/parameters/Path-ObjectId" + responses: + "202": + description: Returns the number of objects that have been added to the queue. + content: + text/plain: + schema: + type: integer + example: 5 + description: number of objects added to the queue + "401": + $ref: "#/components/responses/Unauthorized" + "403": + $ref: "#/components/responses/Forbidden" + "422": + $ref: "#/components/responses/UnprocessableEntity" + default: + $ref: "#/components/responses/Error" + /object/{objectId}/tagging: + patch: + summary: Adds tags to an object + description: >- + Adds a specified set of tags to the object. Multiple Key/Value pairs can + be provided in the query. + operationId: addTagging + tags: + - Tagging + parameters: + - $ref: "#/components/parameters/Path-ObjectId" + - $ref: "#/components/parameters/Query-TagSet" + - $ref: "#/components/parameters/Query-S3VersionId" + - $ref: "#/components/parameters/Query-VersionId" + responses: + "204": + $ref: "#/components/responses/NoContent" + "401": + $ref: "#/components/responses/Unauthorized" + "403": + $ref: "#/components/responses/Forbidden" + "409": + $ref: "#/components/responses/Conflict" + "422": + $ref: "#/components/responses/UnprocessableEntity" + default: + $ref: "#/components/responses/Error" + put: + summary: Replaces tags of an object + description: >- + Replace the existing tag-set of an object with the set of given tags. + Multiple Key/Value pairs can be provided in the query. + operationId: replaceTagging + tags: + - Tagging + parameters: + - $ref: "#/components/parameters/Path-ObjectId" + - $ref: "#/components/parameters/Query-TagSet" + - $ref: "#/components/parameters/Query-S3VersionId" + - $ref: "#/components/parameters/Query-VersionId" + responses: + "204": + $ref: "#/components/responses/NoContent" + "401": + $ref: "#/components/responses/Unauthorized" + "403": + $ref: "#/components/responses/Forbidden" + "422": + $ref: "#/components/responses/UnprocessableEntity" + default: + $ref: "#/components/responses/Error" + delete: + summary: Delete tags of an object. + description: >- + Removes the specified set of tags from the object. Multiple Key/Value + pairs can be provided in the query. All tags in the tag-set will be + removed from the object if no tags are specified. + operationId: deleteTagging + tags: + - Tagging + parameters: + - $ref: "#/components/parameters/Path-ObjectId" + - $ref: "#/components/parameters/Query-TagSet" + - $ref: "#/components/parameters/Query-S3VersionId" + - $ref: "#/components/parameters/Query-VersionId" + responses: + "204": + $ref: "#/components/responses/NoContent" + "401": + $ref: "#/components/responses/Unauthorized" + "403": + $ref: "#/components/responses/Forbidden" + default: + $ref: "#/components/responses/Error" + /object/metadata: + get: + summary: List metadata for objects + description: >- + Gets a list of metadata matching the given parameters. Multiple + Key/Value metadata pairs can be provided in the query to narrow down + results. If none are provided all metadata will be returned for each + object. + operationId: fetchMetadata + tags: + - Metadata + parameters: + - $ref: "#/components/parameters/Header-Metadata" + - $ref: "#/components/parameters/Query-BucketId" + - $ref: "#/components/parameters/Query-ObjectId" + responses: + "200": + description: Returns an array objects with matching metadata key/value pairs. + content: + application/json: + schema: + type: array + items: + type: object + required: + - objectId + - metadata + properties: + objectId: + type: string + format: uuid + example: ac246e31-c807-496c-bc93-cd8bc2f1b2b4 + metadata: + type: array + items: + $ref: "#/components/schemas/DB-MetadataKeyValue" + "422": + $ref: "#/components/responses/UnprocessableEntity" + default: + $ref: "#/components/responses/Error" + /object/tagging: + get: + summary: List Tags for objects + description: >- + Gets a list of tags matching the given parameters. Multiple Key/Value + metadata pairs can be provided in the query to narrow down results. If + none are provided all tags will be returned for each object. + operationId: fetchTags + tags: + - Tagging + parameters: + - $ref: "#/components/parameters/Query-BucketId" + - $ref: "#/components/parameters/Query-ObjectId" + - $ref: "#/components/parameters/Query-TagSet" + responses: + "200": + description: Returns an array objects with matching key/value tag pairs. + content: + application/json: + schema: + type: array + items: + type: object + required: + - objectId + - tagset + properties: + objectId: + type: string + format: uuid + example: ac246e31-c807-496c-bc93-cd8bc2f1b2b4 + tagset: + type: array + items: + $ref: "#/components/schemas/DB-TagKeyValue" + "422": + $ref: "#/components/responses/UnprocessableEntity" + default: + $ref: "#/components/responses/Error" + /permission/bucket: + get: + summary: Searches for bucket permissions + description: >- + Returns an array of buckets and their permissions meeting the filtering + parameters provided. Use `objectPerms=true` in conjunction with a userId + to get an extended bucket list factoring in object inheritance. + operationId: bucketSearchPermissions + tags: + - Permission + parameters: + - $ref: "#/components/parameters/Query-BucketId" + - $ref: "#/components/parameters/Query-UserId" + - $ref: "#/components/parameters/Query-PermCode" + - $ref: "#/components/parameters/Query-ObjectPerms" + responses: + "200": + description: >- + Returns an array of bucketIds and permissions that match the + provided parameters + content: + application/json: + schema: + type: array + items: + anyOf: + - $ref: "#/components/schemas/Response-BucketPerms" + - $ref: "#/components/schemas/Response-BucketPermsWithObject" + "401": + $ref: "#/components/responses/Unauthorized" + "403": + $ref: "#/components/responses/Forbidden" + default: + $ref: "#/components/responses/Error" + /permission/bucket/{bucketId}: + get: + summary: Returns the bucket permissions + description: >- + Returns an array of permissions for a specific bucket meeting the + filtering parameters provided + operationId: bucketListPermissions + tags: + - Permission + parameters: + - $ref: "#/components/parameters/Query-UserId" + - $ref: "#/components/parameters/Path-BucketId" + - $ref: "#/components/parameters/Query-PermCode" + responses: + "200": + description: >- + Returns an array of bucketId/userId/permCode triplets that match the + provided parameters + content: + application/json: + schema: + type: array + items: + $ref: "#/components/schemas/DB-BucketPermission" + "401": + $ref: "#/components/responses/Unauthorized" + "403": + $ref: "#/components/responses/Forbidden" + default: + $ref: "#/components/responses/Error" + put: + summary: Grants bucket permissions to users + description: >- + Adds permissions for a specific bucket to users by specifying an + arbitrary array of permCode and user tuples. This is an idempotent + operation, so users that already have a requested permission will remain + unaffected. Only permissions successfully added to COMS will appear in + the response. + operationId: bucketAddPermissions + tags: + - Permission + parameters: + - $ref: "#/components/parameters/Path-BucketId" + requestBody: + description: >- + An array of bucket permissions, each containing a `userId` and + `permCode` + content: + application/json: + schema: + type: array + items: + $ref: "#/components/schemas/Request-PermissionTuple" + responses: + "201": + description: Returns an array of added permissions + content: + application/json: + schema: + type: array + items: + $ref: "#/components/schemas/DB-BucketPermission" + "401": + $ref: "#/components/responses/Unauthorized" + "403": + $ref: "#/components/responses/Forbidden" + default: + $ref: "#/components/responses/Error" + delete: + summary: Deletes permissions for a bucket + description: >- + Removes permissions for a specific bucket by optionally specifying a set + of users and subset of permissions to revoke. This is an idempotent + operation, so users that already lack the specified permission(s) will + remain unaffected. Only permissions successfully removed from COMS will + appear in the response. WARNING: Specifying no parameters will delete + all permissions associated with a bucket; it is possible to lock + yourself out of your own bucket! + operationId: bucketRemovePermissions + tags: + - Permission + parameters: + - $ref: "#/components/parameters/Path-BucketId" + - $ref: "#/components/parameters/Query-UserId" + - $ref: "#/components/parameters/Query-PermCode" + responses: + "200": + description: Returns an array of deleted permissions + content: + application/json: + schema: + type: array + items: + $ref: "#/components/schemas/DB-BucketPermission" + "401": + $ref: "#/components/responses/Unauthorized" + "403": + $ref: "#/components/responses/Forbidden" + default: + $ref: "#/components/responses/Error" + /permission/object: + get: + summary: Search for object permissions + description: >- + Returns an array of permissions meeting the filtering parameters + provided. Use `bucketPerms=true` in conjunction with a userId to get an + extended object list factoring in bucket inheritance. + operationId: objectSearchPermissions + tags: + - Permission + parameters: + - $ref: "#/components/parameters/Query-BucketId" + - $ref: "#/components/parameters/Query-ObjectId" + - $ref: "#/components/parameters/Query-UserId" + - $ref: "#/components/parameters/Query-PermCode" + - $ref: "#/components/parameters/Query-BucketPerms" + responses: + "200": + description: >- + Returns an array of objectId/userId/permCode triplets that match the + provided parameters + content: + application/json: + schema: + type: array + items: + anyOf: + - $ref: "#/components/schemas/Response-ObjectPerms" + - $ref: "#/components/schemas/Response-ObjectPermsWithBucket" + "401": + $ref: "#/components/responses/Unauthorized" + "403": + $ref: "#/components/responses/Forbidden" + default: + $ref: "#/components/responses/Error" + /permission/object/{objectId}: + get: + summary: Returns the object permissions + description: >- + Returns an array of object permissions for a specific object meeting the + filtering parameters provided + operationId: objectListPermissions + tags: + - Permission + parameters: + - $ref: "#/components/parameters/Path-ObjectId" + - $ref: "#/components/parameters/Query-UserId" + - $ref: "#/components/parameters/Query-PermCode" + responses: + "200": + description: >- + Returns an array of objectId/userId/permCode triplets that match the + provided parameters + content: + application/json: + schema: + type: array + items: + $ref: "#/components/schemas/DB-ObjectPermission" + "401": + $ref: "#/components/responses/Unauthorized" + "403": + $ref: "#/components/responses/Forbidden" + default: + $ref: "#/components/responses/Error" + put: + summary: Grants object permissions to users + description: >- + Adds permissions for a specific object to users by specifying an + arbitrary array of permCode and user tuples. This is an idempotent + operation, so users that already have a requested permission will remain + unaffected. Only permissions successfully added to COMS will appear in + the response. + operationId: objectAddPermissions + tags: + - Permission + parameters: + - $ref: "#/components/parameters/Path-ObjectId" + requestBody: + description: An array of objects, each containing a `permCode` and `userId` + content: + application/json: + schema: + type: array + items: + $ref: "#/components/schemas/Request-PermissionTuple" + responses: + "201": + description: Returns an array of added permissions + content: + application/json: + schema: + type: array + items: + $ref: "#/components/schemas/DB-ObjectPermission" + "401": + $ref: "#/components/responses/Unauthorized" + "403": + $ref: "#/components/responses/Forbidden" + default: + $ref: "#/components/responses/Error" + delete: + summary: Deletes permissions for an object + description: >- + Removes permissions for a specific object by optionally specifying a set + of users and subset of permissions to revoke. This is an idempotent + operation, so users that already lack the specified permission(s) will + remain unaffected. Only permissions successfully removed from COMS will + appear in the response. WARNING: Specifying no parameters will delete + all permissions associated with an object; it is possible to lock + yourself out of your own object! + operationId: objectRemovePermissions + tags: + - Permission + parameters: + - $ref: "#/components/parameters/Path-ObjectId" + - $ref: "#/components/parameters/Query-UserId" + - $ref: "#/components/parameters/Query-PermCode" + responses: + "200": + description: Returns an array of deleted permissions + content: + application/json: + schema: + type: array + items: + $ref: "#/components/schemas/DB-ObjectPermission" + "401": + $ref: "#/components/responses/Unauthorized" + "403": + $ref: "#/components/responses/Forbidden" + default: + $ref: "#/components/responses/Error" + /sync: + get: + summary: Synchronize with the default bucket + description: >- + Synchronize the contents of the default bucket with COMS so that it can + track and manage objects that are already in the bucket. This avoids the + need to re-upload preexisting files through the COMS API. This endpoint + does not guarantee immediately synchronized results. Synchronization + latency will be affected by the remaining number of objects awaiting + synchronization. + operationId: syncDefault + tags: + - Sync + responses: + "202": + $ref: "#/components/responses/AddedQueueLength" + "401": + $ref: "#/components/responses/Unauthorized" + "403": + $ref: "#/components/responses/Forbidden" + default: + $ref: "#/components/responses/Error" + /sync/status: + get: + summary: Get sync queue size + description: Returns the number of objects currently awaiting processing by COMS. + operationId: syncStatus + tags: + - Sync + responses: + "200": + description: Returns the number of objects currently in the queue + content: + text/plain: + schema: + type: integer + example: 5 + description: number of objects currently in the queue + "401": + $ref: "#/components/responses/Unauthorized" + "403": + $ref: "#/components/responses/Forbidden" + default: + $ref: "#/components/responses/Error" + /tagging: + get: + summary: Search all tags + description: >- + Gets a list of tags matching the given parameters. Multiple Key/Value + pairs can be provided in the headers to narrow down results. If none are + provided the full set of tags will be returned. + operationId: searchTags + tags: + - Tagging + parameters: + - $ref: "#/components/parameters/Query-TagSet" + responses: + "200": + description: Returns an array of matching key/value pairs. + content: + application/json: + schema: + type: array + items: + $ref: "#/components/schemas/DB-TagKeyValue" + "422": + $ref: "#/components/responses/UnprocessableEntity" + default: + $ref: "#/components/responses/Error" + /user: + get: + summary: Search for users + description: >- + Returns a list of users based on the provided filtering parameters. At + least one query parameter must be present. + operationId: searchUsers + tags: + - User + parameters: + - $ref: "#/components/parameters/Query-UserId" + - $ref: "#/components/parameters/Query-IdentityId" + - $ref: "#/components/parameters/Query-Idp" + - $ref: "#/components/parameters/Query-Username" + - $ref: "#/components/parameters/Query-Email" + - $ref: "#/components/parameters/Query-FirstName" + - $ref: "#/components/parameters/Query-FullName" + - $ref: "#/components/parameters/Query-LastName" + - $ref: "#/components/parameters/Query-Active" + - $ref: "#/components/parameters/Query-UserSearch" + responses: + "200": + description: >- + Returns a JSON object representation of the user matching provided + parameters + content: + application/json: + schema: + type: array + items: + $ref: "#/components/schemas/DB-User" + "401": + $ref: "#/components/responses/Unauthorized" + "403": + $ref: "#/components/responses/Forbidden" + default: + $ref: "#/components/responses/Error" + /user/idpList: + get: + summary: Lists all identity providers + description: Returns a list of all currently known identity providers + operationId: listIdps + tags: + - User + parameters: + - $ref: "#/components/parameters/Query-Active" + responses: + "200": + description: Returns a JSON array of known identity providers + content: + application/json: + schema: + type: array + items: + $ref: "#/components/schemas/DB-IdentityProvider" + "401": + $ref: "#/components/responses/Unauthorized" + "403": + $ref: "#/components/responses/Forbidden" + default: + $ref: "#/components/responses/Error" + /version/metadata: + get: + summary: List metadata for versions + description: >- + Gets a metadata matching the given versions. Multiple Key/Value pairs + can be provided in the headers to narrow down results. If none are + provided the full set of metadata will be returned. + operationId: fetchMetadataForVersion + tags: + - Metadata + parameters: + - $ref: "#/components/parameters/Header-Metadata" + - in: query + name: s3VersionId + description: >- + One or an array of version identifiers created in S3. This parameter + is incompatible with `versionId` + schema: + oneOf: + - $ref: "#/components/schemas/S3VersionId" + - type: array + items: + $ref: "#/components/schemas/S3VersionId" + - in: query + name: versionId + description: >- + One or an array of version identifiers created in COMS. This + parameter is incompatible with `s3VersionId` + schema: + oneOf: + - $ref: "#/components/schemas/VersionId" + - type: array + items: + $ref: "#/components/schemas/VersionId" + responses: + "200": + description: Returns an array of versions with matching key/value pairs. + content: + application/json: + schema: + type: array + items: + type: object + required: + - versionId + - metadata + properties: + versionId: + type: string + format: uuid + example: ac246e31-c807-496c-bc93-cd8bc2f1b2b4 + metadata: + type: array + items: + $ref: "#/components/schemas/DB-MetadataKeyValue" + "422": + $ref: "#/components/responses/UnprocessableEntity" + default: + $ref: "#/components/responses/Error" + /version/tagging: + get: + summary: List tags for versions + description: >- + Gets matching tags for matching versions. Multiple Key/Value pairs can + be provided in the headers to narrow down results. If none are provided + the full set of tags will be returned. + operationId: fetchTagsForVersion + tags: + - Tagging + parameters: + - $ref: "#/components/parameters/Query-TagSet" + - in: query + name: s3VersionId + description: >- + One or an array of version identifiers created in S3. This parameter + is incompatible with `versionId` + schema: + oneOf: + - $ref: "#/components/schemas/S3VersionId" + - type: array + items: + $ref: "#/components/schemas/S3VersionId" + - in: query + name: versionId + description: >- + One or an array of version identifiers created in COMS. This + parameter is incompatible with `s3VersionId` + schema: + oneOf: + - $ref: "#/components/schemas/VersionId" + - type: array + items: + $ref: "#/components/schemas/VersionId" + responses: + "200": + description: Returns an array of versions with matching key/value pairs. + content: + application/json: + schema: + type: array + items: + type: object + required: + - versionId + - tagset + properties: + versionId: + type: string + format: uuid + example: ac246e31-c807-496c-bc93-cd8bc2f1b2b4 + tagset: + type: array + items: + $ref: "#/components/schemas/DB-TagKeyValue" + "422": + $ref: "#/components/responses/UnprocessableEntity" + default: + $ref: "#/components/responses/Error" +components: + headers: + Content-Disposition: + description: Specifies presentational information for the object + schema: + type: string + example: >- + attachment; filename=bittercherrytree.txt; + filename*=UTF-8''skwc%E2%80%99%D3%99nj%C3%AD%C5%82c.txt + Content-Length: + description: Size of the file body in bytes + schema: + type: integer + maximum: 5497558138880 + example: 529 + Content-Type: + description: A standard MIME type describing the format of the object data + schema: + type: string + example: text/plain; charset=utf-8 + ETag: + description: >- + An entity tag (ETag) is an opaque identifier assigned by a web server to + a specific version of a resource found at a URL + schema: + type: integer + example: '"9d1aaa54b84e1d6ccc6e0477c5717fe3"' + Last-Modified: + description: Creation date of the object + schema: + type: string + format: date-time + example: Fri March 11 2022 15:42:05 GMT-0700 (Pacific Daylight Time) + Location: + description: URL Location of the moved resource + schema: + type: string + example: >- + https://your.objectstore.com/yourbucket/coms/env/00000000-0000-0000-0000-000000000000?X-Amz-Algorithm=AWS4-HMAC-SHA256&X-Amz-Content-Sha256=UNSIGNED-PAYLOAD&X-Amz-Credential=credential%2F20220411%2Fus-east-1%2Fs3%2Faws4_request&X-Amz-Date=20220411T204528Z&X-Amz-Expires=300&X-Amz-Signature=SIGNATURE&X-Amz-SignedHeaders=host&x-id=GetObject + x-amz-server-side-encryption: + description: >- + If the object is stored using server-side encryption either with an AWS + KMS key or an Amazon S3-managed encryption key, the response includes + this header with the value of the server-side encryption algorithm used + when storing this object in Amazon S3 (for example, AES256, aws:kms). + schema: + type: string + example: AES256 + x-amz-version-id: + description: Version of the object + schema: + type: string + example: 1649457725874 + x-amz-meta-id: + description: The Object ID + schema: + type: string + example: ac246e31-c807-496c-bc93-cd8bc2f1b2b4 + x-amz-meta-name: + description: The original filename of the object + schema: + type: string + example: foobar.txt + parameters: + Header-ContentDisposition: + in: header + name: Content-Disposition + required: true + schema: + type: string + description: >- + A valid RFC 6266 header. Must be of type `attachment` with either a + `filename` or `filename*` parameter using the encoding defined in RFC + 8187 (normally UTF-8). For example, `skwc’әnjíłc.txt` must be + represented as a url-encoded string + `skwc%E2%80%99%D3%99nj%C3%AD%C5%82c.txt`. + example: >- + attachment; filename=bittercherrytree.txt; + filename*=UTF-8''skwc%E2%80%99%D3%99nj%C3%AD%C5%82c.txt + Header-ContentLength: + in: header + name: Content-Length + required: true + schema: + type: integer + description: A valid RFC 2616 header. Must be present for PUT requests. + maximum: 5497558138880 + example: 3495 + Header-ContentType: + in: header + name: Content-Type + schema: + type: string + description: A valid RFC 2045 MIME type + default: application/octet-stream + example: application/octet-stream + Header-Metadata: + in: header + name: x-amz-meta-* + description: >- + An arbitrary metadata key/value pair. Must contain the x-amz-meta- + prefix to be valid. Multiple metadata pairs can be defined. Keys must be + unique, contain no whitespace, and will be converted to lowercase. + schema: + type: string + minimum: 1 + example: + - x-amz-meta-foo + - x-amz-meta-bar + - x-amz-meta-baz + Path-BucketId: + in: path + name: bucketId + description: Uuid of a bucket + required: true + schema: + type: string + format: uuid + example: ac246e31-c807-496c-bc93-cd8bc2f1b2b4 + Path-ObjectId: + in: path + name: objectId + description: Uuid of an object + required: true + schema: + type: string + format: uuid + example: 48a65990-2e48-46b2-94eb-7f4fe13468ea + Query-DeleteMarker: + in: query + name: deleteMarker + description: Boolean on object version DeleteMarker status + schema: + type: boolean + example: true + Query-Latest: + in: query + name: latest + description: Boolean on object version is latest + schema: + type: boolean + example: true + Query-Active: + in: query + name: active + description: Boolean on active status + schema: + type: boolean + example: true + Query-Download: + in: query + name: download + description: >- + Download mode behavior. Default behavior (undefined) will yield an HTTP + 302 redirect to the S3 bucket via presigned URL. If `proxy` is + specified, the object contents will be available proxied through COMS. + If `url` is specified, expect an HTTP 201 cotaining the presigned URL as + a JSON string in the response. + schema: + $ref: "#/components/schemas/DownloadMode" + Query-Email: + in: query + name: email + description: Search by specific email + schema: + type: string + example: bobsmith@gov.bc.ca + Query-ExpiresIn: + in: query + name: expiresIn + description: How many seconds the pre-signed URL should remain valid for + schema: + type: integer + format: int32 + default: 300 + example: 300 + Query-IdentityId: + in: query + name: identityId + description: >- + String or array of strings representing the user (identified by optional + KC_IDENTITYKEY env variable) + schema: + oneOf: + - type: string + example: 5dad1ec9d3c04b0f8eadcb4d9fa98987 + - type: array + items: + type: string + example: 5dad1ec9d3c04b0f8eadcb4d9fa98987 + Query-FirstName: + in: query + name: firstName + description: Search by specific first name + schema: + type: string + example: bob + Query-FullName: + in: query + name: fullName + description: Search by specific full name + schema: + type: string + example: bob smith + Query-Idp: + in: query + name: idp + description: >- + Idp or array of idps representing the identity provider(s) (i.e. `idir`, + `bceid-basic`, etc) + schema: + oneOf: + - type: string + example: idir + - type: array + items: + type: string + example: idir + Query-LastName: + in: query + name: lastName + description: Search by specific last name + schema: + type: string + example: smith + Query-MimeType: + in: query + name: mimeType + description: The object MIME Type + schema: + type: string + description: The object MIME Type + default: application/octet-stream + example: text/plain + Query-ObjectId: + in: query + name: objectId + description: Uuid or array of uuids representing the object + schema: + oneOf: + - type: string + format: uuid + example: ac246e31-c807-496c-bc93-cd8bc2f1b2b4 + - type: array + items: + type: string + format: uuid + example: ac246e31-c807-496c-bc93-cd8bc2f1b2b4 + Query-BucketId: + in: query + name: bucketId + description: Uuid or array of uuids representing the bucket + schema: + oneOf: + - type: string + format: uuid + example: ac246e31-c807-496c-bc93-cd8bc2f1b2b4 + - type: array + items: + type: string + format: uuid + example: ac246e31-c807-496c-bc93-cd8bc2f1b2b4 + Query-BucketName: + in: query + name: name + description: A display name given to the bucket on creation + schema: + type: string + example: Environmental Documents + Query-BucketPerms: + in: query + name: bucketPerms + description: >- + Boolean representing whether or not to include objects from buckets with + matching permissions + schema: + type: boolean + example: true + Query-Key: + in: query + name: key + description: The prefix given to objects in the bucket + schema: + type: string + example: coms/env + Query-Name: + in: query + name: name + description: The filename of the object (typically the original filename) + schema: + type: string + example: foobar.txt + Query-ObjectPerms: + in: query + name: objectPerms + description: >- + Boolean representing whether or not to include buckets containing + objects with matching permissions + schema: + type: boolean + example: true + Query-Path: + in: query + name: path + description: The canonical S3 path string of the object + schema: + type: string + example: coms/env/ac246e31-c807-496c-bc93-cd8bc2f1b2b4 + Query-PermCode: + in: query + name: permCode + description: The permission type + schema: + oneOf: + - $ref: "#/components/schemas/PermCode" + - type: array + items: + $ref: "#/components/schemas/PermCode" + Query-Public: + in: query + name: public + description: Boolean on public status + schema: + type: boolean + example: true + Query-TagSet: + in: query + name: tagset[*] + description: >- + Tags for the object, defined as a Key/Value tag. The query must be + formatted in deepObject style notation, where a tag-set made out of + multiple tags would be encoded something similar to + `tagset[x]=a&tagset[y]=b`. Only one value can exist for a given tag key. + schema: + $ref: "#/components/schemas/DB-TagKeyValue" + style: deepObject + explode: true + Query-UserSearch: + in: query + name: search + description: >- + General OR search across username, email and fullName. Intended for use + in freetext searches or autofilling fields. + schema: + type: string + example: smith + Query-UserId: + in: query + name: userId + description: Uuid or array of uuids representing the user + schema: + oneOf: + - type: string + format: uuid + example: 00000000-0000-0000-0000-000000000000 + - type: array + items: + type: string + format: uuid + example: 00000000-0000-0000-0000-000000000000 + Query-Username: + in: query + name: username + description: Search by specific username + schema: + type: string + example: bobsmith + Query-S3VersionId: + in: query + name: s3VersionId + description: A specified version + schema: + $ref: "#/components/schemas/S3VersionId" + Query-VersionId: + in: query + name: versionId + description: A specified version + schema: + $ref: "#/components/schemas/VersionId" + schemas: + DB-Bucket: + title: DB Bucket + type: object + allOf: + - type: object + required: + - accessKeyId + - active + - bucket + - bucketId + - bucketName + - endpoint + - key + - region + - secretAccessKey + properties: + accessKeyId: + type: string + description: >- + S3 account Access Key Id, similar to a username. This value will + be redacted from the API response. + example: REDACTED + active: + type: boolean + description: Determines whether this bucket is considered active in COMS + default: true + example: true + bucket: + type: string + description: The ID of a bucket + example: ezakyp + bucketName: + type: string + description: A human-readable label or title + example: Geospatial Maps + bucketId: + type: string + description: The primary identifier for this bucket + format: uuid + example: ac246e31-c807-496c-bc93-cd8bc2f1b2b4 + endpoint: + type: string + description: A valid RFC 3986 formatted S3 Service URI endpoint + maxLength: 255 + example: https://my-objectstore.ca + key: + description: The prefix given to objects in the bucket + type: string + example: coms/env + maxLength: 255 + region: + type: string + description: >- + The geographical zone defined by the S3 service where your + bucket is located + example: ca-central-1 + secretAccessKey: + type: string + description: >- + S3 account Secret Access Key, similar to a password. This value + will be redacted from the API response. + example: REDACTED + - $ref: "#/components/schemas/DB-TimestampUserData" + DB-BucketPermission: + title: DB Bucket Permission + type: object + allOf: + - type: object + required: + - id + - bucketId + - userId + - permCode + properties: + id: + type: string + format: uuid + description: The unique identifier for this permission tuple + example: ac246e31-c807-496c-bc93-cd8bc2f1b2b4 + bucketId: + type: string + format: uuid + description: The unique identifier for the bucket + example: ac246e31-c807-496c-bc93-cd8bc2f1b2b4 + userId: + type: string + format: uuid + description: The unique identifier of the user + example: ac246e31-c807-496c-bc93-cd8bc2f1b2b4 + permCode: + $ref: "#/components/schemas/PermCode" + - $ref: "#/components/schemas/DB-TimestampUserData" + DB-IdentityProvider: + title: DB Identity provider + type: object + allOf: + - type: object + required: + - idp + - active + properties: + idp: + type: string + description: The identity provider string + example: idir + active: + type: boolean + description: Determines whether this identity provider is considered active + default: true + example: true + - $ref: "#/components/schemas/DB-TimestampUserData" + DB-Metadata: + title: DB Metadata + type: object + required: + - metadata + properties: + metadata: + type: object + description: User-defined metadata + example: + color: red + department: finance + DB-MetadataKeyValue: + title: DB Metadata Key Value + type: object + required: + - key + - value + properties: + key: + type: string + description: The metadata key + example: x + value: + type: string + description: The metadata value + example: a + DB-Object: + title: DB Object + type: object + allOf: + - type: object + required: + - id + - path + - public + - active + properties: + id: + type: string + description: The primary identifier for this object + format: uuid + example: ac246e31-c807-496c-bc93-cd8bc2f1b2b4 + path: + type: string + description: The canonical S3 path string of the object + example: coms/env/foobar.txt + public: + type: boolean + description: Determines whether this object is publicly accessible + default: false + example: false + active: + type: boolean + description: Determines whether this object is considered active + default: true + example: true + bucketId: + type: string + description: The primary identifier for the bucket + format: uuid + example: c05c7650-5f48-4e51-bf17-762e8fc121a1 + name: + type: string + description: The filename of the original file uploaded + example: foobar.txt + - $ref: "#/components/schemas/DB-TimestampUserData" + DB-ObjectPermission: + title: DB Object Permission + type: object + allOf: + - type: object + required: + - id + - objectId + - userId + - permCode + properties: + id: + type: string + format: uuid + description: The unique identifier for this permission tuple + example: ac246e31-c807-496c-bc93-cd8bc2f1b2b4 + objectId: + type: string + format: uuid + description: The unique identifier for the object + example: ac246e31-c807-496c-bc93-cd8bc2f1b2b4 + userId: + type: string + format: uuid + description: The unique identifier of the user + example: ac246e31-c807-496c-bc93-cd8bc2f1b2b4 + permCode: + $ref: "#/components/schemas/PermCode" + - $ref: "#/components/schemas/DB-TimestampUserData" + DB-TagKeyValue: + title: DB Tag Key Value + type: object + required: + - key + properties: + key: + type: string + description: The tag key + example: x + value: + type: string + description: The tag value + example: a + DB-Tags: + title: DB Tags + type: object + allOf: + - type: object + properties: + tags: + type: object + description: User-defined tags + example: + classification: Top Secret + coms-id: 989de109-1487-423b-bf82-bee38c26ee3c + priority: urgent + DB-TimestampUserData: + title: DB Timestamp User Data + type: object + required: + - createdBy + - createdAt + properties: + createdBy: + type: string + format: uuid + description: >- + The subject id of the current user if request was authenticated with + a Bearer token (ex. JWT), or a 'nil' uuid if request was + authenticated via Basic auth + example: 72cf13d7-aef7-4ad5-8ddd-0ff23eefb442 + createdAt: + type: string + format: date-time + description: Time when this record was created + example: "2022-03-11T23:19:16.343Z" + updatedBy: + type: string + format: uuid + description: >- + The subject id of the current user if request was authenticated with + a Bearer token (ex. JWT), or a 'nil' uuid if request was + authenticated via Basic auth + default: null + example: 72cf13d7-aef7-4ad5-8ddd-0ff23eefb442 + updatedAt: + type: string + format: date-time + description: Time when this record was last updated + example: "2022-03-11T23:19:16.343Z" + DB-User: + title: DB User + type: object + allOf: + - type: object + properties: + userId: + type: string + description: Subject id of the user (Usually the sub claim in a JWT) + example: 5dad1ec9-d3c0-4b0f-8ead-cb4d9fa98987 + identityId: + type: string + description: >- + Identity id of the user (Usually the sub claim in a JWT but can + be redefined to a different claim with the `KC_IDENTITYKEY` env + variable) + example: 5dad1ec9d3c04b0f8eadcb4d9fa98987 + idp: + type: string + description: Identity provider used by the OIDC server + example: idir + email: + type: string + description: Registered email for this user + example: jsmith@gov.bc.ca + username: + type: string + description: Username of this user + example: jsmith + firstName: + type: string + description: First name of this user + example: Jane + lastName: + type: string + description: Last name of this user + example: Smith + active: + type: boolean + description: Determines whether this user is considered active + default: true + example: true + - $ref: "#/components/schemas/DB-TimestampUserData" + DB-Version: + title: DB Version + type: object + allOf: + - type: object + required: + - id + - s3VersionId + - objectId + - mimeType + - deleteMarker + properties: + id: + type: string + description: The primary identifier for this version + format: uuid + example: ac246e31-c807-496c-bc93-cd8bc2f1b2b4 + s3VersionId: + type: string + description: a version identifier created in S3 + example: "1647462569641" + objectId: + type: string + description: The primary identifier for the parent object + format: uuid + example: ac246e31-c807-496c-bc93-cd8bc2f1b2b4 + mimeType: + type: string + description: The MIME Type of the version + default: application/octet-stream + example: text/plain + deleteMarker: + type: boolean + description: Whether or not this version is a S3 delete-marker + example: false + etag: + type: integer + description: >- + An entity tag (ETag) is an opaque identifier assigned by a web + server to a specific version of a resource found at a URL + example: '"9d1aaa54b84e1d6ccc6e0477c5717fe3"' + - $ref: "#/components/schemas/DB-TimestampUserData" + DownloadMode: + title: Download Mode + type: string + description: Download mode behavior overrides + enum: + - proxy + - url + example: proxy + PermCode: + title: Permission Code + type: string + description: Permission code/type + enum: + - CREATE + - READ + - UPDATE + - DELETE + - MANAGE + example: UPDATE + Request-CreateBucket: + title: Request Create Bucket + type: object + required: + - accessKeyId + - bucket + - bucketName + - endpoint + - secretAccessKey + allOf: + - $ref: "#/components/schemas/Request-UpdateBucket" + - type: object + properties: + key: + description: >- + The prefix given to objects in the bucket. Defaults to "/" if + undefined. + type: string + default: / + example: coms/env + Request-UpdateBucket: + title: Request Update Bucket + type: object + properties: + accessKeyId: + type: string + description: >- + S3 account Access Key Id, similar to a username. This value will be + redacted from the API response. + example: REDACTED + active: + type: boolean + description: Whether this bucket is considered active in COMS + default: true + example: true + bucket: + type: string + description: The ID of a bucket + example: ezakyp + bucketName: + type: string + description: A human-readable label or title + example: Geospatial Maps + endpoint: + type: string + description: A valid RFC 3986 formatted S3 Service URI endpoint + maxLength: 255 + example: https://my-objectstore.ca + region: + type: string + description: >- + The geographical zone defined by the S3 service where your bucket is + located + example: ca-central-1 + secretAccessKey: + type: string + description: >- + S3 account Secret Access Key, similar to a password. This value will + be redacted from the API response. + example: REDACTED + Request-PermissionTuple: + title: Response Permission Tuple + type: object + required: + - userId + - permCode + properties: + permCode: + $ref: "#/components/schemas/PermCode" + userId: + type: string + description: The primary identifier for a user + format: uuid + example: ac246e31-c807-496c-bc93-cd8bc2f1b2b4 + Response-BadRequest: + title: Response Bad Request + type: object + allOf: + - $ref: "#/components/schemas/Response-Problem" + - type: object + properties: + status: + example: 400 + title: + example: Bad Request + type: + example: https://developer.mozilla.org/en-US/docs/Web/HTTP/Status/400 + Response-BucketPerms: + title: Response Buckets with bucket-level permissions + type: object + properties: + bucketId: + type: string + format: uuid + description: >- + The unique identifier for the bucket. When `objectPerms=false` only + buckets with bucket-level permissions are returned. + example: ac246e31-c807-496c-bc93-cd8bc2f1b2b4 + permissions: + type: array + items: + $ref: "#/components/schemas/DB-BucketPermission" + Response-BucketPermsWithObject: + title: Response Buckets containing objects with object-level permissions + description: >- + When `objectPerms=true` response will include buckets containing objects + with matching permissions. + type: object + properties: + bucketId: + type: string + format: uuid + description: >- + The unique identifier for the bucket. When `objectPerms=true` + response will include buckets containing objects with matching + permissions. + example: bf246e31-c807-496c-bc93-cd8bc2f1897f + permissions: + type: array + items: + type: object + description: Array will be empty. + example: [] + Response-Conflict: + title: Response Conflict + type: object + allOf: + - $ref: "#/components/schemas/Response-Problem" + - type: object + properties: + status: + example: 409 + title: + example: Conflict + type: + example: https://developer.mozilla.org/en-US/docs/Web/HTTP/Status/409 + Response-ContentTooLarge: + title: Response Content Too Large + type: object + allOf: + - $ref: "#/components/schemas/Response-Problem" + - type: object + properties: + status: + example: 413 + title: + example: Content Too Large + type: + example: https://developer.mozilla.org/en-US/docs/Web/HTTP/Status/413 + Response-Error: + title: Response Error + type: object + allOf: + - $ref: "#/components/schemas/Response-Problem" + - type: object + properties: + status: + example: 500 + title: + example: Internal Server Error + type: + example: https://developer.mozilla.org/en-US/docs/Web/HTTP/Status/500 + Response-Forbidden: + title: Response Forbidden + type: object + allOf: + - $ref: "#/components/schemas/Response-Problem" + - type: object + properties: + type: + example: https://developer.mozilla.org/en-US/docs/Web/HTTP/Status/403 + title: + example: Forbidden + status: + example: 403 + detail: + example: User lacks permission to complete this action + Response-LengthRequired: + title: Length Required + type: object + allOf: + - $ref: "#/components/schemas/Response-Problem" + - type: object + properties: + status: + example: 411 + title: + example: Length Required + type: + example: https://developer.mozilla.org/en-US/docs/Web/HTTP/Status/411 + Response-NotFound: + title: Response Not Found + type: object + allOf: + - $ref: "#/components/schemas/Response-Problem" + - type: object + properties: + status: + example: 404 + title: + example: Not Found + type: + example: https://developer.mozilla.org/en-US/docs/Web/HTTP/Status/404 + Response-NotImplemented: + title: Response Not Implemented + type: object + allOf: + - $ref: "#/components/schemas/Response-Problem" + - type: object + properties: + status: + example: 501 + title: + example: This action is not supported in the current authentication mode + type: + example: https://developer.mozilla.org/en-US/docs/Web/HTTP/Status/501 + Response-ObjectDeleted: + title: Response Object Deleted + type: object + properties: + $metadata: + $ref: "#/components/schemas/S3-Metadata" + DeleteMarker: + type: boolean + example: true + s3VersionId: + type: string + description: A version identifier created in S3 + example: "1647462569641" + Response-ObjectDetails: + title: Response Object Details + type: object + allOf: + - $ref: "#/components/schemas/DB-Object" + - type: object + required: + - length + - mimeType + - Key + - Location + properties: + length: + type: integer + description: Size of the file body in bytes + maximum: 5497558138880 + example: 529 + mimeType: + type: string + description: The object MIME Type + default: application/octet-stream + example: text/plain + versionId: + type: string + description: The primary identifier for this version in the COMS database + format: uuid + example: ac246e31-c807-496c-bc93-cd8bc2f1b2b4 + - $ref: "#/components/schemas/DB-Metadata" + - $ref: "#/components/schemas/DB-Tags" + - $ref: "#/components/schemas/S3-Object" + Response-ObjectPerms: + title: Response Objects with object-level permissions + type: object + properties: + objectId: + type: string + format: uuid + description: >- + The unique identifier for the object. When `bucketPerms=false` only + objects with matching object-level permissions are returned. + example: ac246e31-c807-496c-bc93-cd8bc2f1b2b4 + permissions: + type: array + items: + $ref: "#/components/schemas/DB-ObjectPermission" + Response-ObjectPermsWithBucket: + title: Response Objects in buckets with bucket-level permissions + description: >- + When `objectPerms=true` response will include objects in buckets with + matching permissions. + type: object + properties: + objectId: + type: string + format: uuid + description: >- + The unique identifier for the object. When `bucketPerms=true` + response will include objects in buckets with matching permissions. + example: bf246e31-c807-496c-bc93-cd8bc2f1897hf + permissions: + type: array + items: + type: object + description: Array will be empty. + example: [] + Response-PresignedURL: + title: Response Presigned URL + type: string + description: A Presigned S3 URL + example: >- + https://your.objectstore.com/yourbucket/coms/env/00000000-0000-0000-0000-000000000000?X-Amz-Algorithm=AWS4-HMAC-SHA256&X-Amz-Content-Sha256=UNSIGNED-PAYLOAD&X-Amz-Credential=credential%2F20220411%2Fus-east-1%2Fs3%2Faws4_request&X-Amz-Date=20220411T204528Z&X-Amz-Expires=300&X-Amz-Signature=SIGNATURE&X-Amz-SignedHeaders=host&x-id=GetObject + Response-Problem: + title: Response Problem + type: object + required: + - type + - title + - status + properties: + type: + type: string + description: What type of problem, link to explanation of problem + title: + type: string + description: Title of problem, generally the HTTP Status Code description + status: + type: string + description: The HTTP Status code + detail: + type: string + description: >- + A short, human-readable explanation specific to this occurrence of + the problem + instance: + type: string + description: >- + A URI reference that identifies the specific occurrence of the + problem + Response-Unauthorized: + title: Response Unauthorized + type: object + allOf: + - $ref: "#/components/schemas/Response-Problem" + - type: object + properties: + type: + example: https://developer.mozilla.org/en-US/docs/Web/HTTP/Status/401 + title: + example: Unauthorized + status: + example: 401 + detail: + example: Invalid authorization credentials + Response-UnsupportedMediaType: + title: Unsupported Media Type + type: object + allOf: + - $ref: "#/components/schemas/Response-Problem" + - type: object + properties: + status: + example: 415 + title: + example: Unsupported Media Type + type: + example: https://developer.mozilla.org/en-US/docs/Web/HTTP/Status/415 + Response-ValidationError: + title: Response Validation Error + type: object + allOf: + - $ref: "#/components/schemas/Response-Problem" + - type: object + required: + - errors + properties: + errors: + type: array + items: + type: object + required: + - message + properties: + value: + type: object + description: Contents of the field that was in error. + example: {} + message: + type: string + description: The error message for the field. + example: Invalid value `encoding`. + status: + example: 422 + title: + example: Unprocessable Entity + type: + example: https://developer.mozilla.org/en-US/docs/Web/HTTP/Status/422 + Response-VersionDeleted: + title: Response Version Deleted + type: object + properties: + $metadata: + $ref: "#/components/schemas/S3-Metadata" + s3VersionId: + type: string + description: a version identifier created in S3 + example: "1647462569641" + S3-Metadata: + title: S3 Metadata + type: object + required: + - httpStatusCode + - extendedRequestId + properties: + httpStatusCode: + type: integer + description: S3 Endpoint internal HTTP code + example: 200 + extendedRequestId: + type: string + description: S3 Endpoint unique internal request id + example: a0ec1be0a99a08aee6026ba9756da40327f2d3ce844499b7ee752082f3fb22e3 + attempts: + type: integer + description: Number of attempts performed by S3 + example: 1 + totalRetryDelay: + type: integer + description: S3 delay for retries + example: 0 + S3-Object: + title: S3 Object + type: object + required: + - $metadata + - ETag + properties: + $metadata: + $ref: "#/components/schemas/S3-Metadata" + ETag: + type: string + description: S3 Identifier for a specific version of this object + example: '"9d1aaa54b84e1d6ccc6e0477c5717fe3"' + Bucket: + type: string + description: S3 Identifier for the bucket + example: ezakyp + Key: + type: string + description: Key of the object + example: coms/env/00000000-0000-0000-0000-000000000000 + Location: + type: string + description: URL to the resource just uploaded + example: >- + https://your.objectstore.com/yourbucket/coms/env/00000000-0000-0000-0000-000000000000 + ServerSideEncryption: + type: string + description: >- + If the object is stored using server-side encryption either with an + AWS KMS key or an Amazon S3-managed encryption key, this field + specifies the server-side encryption algorithm used when storing + this object in Amazon S3 (for example, AES256, aws:kms). + example: AES256 + s3VersionId: + type: string + description: A version identifier created in S3 + example: "1647462569641" + S3-Version: + title: S3 Version + type: object + required: + - ETag + - Size + - StorageClass + - s3VersionId + - IsLatest + - LastModified + properties: + ETag: + type: string + description: S3 Identifier for a specific version of this object + example: 9d1aaa54b84e1d6ccc6e0477c5717fe3 + Size: + type: integer + description: Size of the object in bytes + example: 529 + StorageClass: + description: >- + S3 storage classes based on the data access, resiliency, and cost + requirements of your workloads + example: STANDARD + key: + description: Key of the object + s3VersionId: + type: string + description: A version identifier created in S3 + example: "1647462569641" + IsLatest: + description: >- + Specifies whether the object is (true) or is not (false) the latest + version of an object. + LastModified: + type: string + format: date-time + example: "2022-03-11T23:19:16.343Z" + Owner: + $ref: "#/components/schemas/S3-VersionOwner" + S3-VersionList: + title: S3 Version List + type: object + required: + - $metadata + - IsTruncated + - MaxKeys + - Name + - Prefix + - Versions + properties: + $metadata: + $ref: "#/components/schemas/S3-Metadata" + IsTruncated: + type: boolean + description: Whether or not this object listing is complete + example: false + KeyMarker: + type: string + description: Marks the last key returned in a truncated response + example: "" + MaxKeys: + type: integer + format: int32 + description: >- + Sets the maximum number of keys returned in the response. By default + the action returns up to 1,000 key names. The response might contain + fewer keys but will never contain more. + example: 1000 + Name: + type: string + description: Name of the S3 bucket + Prefix: + type: string + description: Keys that begin with the indicated prefix + example: coms/env/ac246e31-c807-496c-bc93-cd8bc2f1b2b4 + VersionIdMarker: + type: string + description: Marks the last version of the key returned in a truncated response + example: "" + Versions: + type: array + items: + $ref: "#/components/schemas/S3-Version" + S3-VersionOwner: + title: S3 Version Owner + type: object + required: + - DisplayName + - ID + properties: + DisplayName: + type: string + description: The bucket accessKeyId/owner + example: abcdef + ID: + type: string + description: The bucket accessKeyId/owner + example: abcdef + S3VersionId: + title: S3 Version ID + type: string + description: >- + A version identifier created in S3. This parameter is incompatible with + `versionId` + maximum: 1024 + example: "1647462569641" + VersionId: + title: Version ID + type: string + description: >- + A version identifier created by COMS. This parameter is incompatible + with `s3VersionId` + format: uuid + example: c05c7650-5f48-4e51-bf17-762e8fc121a1 + responses: + AddedQueueLength: + description: Returns the number of objects that have been added to the queue + content: + text/plain: + schema: + type: integer + example: 5 + description: Number of objects added to the queue + Accepted: + description: Accepted + BadRequest: + description: Bad Request (Request is missing content or malformed) + content: + application/json: + schema: + $ref: "#/components/schemas/Response-BadRequest" + Conflict: + description: Conflict (Request conflicts with server state) + content: + application/json: + schema: + $ref: "#/components/schemas/Response-Conflict" + ContentTooLarge: + description: Content Too Large + content: + application/json: + schema: + $ref: "#/components/schemas/Response-ContentTooLarge" + Error: + description: Internal Server Error + content: + application/json: + schema: + $ref: "#/components/schemas/Response-Error" + Found: + description: Resource requested has been temporarily moved + Forbidden: + description: Forbidden + content: + application/json: + schema: + $ref: "#/components/schemas/Response-Forbidden" + LengthRequired: + description: Length Required + content: + application/json: + schema: + $ref: "#/components/schemas/Response-LengthRequired" + NoContent: + description: No Content + NotFound: + description: Not Found + content: + application/json: + schema: + $ref: "#/components/schemas/Response-NotFound" + NotImplemented: + description: >- + The server does not support the functionality required to fulfill the + request. + content: + application/json: + schema: + $ref: "#/components/schemas/Response-NotImplemented" + NotModified: + description: Not Modified + S3Found: + description: Returns a temporary pre-signed S3 object URL location header + headers: + Location: + $ref: "#/components/headers/Location" + Unauthorized: + description: Unauthorized + content: + application/json: + schema: + $ref: "#/components/schemas/Response-Unauthorized" + UnprocessableEntity: + description: Unprocessable Content (Generally validation error(s)) + content: + application/json: + schema: + $ref: "#/components/schemas/Response-ValidationError" + UnsupportedMediaType: + description: Unsupported Media Type + content: + application/json: + schema: + $ref: "#/components/schemas/Response-UnsupportedMediaType" + securitySchemes: + BasicAuth: + type: http + description: Basic auth via environment defined username and password + scheme: basic + BearerAuth: + type: http + description: Bearer token auth using an OIDC issued JWT token + scheme: bearer + bearerFormat: JWT + OpenID: + type: openIdConnect + description: OpenID Connect endpoint for acquiring JWT tokens + openIdConnectUrl: >- + https://logonproxy.gov.bc.ca/auth/realms/your-realm-name/.well-known/openid-configuration diff --git a/comsapi/app/src/middleware/authentication.js b/comsapi/app/src/middleware/authentication.js new file mode 100644 index 00000000..59777860 --- /dev/null +++ b/comsapi/app/src/middleware/authentication.js @@ -0,0 +1,114 @@ +const Problem = require('api-problem'); +const config = require('config'); +const basicAuth = require('express-basic-auth'); +const jwt = require('jsonwebtoken'); + +const { AuthType } = require('../components/constants'); +const { userService } = require('../services'); +const log = require('../components/log')(module.filename); + +/** + * Basic Auth configuration object + * @see {@link https://github.com/LionC/express-basic-auth} + */ +const _basicAuthConfig = { + // Must be a synchronous function + authorizer: (username, password) => { + const userMatch = basicAuth.safeCompare(username, config.get('basicAuth.username')); + const pwMatch = basicAuth.safeCompare(password, config.get('basicAuth.password')); + return userMatch & pwMatch; + }, + unauthorizedResponse: () => { + return new Problem(401, { detail: 'Invalid authorization credentials' }); + } +}; + +/** + * An express middleware function that checks basic authentication validity + * @see {@link https://github.com/LionC/express-basic-auth} + */ +const _checkBasicAuth = basicAuth(_basicAuthConfig); + +/** + * @function _spkiWrapper + * Wraps an SPKI key with PEM header and footer + * @param {string} spki The PEM-encoded Simple public-key infrastructure string + * @returns {string} The PEM-encoded SPKI with PEM header and footer + */ +const _spkiWrapper = (spki) => `-----BEGIN PUBLIC KEY-----\n${spki}\n-----END PUBLIC KEY-----`; + +/** + * @function currentUser + * Injects a currentUser object to the request if there exists valid authentication artifacts. + * Subsequent logic should check `req.currentUser.authType` for authentication method if needed. + * @param {object} req Express request object + * @param {object} res Express response object + * @param {function} next The next callback function + * @returns {function} Express middleware function + * @throws The error encountered upon failure + */ +const currentUser = async (req, res, next) => { + const authorization = req.get('Authorization'); + const currentUser = { + authType: AuthType.NONE + }; + + if (authorization) { + log.info('step - 0 '); + // Basic Authorization + if (config.has('basicAuth.enabled') && authorization.toLowerCase().startsWith('basic ')) { + log.info('step - 1 '); + currentUser.authType = AuthType.BASIC; + } + + // OIDC JWT Authorization + else if (config.has('keycloak.enabled') && authorization.toLowerCase().startsWith('bearer ')) { + log.info('step - 2 '); + currentUser.authType = AuthType.BEARER; + + try { + const bearerToken = authorization.substring(7); + let isValid = false; + + if (config.has('keycloak.publicKey')) { + log.info('step - 3 '); + const publicKey = config.get('keycloak.publicKey'); + const pemKey = publicKey.startsWith('-----BEGIN') + ? publicKey + : _spkiWrapper(publicKey); + isValid = jwt.verify(bearerToken, pemKey, { + issuer: `${config.get('keycloak.serverUrl')}/realms/${config.get('keycloak.realm')}` + }); + log.info('step - 4 ', isValid); + } else { + throw new Error('OIDC environment variable KC_PUBLICKEY or keycloak.publicKey must be defined'); + } + + if (isValid) { + log.info('step - 5 '); + currentUser.tokenPayload = typeof isValid === 'object' ? isValid : jwt.decode(bearerToken); + await userService.login(currentUser.tokenPayload); + log.info('step - 6 '); + } else { + throw new Error('Invalid authorization token'); + } + } catch (err) { + log.info('step - 7 ',err); + return next(new Problem(403, { detail: err.message, instance: req.originalUrl })); + } + } + } + + // Inject currentUser data into request + req.currentUser = Object.freeze(currentUser); + + // Continue middleware stack based on detected AuthType + if (currentUser.authType === AuthType.BASIC) { + _checkBasicAuth(req, res, next); + } + else next(); +}; + +module.exports = { + _basicAuthConfig, _checkBasicAuth, currentUser, _spkiWrapper +}; diff --git a/comsapi/app/src/middleware/authorization.js b/comsapi/app/src/middleware/authorization.js new file mode 100644 index 00000000..9f8aa7d7 --- /dev/null +++ b/comsapi/app/src/middleware/authorization.js @@ -0,0 +1,148 @@ +const Problem = require('api-problem'); + +const log = require('../components/log')(module.filename); +const { AuthMode, AuthType, Permissions } = require('../components/constants'); +const { getAppAuthMode, getCurrentIdentity } = require('../components/utils'); +const { NIL: SYSTEM_USER } = require('uuid'); +const { bucketPermissionService, objectService, objectPermissionService, userService } = require('../services'); + +/** + * @function _checkPermission + * Checks if the current user is authorized to perform the operation + * @param {object} req.currentObject Express request object currentObject + * @param {object} req.currentUser Express request object currentUser + * @param {object} req.params Express request object params + * @param {string} permission The permission to check against + * @returns {boolean} True if permission exists; false otherwise + */ +const _checkPermission = async ({ currentObject, currentUser, params }, permission) => { + const authType = currentUser ? currentUser.authType : undefined; + let result = false; + + // Guard against unauthorized access for all other cases + const userId = await userService.getCurrentUserId(getCurrentIdentity(currentUser, SYSTEM_USER)); + + if (authType === AuthType.BEARER && userId) { + const permissions = []; + const searchParams = { permCode: permission, userId: userId }; + + if (params.objectId) { + permissions.push(...await objectPermissionService.searchPermissions({ + objId: params.objectId, ...searchParams + })); + } + if (params.bucketId || currentObject.bucketId) { + permissions.push(...await bucketPermissionService.searchPermissions({ + bucketId: params.bucketId || currentObject.bucketId, ...searchParams + })); + } + + // Check if user has the required permission in their permission set + result = permissions.some(p => p.permCode === permission); + } + + log.debug('Missing user identification', { function: '_checkPermission' }); + return result; +}; + +/** + * @function checkAppMode + * Rejects the request if the incoming authentication mode does not match the application mode + * @param {object} req Express request object + * @param {object} _res Express response object + * @param {function} next The next callback function + * @returns {function} Express middleware function + * @throws The error encountered upon failure + */ +const checkAppMode = (req, _res, next) => { + const authMode = getAppAuthMode(); + const authType = req.currentUser ? req.currentUser.authType : undefined; + + try { + if (authMode === AuthMode.BASICAUTH && authType === AuthType.BEARER) { + throw new Error('Basic auth mode does not support Bearer type auth'); + } else if (authMode === AuthMode.OIDCAUTH && authType === AuthType.BASIC) { + throw new Error('Oidc auth mode does not support Basic type auth'); + } + } catch (err) { + log.verbose(err.message, { function: 'checkAppMode', authMode: authMode, authType: authType }); + throw new Problem(501, { + detail: 'Current application mode does not support incoming authentication type', + instance: req.originalUrl, + authMode: authMode, + authType: authType + }); + } + + next(); +}; + +/** + * @function currentObject + * Injects a currentObject object to the request if there is an applicable object record + * @param {object} req Express request object + * @param {object} _res Express response object + * @param {function} next The next callback function + * @returns {function} Express middleware function + */ +const currentObject = async (req, _res, next) => { + try { + if (req.params.objectId) { + req.currentObject = Object.freeze({ + ...await objectService.read(req.params.objectId) + }); + } + } catch (err) { + log.warn(err.message, { function: 'currentObject' }); + } + + next(); +}; + +/** + * @function hasPermission + * function checks: + * - request is allowed in current auth mode + * - request contains an objectId or bucketId param + * - request contains currentObject property if objectId param passed + * - if objectId param is for a public object and request requires READ permission + * - if passed permission exists for current user on object or bucket (see: _checkPermission) + * @param {string} permission a permission code (eg: READ) + * @returns {function} Express middleware function + * @throws The error encountered upon failure + */ +const hasPermission = (permission) => { + return async (req, _res, next) => { + const authMode = getAppAuthMode(); + const authType = req.currentUser ? req.currentUser.authType : undefined; + + const canBasicMode = (mode) => [AuthMode.BASICAUTH, AuthMode.FULLAUTH].includes(mode); + const canOidcMode = (mode) => [AuthMode.OIDCAUTH, AuthMode.FULLAUTH].includes(mode); + + try { + if (!canOidcMode(authMode)) { + log.debug('Current application mode does not enforce permission checks', { function: 'hasPermission' }); + } else if (!req.params.objectId && !req.params.bucketId) { + throw new Error('Missing request parameter(s)'); + } else if (req.params.objectId && !req.currentObject) { + // Force 403 on unauthorized or not found; do not allow 404 id brute force discovery + throw new Error('Missing object record'); + } else if (authType === AuthType.BASIC && canBasicMode(authMode)) { + log.debug('Basic authTypes are always permitted', { function: 'hasPermission' }); + } else if (req.params.objectId && req.currentObject.public && permission === Permissions.READ) { + log.debug('Read requests on public objects are always permitted', { function: 'hasPermission' }); + } else if (!await _checkPermission(req, permission)) { + throw new Error(`User lacks required permission ${permission}`); + } + } catch (err) { + log.verbose(err.message, { function: 'hasPermission' }); + return next(new Problem(403, { detail: 'User lacks permission to complete this action', instance: req.originalUrl })); + } + + next(); + }; +}; + +module.exports = { + _checkPermission, checkAppMode, currentObject, hasPermission +}; diff --git a/comsapi/app/src/middleware/featureToggle.js b/comsapi/app/src/middleware/featureToggle.js new file mode 100644 index 00000000..09520a7d --- /dev/null +++ b/comsapi/app/src/middleware/featureToggle.js @@ -0,0 +1,63 @@ +const Problem = require('api-problem'); + +const { AuthMode, AuthType } = require('../components/constants'); +const { getAppAuthMode } = require('../components/utils'); + +/** + * @function requireBasicAuth + * Only allows basic authentication requests if application is in the appropriate mode + * @param {object} req Express request object + * @param {object} _res Express response object + * @param {function} next The next callback function + * @returns {function} Express middleware function + * @throws The error encountered upon failure + */ +const requireBasicAuth = (req, _res, next) => { + const authMode = getAppAuthMode(); + const authType = req.currentUser ? req.currentUser.authType : undefined; + + const canBasicMode = (mode) => [AuthMode.BASICAUTH, AuthMode.FULLAUTH].includes(mode); + + if (authMode === AuthMode.OIDCAUTH) { + throw new Problem(501, { + detail: 'This action is not supported in the current authentication mode', + instance: req.originalUrl + }); + } + + if (canBasicMode(authMode) && authType !== AuthType.BASIC) { + throw new Problem(403, { + detail: 'User lacks permission to complete this action', + instance: req.originalUrl + }); + } + + next(); +}; + +/** + * @function requireSomeAuth + * Rejects the request if there is no authorization in the appropriate mode + * @param {object} req Express request object + * @param {object} _res Express response object + * @param {function} next The next callback function + * @returns {function} Express middleware function + * @throws The error encountered upon failure + */ +const requireSomeAuth = (req, _res, next) => { + const authMode = getAppAuthMode(); + const authType = req.currentUser ? req.currentUser.authType : undefined; + + if (authMode !== AuthMode.NOAUTH && (!authType || authType === AuthType.NONE)) { + throw new Problem(403, { + detail: 'User lacks permission to complete this action', + instance: req.originalUrl + }); + } + + next(); +}; + +module.exports = { + requireBasicAuth, requireSomeAuth +}; diff --git a/comsapi/app/src/middleware/upload.js b/comsapi/app/src/middleware/upload.js new file mode 100644 index 00000000..2fed8787 --- /dev/null +++ b/comsapi/app/src/middleware/upload.js @@ -0,0 +1,69 @@ +const Problem = require('api-problem'); +const contentDisposition = require('content-disposition'); + +/** + * @function currentUpload + * Injects a currentUpload object to the request based on incoming headers + * @param {boolean} [strict=false] Short circuit returns response if misformatted + * @param {object} _res Express response object + * @param {function} next The next callback function + * @returns {function} Express middleware function + * @throws The error encountered upon failure + */ +const currentUpload = (strict = false) => { + return (req, _res, next) => { + // Check Content-Length Header + const contentLength = parseInt(req.get('Content-Length')); + // TODO: Figure out what's killing and returning a 400 in response stack + if (!contentLength) throw new Problem(411, { + detail: 'Content-Length must be greater than 0', + instance: req.originalUrl + }); + + // Check Content-Disposition Header + let filename; + const disposition = req.get('Content-Disposition'); + if (disposition) { + try { + const { type, parameters } = contentDisposition.parse(disposition); + if (strict && !type || type !== 'attachment') throw new Error('Disposition type is not \'attachment\''); + if (strict && !parameters?.filename) throw new Error('Disposition missing \'filename\' parameter'); + filename = parameters?.filename; + } catch (e) { + // Ignore improperly formatted Content-Disposition when not in strict mode + if (strict) throw new Problem(400, { + detail: `Content-Disposition header error: ${e.message}`, + instance: req.originalUrl + }); + } + } else { + if (strict) throw new Problem(415, { + detail: 'Content-Disposition header missing', + instance: req.originalUrl + }); + } + + // Check Content-Type Header + const mimeType = req.get('Content-Type') ?? 'application/octet-stream'; + + req.currentUpload = Object.freeze({ + contentLength: contentLength, + filename: filename, + mimeType: mimeType + }); + + /** + * Removes the default 5 minute request timeout added in Node v18 + * This change reverts the behavior to be similar to Node v16 and earlier + * This value should not be 0x7FFFFFFF as behavior becomes unpredictable + * @see {@link https://nodejs.org/en/blog/release/v18.0.0#http-timeouts} + */ + req.socket.server.requestTimeout = 0; + + next(); + }; +}; + +module.exports = { + currentUpload +}; diff --git a/comsapi/app/src/middleware/validation.js b/comsapi/app/src/middleware/validation.js new file mode 100644 index 00000000..df8f8449 --- /dev/null +++ b/comsapi/app/src/middleware/validation.js @@ -0,0 +1,34 @@ +const Problem = require('api-problem'); + +/** + * @function validator + * Performs express request validation against a specified `schema` + * @param {object} schema An object containing Joi validation schema definitions + * @returns {function} Express middleware function + * @throws The error encountered upon failure + */ +const validate = (schema) => { + return (req, _res, next) => { + const validationErrors = Object.entries(schema) + .map(([prop, def]) => { + const result = def.validate(req[prop], { abortEarly: false })?.error; + return result ? [prop, result?.details] : undefined; + }) + .filter(error => !!error); + + if (Object.keys(validationErrors).length) { + throw new Problem(422, { + detail: validationErrors + .flatMap(groups => groups[1]?.map(error => error?.message)) + .join('; '), + instance: req.originalUrl, + errors: Object.fromEntries(validationErrors) + }); + } + else next(); + }; +}; + +module.exports = { + validate +}; diff --git a/comsapi/app/src/routes/v1/bucket.js b/comsapi/app/src/routes/v1/bucket.js new file mode 100644 index 00000000..4b85cc67 --- /dev/null +++ b/comsapi/app/src/routes/v1/bucket.js @@ -0,0 +1,53 @@ +const express = require('express'); +const router = express.Router(); + +const { Permissions } = require('../../components/constants'); +const { bucketController, syncController } = require('../../controllers'); +const { bucketValidator } = require('../../validators'); +const { requireSomeAuth } = require('../../middleware/featureToggle'); +const { checkAppMode, hasPermission } = require('../../middleware/authorization'); + +router.use(checkAppMode); +router.use(requireSomeAuth); + +/** Creates a bucket */ +router.put('/', express.json(), bucketValidator.createBucket, (req, res, next) => { + bucketController.createBucket(req, res, next); +}); + +/** + * Returns bucket headers + * Notes: + * - router.head() should appear before router.get() method using same path, otherwise router.get() will be called instead. + * - if bucketId path param is not given, router.get('/') (the bucket search endpoint) is called instead. + */ +router.head('/:bucketId', bucketValidator.headBucket, hasPermission(Permissions.READ), (req, res, next) => { + bucketController.headBucket(req, res, next); +}); + +/** Returns a bucket */ +router.get('/:bucketId', bucketValidator.readBucket, hasPermission(Permissions.READ), (req, res, next) => { + bucketController.readBucket(req, res, next); +}); + +/** Search for buckets */ +router.get('/', bucketValidator.searchBuckets, (req, res, next) => { + bucketController.searchBuckets(req, res, next); +}); + +/** Updates a bucket */ +router.patch('/:bucketId', express.json(), bucketValidator.updateBucket, hasPermission(Permissions.UPDATE), (req, res, next) => { + bucketController.updateBucket(req, res, next); +}); + +/** Deletes the bucket */ +router.delete('/:bucketId', bucketValidator.deleteBucket, hasPermission(Permissions.DELETE), (req, res, next) => { + bucketController.deleteBucket(req, res, next); +}); + +/** Synchronizes a bucket */ +router.get('/:bucketId/sync', bucketValidator.syncBucket, hasPermission(Permissions.READ), (req, res, next) => { + syncController.syncBucket(req, res, next); +}); + +module.exports = router; diff --git a/comsapi/app/src/routes/v1/docs.js b/comsapi/app/src/routes/v1/docs.js new file mode 100644 index 00000000..3fe041ad --- /dev/null +++ b/comsapi/app/src/routes/v1/docs.js @@ -0,0 +1,34 @@ +const config = require('config'); +const router = require('express').Router(); +const { readFileSync } = require('fs'); +const yaml = require('js-yaml'); +const { join } = require('path'); + +/** Gets the OpenAPI specification */ +function getSpec() { + const rawSpec = readFileSync(join(__dirname, '../../docs/v1.api-spec.yaml'), 'utf8'); + const spec = yaml.load(rawSpec); + spec.servers[0].url = '/api/v1'; + if (config.has('keycloak.enabled')) { + spec.components.securitySchemes.OpenID.openIdConnectUrl = `${config.get('keycloak.serverUrl')}/realms/${config.get('keycloak.realm')}/.well-known/openid-configuration`; + } + return spec; +} + +/** OpenAPI Docs */ +router.get('/', (_req, res) => { + const docs = require('../../docs/docs'); + res.send(docs.getDocHTML('v1')); +}); + +/** OpenAPI YAML Spec */ +router.get('/api-spec.yaml', (_req, res) => { + res.status(200).type('application/yaml').send(yaml.dump(getSpec())); +}); + +/** OpenAPI JSON Spec */ +router.get('/api-spec.json', (_req, res) => { + res.status(200).json(getSpec()); +}); + +module.exports = router; diff --git a/comsapi/app/src/routes/v1/index.js b/comsapi/app/src/routes/v1/index.js new file mode 100644 index 00000000..98b3b3a9 --- /dev/null +++ b/comsapi/app/src/routes/v1/index.js @@ -0,0 +1,50 @@ +const router = require('express').Router(); +const { currentUser } = require('../../middleware/authentication'); + +router.use(currentUser); + +// Base v1 Responder +router.get('/', (_req, res) => { + res.status(200).json({ + endpoints: [ + '/bucket', + '/docs', + '/metadata', + '/object', + '/permission', + '/sync', + '/tagging', + '/user', + '/version' + ] + }); +}); + +/** Bucket Router */ +router.use('/bucket', require('./bucket')); + +/** Documentation Router */ +router.use('/docs', require('./docs')); + +/** Metadata Router */ +router.use('/metadata', require('./metadata')); + +/** Object Router */ +router.use('/object', require('./object')); + +/** Permission Router */ +router.use('/permission', require('./permission')); + +/** Sync Router */ +router.use('/sync', require('./sync')); + +/** Tagging Router */ +router.use('/tagging', require('./tag')); + +/** User Router */ +router.use('/user', require('./user')); + +/** Version Router */ +router.use('/version', require('./version')); + +module.exports = router; diff --git a/comsapi/app/src/routes/v1/metadata.js b/comsapi/app/src/routes/v1/metadata.js new file mode 100644 index 00000000..4af66913 --- /dev/null +++ b/comsapi/app/src/routes/v1/metadata.js @@ -0,0 +1,16 @@ +const router = require('express').Router(); + +const { metadataController } = require('../../controllers'); +const { metadataValidator } = require('../../validators'); +const { requireSomeAuth } = require('../../middleware/featureToggle'); +const { checkAppMode } = require('../../middleware/authorization'); + +router.use(checkAppMode); +router.use(requireSomeAuth); + +/** Search for metadata */ +router.get('/', metadataValidator.searchMetadata, (req, res, next) => { + metadataController.searchMetadata(req, res, next); +}); + +module.exports = router; diff --git a/comsapi/app/src/routes/v1/object.js b/comsapi/app/src/routes/v1/object.js new file mode 100644 index 00000000..1145200d --- /dev/null +++ b/comsapi/app/src/routes/v1/object.js @@ -0,0 +1,98 @@ +const router = require('express').Router(); + +const { Permissions } = require('../../components/constants'); +const { objectController, syncController } = require('../../controllers'); +const { objectValidator } = require('../../validators'); +const { checkAppMode, currentObject, hasPermission } = require('../../middleware/authorization'); +const { requireSomeAuth } = require('../../middleware/featureToggle'); +const { currentUpload } = require('../../middleware/upload'); + +router.use(checkAppMode); + +/** Creates new objects */ +router.put('/', requireSomeAuth, objectValidator.createObject, currentUpload(true), (req, res, next) => { + objectController.createObject(req, res, next); +}); + +/** Search for objects */ +router.get('/', requireSomeAuth, objectValidator.searchObjects, (req, res, next) => { + objectController.searchObjects(req, res, next); +}); + +/** Fetch metadata for specific objects */ +router.get('/metadata', requireSomeAuth, objectValidator.fetchMetadata, (req, res, next) => { + objectController.fetchMetadata(req, res, next); +}); + +/** Fetch tags for specific objects */ +router.get('/tagging', requireSomeAuth, objectValidator.fetchTags, (req, res, next) => { + objectController.fetchTags(req, res, next); +}); + +/** Returns object headers */ +router.head('/:objectId', objectValidator.headObject, currentObject, hasPermission(Permissions.READ), (req, res, next) => { + objectController.headObject(req, res, next); +}); + +/** Returns the object */ +router.get('/:objectId', objectValidator.readObject, currentObject, hasPermission(Permissions.READ), (req, res, next) => { + // TODO: Add validation to reject unexpected query parameters + objectController.readObject(req, res, next); +}); + +/** Updates an object */ +router.put('/:objectId', requireSomeAuth, objectValidator.updateObject, currentUpload(), currentObject, hasPermission(Permissions.UPDATE), (req, res, next) => { + objectController.updateObject(req, res, next); +}); + +/** Deletes the object */ +router.delete('/:objectId', requireSomeAuth, objectValidator.deleteObject, currentObject, hasPermission(Permissions.DELETE), (req, res, next) => { + objectController.deleteObject(req, res, next); +}); + +/** Returns the object version history */ +router.get('/:objectId/version', requireSomeAuth, objectValidator.listObjectVersion, currentObject, hasPermission(Permissions.READ), (req, res, next) => { + objectController.listObjectVersion(req, res, next); +}); + +/** Sets the public flag of an object */ +router.patch('/:objectId/public', requireSomeAuth, objectValidator.togglePublic, currentObject, hasPermission(Permissions.MANAGE), (req, res, next) => { + objectController.togglePublic(req, res, next); +}); + +/** Add metadata to an object */ +router.patch('/:objectId/metadata', requireSomeAuth, objectValidator.addMetadata, currentObject, hasPermission(Permissions.UPDATE), (req, res, next) => { + objectController.addMetadata(req, res, next); +}); + +/** Replace metadata on an object */ +router.put('/:objectId/metadata', requireSomeAuth, objectValidator.replaceMetadata, currentObject, hasPermission(Permissions.UPDATE), (req, res, next) => { + objectController.replaceMetadata(req, res, next); +}); + +/** Deletes an objects metadata */ +router.delete('/:objectId/metadata', requireSomeAuth, objectValidator.deleteMetadata, currentObject, hasPermission(Permissions.UPDATE), (req, res, next) => { + objectController.deleteMetadata(req, res, next); +}); + +/** Synchronizes an object */ +router.get('/:objectId/sync', requireSomeAuth, objectValidator.syncObject, currentObject, hasPermission(Permissions.READ), (req, res, next) => { + syncController.syncObject(req, res, next); +}); + +/** Add tags to an object */ +router.patch('/:objectId/tagging', requireSomeAuth, objectValidator.addTags, currentObject, hasPermission(Permissions.UPDATE), (req, res, next) => { + objectController.addTags(req, res, next); +}); + +/** Add tags to an object */ +router.put('/:objectId/tagging', requireSomeAuth, objectValidator.replaceTags, currentObject, hasPermission(Permissions.UPDATE), (req, res, next) => { + objectController.replaceTags(req, res, next); +}); + +/** Add tags to an object */ +router.delete('/:objectId/tagging', requireSomeAuth, objectValidator.deleteTags, currentObject, hasPermission(Permissions.UPDATE), (req, res, next) => { + objectController.deleteTags(req, res, next); +}); + +module.exports = router; diff --git a/comsapi/app/src/routes/v1/permission/bucketPermission.js b/comsapi/app/src/routes/v1/permission/bucketPermission.js new file mode 100644 index 00000000..765d572f --- /dev/null +++ b/comsapi/app/src/routes/v1/permission/bucketPermission.js @@ -0,0 +1,33 @@ +const express = require('express'); +const router = express.Router(); + +const { Permissions } = require('../../../components/constants'); +const { bucketPermissionController } = require('../../../controllers'); +const { bucketPermissionValidator } = require('../../../validators'); +const { checkAppMode, currentObject, hasPermission } = require('../../../middleware/authorization'); +const { requireSomeAuth } = require('../../../middleware/featureToggle'); + +router.use(checkAppMode); +router.use(requireSomeAuth); + +/** Search for bucket permissions */ +router.get('/', bucketPermissionValidator.searchPermissions, (req, res, next) => { + bucketPermissionController.searchPermissions(req, res, next); +}); + +/** Returns the bucket permissions */ +router.get('/:bucketId', bucketPermissionValidator.listPermissions, currentObject, hasPermission(Permissions.READ), (req, res, next) => { + bucketPermissionController.listPermissions(req, res, next); +}); + +/** Grants bucket permissions to users */ +router.put('/:bucketId', express.json(), bucketPermissionValidator.addPermissions, currentObject, hasPermission(Permissions.MANAGE), (req, res, next) => { + bucketPermissionController.addPermissions(req, res, next); +}); + +/** Deletes bucket permissions for a user */ +router.delete('/:bucketId', bucketPermissionValidator.removePermissions, currentObject, hasPermission(Permissions.MANAGE), (req, res, next) => { + bucketPermissionController.removePermissions(req, res, next); +}); + +module.exports = router; diff --git a/comsapi/app/src/routes/v1/permission/index.js b/comsapi/app/src/routes/v1/permission/index.js new file mode 100644 index 00000000..39ed2c75 --- /dev/null +++ b/comsapi/app/src/routes/v1/permission/index.js @@ -0,0 +1,19 @@ +const router = require('express').Router(); + +// Base Responder +router.get('/', (_req, res) => { + res.status(200).json({ + endpoints: [ + '/bucket', + '/object' + ] + }); +}); + +/** Bucket Permission Router */ +router.use('/bucket', require('./bucketPermission')); + +/** Object Permission Router */ +router.use('/object', require('./objectPermission')); + +module.exports = router; diff --git a/comsapi/app/src/routes/v1/permission/objectPermission.js b/comsapi/app/src/routes/v1/permission/objectPermission.js new file mode 100644 index 00000000..431e5c05 --- /dev/null +++ b/comsapi/app/src/routes/v1/permission/objectPermission.js @@ -0,0 +1,33 @@ +const express = require('express'); +const router = express.Router(); + +const { Permissions } = require('../../../components/constants'); +const { objectPermissionController } = require('../../../controllers'); +const { objectPermissionValidator } = require('../../../validators'); +const { checkAppMode, currentObject, hasPermission } = require('../../../middleware/authorization'); +const { requireSomeAuth } = require('../../../middleware/featureToggle'); + +router.use(checkAppMode); +router.use(requireSomeAuth); + +/** Search for object permissions */ +router.get('/', objectPermissionValidator.searchPermissions, (req, res, next) => { + objectPermissionController.searchPermissions(req, res, next); +}); + +/** Returns the object permissions */ +router.get('/:objectId', objectPermissionValidator.listPermissions, currentObject, hasPermission(Permissions.MANAGE), (req, res, next) => { + objectPermissionController.listPermissions(req, res, next); +}); + +/** Grants object permissions to users */ +router.put('/:objectId', express.json(), objectPermissionValidator.addPermissions, currentObject, hasPermission(Permissions.MANAGE), (req, res, next) => { + objectPermissionController.addPermissions(req, res, next); +}); + +/** Deletes object permissions for a user */ +router.delete('/:objectId', objectPermissionValidator.removePermissions, currentObject, hasPermission(Permissions.MANAGE), (req, res, next) => { + objectPermissionController.removePermissions(req, res, next); +}); + +module.exports = router; diff --git a/comsapi/app/src/routes/v1/sync.js b/comsapi/app/src/routes/v1/sync.js new file mode 100644 index 00000000..14b5db08 --- /dev/null +++ b/comsapi/app/src/routes/v1/sync.js @@ -0,0 +1,21 @@ +const router = require('express').Router(); + +const { syncController } = require('../../controllers'); +const { checkAppMode } = require('../../middleware/authorization'); +const { requireBasicAuth, requireSomeAuth } = require('../../middleware/featureToggle'); + +router.use(checkAppMode); +router.use(requireSomeAuth); + +/** Synchronizes the default bucket */ +router.get('/', requireBasicAuth, (req, res, next) => { + req.params.bucketId = null; + syncController.syncBucket(req, res, next); +}); + +/** Check sync queue size */ +router.get('/status', (req, res, next) => { + syncController.syncStatus(req, res, next); +}); + +module.exports = router; diff --git a/comsapi/app/src/routes/v1/tag.js b/comsapi/app/src/routes/v1/tag.js new file mode 100644 index 00000000..f55beab1 --- /dev/null +++ b/comsapi/app/src/routes/v1/tag.js @@ -0,0 +1,17 @@ +const router = require('express').Router(); + +const { tagController } = require('../../controllers'); +const { tagValidator } = require('../../validators'); +const { requireSomeAuth } = require('../../middleware/featureToggle'); +const { checkAppMode } = require('../../middleware/authorization'); + +router.use(checkAppMode); +router.use(requireSomeAuth); + +/** Search for tags */ +router.get('/', tagValidator.searchTags, (req, res, next) => { + tagController.searchTags(req, res, next); +}); + + +module.exports = router; diff --git a/comsapi/app/src/routes/v1/user.js b/comsapi/app/src/routes/v1/user.js new file mode 100644 index 00000000..db6effde --- /dev/null +++ b/comsapi/app/src/routes/v1/user.js @@ -0,0 +1,21 @@ +const router = require('express').Router(); + +const { userValidator } = require('../../validators'); +const { userController } = require('../../controllers'); +const { checkAppMode } = require('../../middleware/authorization'); +const { requireSomeAuth } = require('../../middleware/featureToggle'); + +router.use(checkAppMode); +router.use(requireSomeAuth); + +/** Search for users */ +router.get('/', userValidator.searchUsers, (req, res, next) => { + userController.searchUsers(req, res, next); +}); + +/** List all identity providers */ +router.get('/idpList', userValidator.listIdps, (req, res, next) => { + userController.listIdps(req, res, next); +}); + +module.exports = router; diff --git a/comsapi/app/src/routes/v1/version.js b/comsapi/app/src/routes/v1/version.js new file mode 100644 index 00000000..8cad052b --- /dev/null +++ b/comsapi/app/src/routes/v1/version.js @@ -0,0 +1,21 @@ +const router = require('express').Router(); + +const { versionValidator } = require('../../validators'); +const { versionController } = require('../../controllers'); +const { checkAppMode } = require('../../middleware/authorization'); +const { requireSomeAuth } = require('../../middleware/featureToggle'); + +router.use(checkAppMode); +router.use(requireSomeAuth); + +/** Fetch metadata for specific version */ +router.get('/metadata', versionValidator.fetchMetadata, (req, res, next) => { + versionController.fetchMetadata(req, res, next); +}); + +/** Fetch tags for specific version */ +router.get('/tagging', versionValidator.fetchTags, (req, res, next) => { + versionController.fetchTags(req, res, next); +}); + +module.exports = router; diff --git a/comsapi/app/src/services/bucket.js b/comsapi/app/src/services/bucket.js new file mode 100644 index 00000000..8dba5e1c --- /dev/null +++ b/comsapi/app/src/services/bucket.js @@ -0,0 +1,238 @@ +const { v4: uuidv4, NIL: SYSTEM_USER } = require('uuid'); + +const bucketPermissionService = require('./bucketPermission'); +const { Permissions } = require('../components/constants'); +const { Bucket } = require('../db/models'); + +/** + * The Bucket DB Service + */ +const service = { + /** + * @function checkGrantPermissions + * Grants a user full permissions to the bucket if the data precisely matches + * accessKeyId and secretAccessKey values. + * @param {string} data.accessKeyId The S3 bucket access key id + * @param {string} data.bucket The S3 bucket identifier + * @param {string} data.endpoint The S3 bucket endpoint + * @param {string} data.key The relative S3 key/subpath managed by this bucket + * @param {string} data.secretAccessKey The S3 bucket secret access key + * @param {object} [etrx=undefined] An optional Objection Transaction object + * @returns {Promise} The result of running the insert operation + * @throws The error encountered upon db transaction failure + */ + checkGrantPermissions: async (data, etrx = undefined) => { + let trx; + try { + trx = etrx ? etrx : await Bucket.startTransaction(); + + // Get existing record from DB + const bucket = await service.readUnique({ + bucket: data.bucket, + endpoint: data.endpoint, + key: data.key ? data.key : '/' + }); + + if ( + bucket.accessKeyId === data.accessKeyId && + bucket.secretAccessKey === data.secretAccessKey + ) { + // Add all permission codes for the uploader + if (data.userId && data.userId !== SYSTEM_USER) { + const perms = Object.values(Permissions).map((p) => ({ + userId: data.userId, + permCode: p + })); + await bucketPermissionService.addPermissions(bucket.bucketId, perms, data.userId, trx); + } + } else { + throw new Error('Bucket credential mismatch'); + } + + if (!etrx) await trx.commit(); + return bucket; + } catch (err) { + if (!etrx && trx) await trx.rollback(); + throw err; + } + }, + + /** + * @function create + * Create a bucket record and give the uploader (if authed) permissions + * @param {string} data.bucketName The user-defined bucket name identifier + * @param {string} data.accessKeyId The S3 bucket access key id + * @param {string} data.bucket The S3 bucket identifier + * @param {string} data.endpoint The S3 bucket endpoint + * @param {string} data.key The relative S3 key/subpath managed by this bucket + * @param {string} data.secretAccessKey The S3 bucket secret access key + * @param {string} [data.region] The optional S3 bucket region + * @param {boolean} [data.active] The optional active flag - defaults to true if undefined + * @param {object} [etrx=undefined] An optional Objection Transaction object + * @returns {Promise} The result of running the insert operation + * @throws The error encountered upon db transaction failure + */ + create: async (data, etrx = undefined) => { + let trx; + try { + trx = etrx ? etrx : await Bucket.startTransaction(); + + // Add bucket record to DB + const obj = { + bucketId: uuidv4(), + bucketName: data.bucketName, + accessKeyId: data.accessKeyId, + bucket: data.bucket, + endpoint: data.endpoint, + key: data.key ? data.key : '/', + secretAccessKey: data.secretAccessKey, + region: data.region, + active: data.active, + createdBy: data.userId + }; + + const response = await Bucket.query(trx).insert(obj).returning('*'); + + // Add all permission codes for the uploader + if (data.userId && data.userId !== SYSTEM_USER) { + const perms = Object.values(Permissions).map((p) => ({ + userId: data.userId, + permCode: p + })); + await bucketPermissionService.addPermissions(obj.bucketId, perms, data.userId, trx); + } + + if (!etrx) await trx.commit(); + return Promise.resolve(response); + } catch (err) { + if (!etrx && trx) await trx.rollback(); + throw err; + } + }, + + /** + * @function delete + * Delete a bucket record. Note this will also delete all objects and permissions + * related to this specific bucket from the database. Use with caution. + * @param {string} bucketId The bucket uuid to delete + * @param {object} [etrx=undefined] An optional Objection Transaction object + * @returns {Promise} The result of running the delete operation + * @throws The error encountered upon db transaction failure + */ + delete: async (bucketId, etrx = undefined) => { + let trx; + try { + trx = etrx ? etrx : await Bucket.startTransaction(); + + const response = await Bucket.query(trx) + .deleteById(bucketId) + .throwIfNotFound() + // Returns array of deleted rows instead of count + // https://vincit.github.io/objection.js/recipes/returning-tricks.html + .returning('*'); + + if (!etrx) await trx.commit(); + return response; + } catch (err) { + if (!etrx && trx) await trx.rollback(); + throw err; + } + }, + + /** + * @function searchBuckets + * Search and filter for specific bucket records + * @param {string|string[]} [params.bucketId] Optional string or array of uuids representing the bucket + * @param {string} [params.bucketName] The optional user-defined bucket name identifier + * @param {string} [params.key] The optional relative S3 key/subpath + * @param {boolean} [params.active] The optional active flag + * @returns {Promise} The result of running the find operation + */ + searchBuckets: (params) => { + return Bucket.query() + .allowGraph('bucketPermission') + .modify('filterBucketIds', params.bucketId) + .modify('filterBucketName', params.bucketName) + .modify('filterKey', params.key) + .modify('filterActive', params.active) + .modify('filterUserId', params.userId) + .then(result => result.map(row => { + // eslint-disable-next-line no-unused-vars + const { bucketPermission, ...bucket } = row; + return bucket; + })); + }, + + /** + * @function read + * Get a bucket db record based on bucketId + * @param {string} bucketId The bucket uuid to read + * @returns {Promise} The result of running the read operation + * @throws If there are no records found + */ + read: (bucketId) => { + return Bucket.query() + .findById(bucketId) + .throwIfNotFound(); + }, + + /** + * @function readUnique + * Get a bucket db record based on unique parameters + * @param {string} data.bucket The S3 bucket identifier + * @param {string} data.endpoint The S3 bucket endpoint + * @param {string} data.key The relative S3 key/subpath managed by this bucket + * @returns {Promise} The result of running the read operation + * @throws If there are no records found + */ + readUnique: (data) => { + return Bucket.query() + .where('bucket', data.bucket) + .where('endpoint', data.endpoint) + .where('key', data.key) + .first() + .throwIfNotFound(); + }, + + /** + * @function update + * Update a bucket DB record + * @param {string} data.bucketId The bucket uuid + * @param {string} [data.bucketName] The optional user-defined bucket name identifier + * @param {string} [data.accessKeyId] The optional S3 bucket access key id + * @param {string} [data.bucket] The optional S3 bucket identifier + * @param {string} [data.endpoint] The optional S3 bucket endpoint + * @param {string} [data.secretAccessKey] The optional S3 bucket secret access key + * @param {string} [data.region] The optional S3 bucket region + * @param {boolean} [data.active] The optional active flag - defaults to true if undefined + * @param {object} [etrx=undefined] An optional Objection Transaction object + * @returns {Promise} The result of running the patch operation + * @throws The error encountered upon db transaction failure + */ + update: async (data, etrx = undefined) => { + let trx; + try { + trx = etrx ? etrx : await Bucket.startTransaction(); + + // Update bucket record in DB + const response = await Bucket.query(trx).patchAndFetchById(data.bucketId, { + bucketName: data.bucketName, + accessKeyId: data.accessKeyId, + bucket: data.bucket, + endpoint: data.endpoint, + secretAccessKey: data.secretAccessKey, + region: data.region, + active: data.active, + updatedBy: data.userId + }); + + if (!etrx) await trx.commit(); + return response; + } catch (err) { + if (!etrx && trx) await trx.rollback(); + throw err; + } + }, +}; + +module.exports = service; diff --git a/comsapi/app/src/services/bucketPermission.js b/comsapi/app/src/services/bucketPermission.js new file mode 100644 index 00000000..cd0e98aa --- /dev/null +++ b/comsapi/app/src/services/bucketPermission.js @@ -0,0 +1,143 @@ +const { v4: uuidv4, NIL: SYSTEM_USER } = require('uuid'); + +const { Permissions } = require('../components/constants'); +const { BucketPermission, ObjectPermission } = require('../db/models'); + +/** + * The Bucket Permission DB Service + */ +const service = { + /** + * @function addPermissions + * Grants bucket permissions to users + * @param {string} bucketId The bucketId uuid + * @param {object[]} data Incoming array of `userId` and `permCode` tuples to add for this `bucketId` + * @param {string} [currentUserId=SYSTEM_USER] The optional userId uuid actor; defaults to system user if unspecified + * @param {object} [etrx=undefined] An optional Objection Transaction object + * @returns {Promise} The result of running the insert operation + * @throws The error encountered upon db transaction failure + */ + addPermissions: async (bucketId, data, currentUserId = SYSTEM_USER, etrx = undefined) => { + if (!bucketId) { + throw new Error('Invalid bucketId supplied'); + } + if (!data || !Array.isArray(data) || !data.length) { + throw new Error('Invalid data supplied'); + } + + let trx; + try { + trx = etrx ? etrx : await BucketPermission.startTransaction(); + + // Get existing permissions for the current bucket + const currentPerms = await service.searchPermissions({ bucketId }); + const obj = data + // Ensure all codes are upper cased + .map(p => ({ ...p, code: p.permCode.toUpperCase().trim() })) + // Filter out any invalid code values + .filter(p => Object.values(Permissions).some(perm => perm === p.permCode)) + // Filter entry tuples that already exist + .filter(p => !currentPerms.some(cp => cp.userId === p.userId && cp.permCode === p.permCode)) + // Create DB buckets to insert + .map(p => ({ + id: uuidv4(), + userId: p.userId, + bucketId: bucketId, + permCode: p.permCode, + createdBy: currentUserId + })); + + // Insert missing entries + let response = []; + if (obj.length) { + response = await BucketPermission.query(trx).insertAndFetch(obj); + } + + if (!etrx) await trx.commit(); + return Promise.resolve(response); + } catch (err) { + if (!etrx && trx) await trx.rollback(); + throw err; + } + }, + + /** + * @function removePermissions + * Deletes bucket permissions for a user + * @param {string} bucketId The bucketId uuid + * @param {string[]} [userIds=undefined] Optional incoming array of user userId uuids to change + * @param {string[]} [permissions=undefined] An optional array of permission codes to remove; defaults to undefined + * @param {object} [etrx=undefined] An optional Objection Transaction object + * @returns {Promise} The result of running the delete operation + * @throws The error encountered upon db transaction failure + */ + removePermissions: async (bucketId, userIds = undefined, permissions = undefined, etrx = undefined) => { + if (!bucketId) { + throw new Error('Invalid bucketId supplied'); + } + + let trx; + try { + trx = etrx ? etrx : await BucketPermission.startTransaction(); + + let perms = undefined; + if (permissions && Array.isArray(permissions) && permissions.length) { + const cleanPerms = permissions + // Ensure all codes are upper cased + .map(p => p.toUpperCase().trim()) + // Filter out any invalid code values + .filter(p => Object.values(Permissions).some(perm => perm === p)); + // Set as undefined if empty array + perms = (cleanPerms.length) ? cleanPerms : undefined; + } + + const response = await BucketPermission.query(trx) + .delete() + .modify('filterUserId', userIds) + .modify('filterBucketId', bucketId) + .modify('filterPermissionCode', perms) + // Returns array of deleted rows instead of count + // https://vincit.github.io/objection.js/recipes/returning-tricks.html + .returning('*'); + + if (!etrx) await trx.commit(); + return response; + } catch (err) { + if (!etrx && trx) await trx.rollback(); + throw err; + } + }, + + /** + * @function listInheritedBucketIds + * Get buckets that contain objects with any permissions for given user(s) + * @param {string[]} [params.userId] Optional array of uuids representing the user + * @returns {Promise} The result of running the find operation + */ + listInheritedBucketIds: async (userIds = []) => { + return ObjectPermission.query() + .select('bucketId') + .distinct('userId') + .joinRelated('object') + .modify('filterUserId', userIds) + .whereNotNull('bucketId') + .then(response => response.map(entry => entry.bucketId)); + }, + + /** + * @function searchPermissions + * Search and filter for specific bucket permissions + * @param {string|string[]} [params.userId] Optional string or array of uuids representing the user + * @param {string|string[]} [params.bucketId] Optional string or array of uuids representing the bucket + * @param {string|string[]} [params.permCode] Optional string or array of permission codes + * @returns {Promise} The result of running the find operation + */ + searchPermissions: (params) => { + return BucketPermission.query() + .modify('filterUserId', params.userId) + .modify('filterBucketId', params.bucketId) + .modify('filterPermissionCode', params.permCode); + } +}; + +module.exports = service; diff --git a/comsapi/app/src/services/index.js b/comsapi/app/src/services/index.js new file mode 100644 index 00000000..d5c1540b --- /dev/null +++ b/comsapi/app/src/services/index.js @@ -0,0 +1,13 @@ +module.exports = { + bucketService: require('./bucket'), + bucketPermissionService: require('./bucketPermission'), + metadataService: require('./metadata'), + objectService: require('./object'), + objectQueueService: require('./objectQueue'), + objectPermissionService: require('./objectPermission'), + storageService: require('./storage'), + syncService: require('./sync'), + tagService: require('./tag'), + userService: require('./user'), + versionService: require('./version'), +}; diff --git a/comsapi/app/src/services/metadata.js b/comsapi/app/src/services/metadata.js new file mode 100644 index 00000000..c2d3f833 --- /dev/null +++ b/comsapi/app/src/services/metadata.js @@ -0,0 +1,322 @@ +const { NIL: SYSTEM_USER } = require('uuid'); +const { ObjectModel, Metadata, VersionMetadata, Version } = require('../db/models'); +const { getObjectsByKeyValue } = require('../components/utils'); + +/** + * The Metadata DB Service + */ +const service = { + /** + * @function associateMetadata + * Makes the incoming list of metadata the definitive set associated with versionId + * Dissociaate extraneous metadata and also does collision detection for null versions (non-versioned) + * @param {string} versionId The uuid id column from version table + * @param {object[]} metadata Incoming array of metadata objects to add for this version (eg: [{ key: 'a', value: '1'}, {key: 'B', value: '2'}]). + * This will always be the definitive metadata we want on the version + * @param {string} [currentUserId=SYSTEM_USER] The optional userId uuid actor; defaults to system user if unspecified + * @param {object} [etrx=undefined] An optional Objection Transaction object + * @returns {Promise} The result of running the insert operation + * @throws The error encountered upon db transaction failure + */ + associateMetadata: async (versionId, metadata, currentUserId = SYSTEM_USER, etrx = undefined) => { + let trx; + try { + trx = etrx ? etrx : await Metadata.startTransaction(); + let response = []; + + if (metadata && metadata.length) { + // get DB records of all input metadata + const dbMetadata = await service.createMetadata(metadata, trx); + + // for non-versioned objects we are updating metadata joins for an existing version + const associatedMetadata = await VersionMetadata.query(trx) + .modify('filterVersionId', versionId); + // remove existing joins for metadata that is not in incomming set + if (associatedMetadata.length) { + const dissociateMetadata = associatedMetadata.filter(({ metadataId }) => !dbMetadata.some(({ id }) => id === metadataId)); + if (dissociateMetadata.length) { + await VersionMetadata.query(trx) + .whereIn('metadataId', dissociateMetadata.map(vm => vm.metadataId)) + .modify('filterVersionId', versionId) + .delete(); + } + } + + // join new metadata + const newJoins = associatedMetadata.length ? dbMetadata.filter(({ id }) => !associatedMetadata.some(({ metadataId }) => metadataId === id)) : dbMetadata; + + if (newJoins.length) { + response = await VersionMetadata.query(trx) + .insert(newJoins.map(({ id }) => ({ + versionId: versionId, + metadataId: id, + createdBy: currentUserId + }))); + } + + // delete all orphaned metadata records + await service.pruneOrphanedMetadata(trx); + // TODO: call a dissociateMetadata() function for this version + // and prune old metadata from there. + + } + + if (!etrx) await trx.commit(); + return Promise.resolve(response); + } catch (err) { + if (!etrx && trx) await trx.rollback(); + throw err; + } + }, + + /** + * @function createMetadata + * Inserts any metadata records if they dont already exist in db + * @param {object} metadata Incoming object with `:` metadata to add for this version + * @param {object} [etrx=undefined] An optional Objection Transaction object + * @returns {Promise} an array of all input metadata + * @throws The error encountered upon db transaction failure + */ + createMetadata: async (metadata, etrx = undefined) => { + let trx; + let response = []; + try { + trx = etrx ? etrx : await Metadata.startTransaction(); + + // get all metadata already in db + const allMetadata = await Metadata.query(trx).select(); + const existingMetadata = []; + const newMetadata = []; + + metadata.forEach(({ key, value }) => { + // if metadata is already in db + if (getObjectsByKeyValue(allMetadata, key, value)) { + // add metadata object to existingMetadata array + existingMetadata.push({ id: getObjectsByKeyValue(allMetadata, key, value).id, key: key, value: value }); + } + // else add to array for inserting + else { + newMetadata.push({ key: key, value: value }); + } + }); + // insert new metadata + if (newMetadata.length) { + const newMetadataRecords = await Metadata.query(trx) + .insert(newMetadata) + .returning('*'); + // merge new with existing metadata + response = existingMetadata.concat(newMetadataRecords); + } + else { + response = existingMetadata; + } + + if (!etrx) await trx.commit(); + return Promise.resolve(response); + } catch (err) { + if (!etrx && trx) await trx.rollback(); + throw err; + } + }, + + + /** + * @function dissociateMetadata + * Dissociates all provided metadata from a version + * @param {string} versionId The uuid id column from version table + * @param {object[]} [metadata=undefined] array of metadata (eg: [{ key: 'a', value: '1'}, {key: 'B', value: ''}]) + * @param {object} [etrx=undefined] An optional Objection Transaction object + * @returns {Promise} The result of running the delete operation (number of rows deleted) + * @throws The error encountered upon db transaction failure + */ + dissociateMetadata: async (versionId, metadata = undefined, etrx = undefined) => { + let trx; + try { + trx = etrx ? etrx : await Metadata.startTransaction(); + let response = 0; + + // TODO: consider doing one bulk delete query instead of using forEach + // eg: get id's of provided metadata records and do whereIn().delete() + metadata.forEach(async meta => { + // match on key + const params = { 'metadata.key': meta.key }; + // if metadata has a value match key and value + if (meta.value && meta.value !== '') params['metadata.value'] = meta.value; + + let count = 0; + count = await VersionMetadata.query(trx) + .allowGraph('metadata') + .withGraphJoined('metadata') + .where(params) + .modify('filterVersionId', versionId) + .delete(); + + if (count) response += count; + }); + + // delete all orphaned metadata + await service.pruneOrphanedMetadata(trx); + + if (!etrx) await trx.commit(); + return Promise.resolve(response); + } catch (err) { + if (!etrx && trx) await trx.rollback(); + throw err; + } + }, + + /** + * @function fetchMetadataForObject + * Fetch metadata for specific objects, optionally scoped to a user's object/bucket READ permission + * @param {string[]} [params.bucketIds] An array of uuids representing buckets + * @param {string[]} params.objId An array of uuids representing the object + * @param {object} [params.metadata] Optional object of metadata key/value pairs + * @param {string} [params.userId] Optional uuid representing a user + * @returns {Promise} The result of running the find operation + */ + fetchMetadataForObject: (params) => { + return ObjectModel.query() + .select('object.id AS objectId', 'object.bucketId as bucketId') + .allowGraph('version.metadata') + .withGraphJoined('version.metadata') + // get latest version that isn't a delete marker by default + .modifyGraph('version', builder => { + builder + .select('version.id', 'version.objectId') + .orderBy([ + 'version.objectId', + { column: 'version.createdAt', order: 'desc' } + ]) + .where('deleteMarker', false) + .distinctOn('version.objectId'); + }) + // match on metadata parameter + .modifyGraph('version.metadata', builder => { + builder + .select('key', 'value') + .modify('filterKeyValue', { metadata: params.metadata }); + }) + // match on objId parameter + .modify('filterIds', params.objId) + // match on bucketIds parameter + .modify('filterBucketIds', params.bucketIds) + // scope to objects that user(s) has READ permission at object or bucket-level + .modify('hasPermission', params.userId, 'READ') + // re-structure result like: [{ objectId: abc, metadata: [{ key: a, value: b }] }] + .then(result => result.map(row => { + return { + objectId: row.objectId, + metadata: row.version[0].metadata + }; + })); + }, + + /** + * @function fetchMetadataForVersion + * Fetch metadata for specific versions, optionally scoped to a user's object/bucket READ permission + * @param {string[]} [params.versionIds] An array of uuids representing versions + * @param {object} [params.metadata] Optional object of metadata key/value pairs + * @param {string} [params.userId] Optional uuid representing a user + * @param {object} [etrx=undefined] An optional Objection Transaction object + * @returns {Promise} The result of running the database select + */ + fetchMetadataForVersion: async (params, etrx = undefined) => { + let trx; + try { + trx = etrx ? etrx : await Metadata.startTransaction(); + + const response = await Version.query(trx) + .select('version.id as versionId', 'version.s3VersionId') + .allowGraph('metadata') + .withGraphJoined('metadata') + .modifyGraph('metadata', builder => { + builder + .select('key', 'value') + .modify('filterKeyValue', { metadata: params.metadata }); + }) + .modify((query) => { + if (params.s3VersionIds) query.modify('filterS3VersionId', params.s3VersionIds); + else query.modify('filterId', params.versionIds); + }) + .modify('filterId', params.versionIds) + // filter by objects that user(s) has READ permission at object or bucket-level + .modify((query) => { + if (params.userId) { + query + .allowGraph('object') + .withGraphJoined('object') + .modifyGraph('object', query => { query.modify('hasPermission', params.userId, 'READ'); }) + .whereNotNull('object.id'); + } + }) + // format result + .orderBy('version.createdAt', 'desc') + .then(result => result.map(row => { + // eslint-disable-next-line no-unused-vars + const { object, ...data } = row; + return data; + })); + + if (!etrx) await trx.commit(); + return Promise.resolve(response); + } catch (err) { + if (!etrx && trx) await trx.rollback(); + throw err; + } + }, + + /** + * @function pruneOrphanedMetadata + * deletes Metadata records if they are no longer related to any versions + * @param {object} [etrx=undefined] An optional Objection Transaction object + * @returns {Promise} The result of running the delete operation (number of rows deleted) + * @throws The error encountered upon db transaction failure + */ + pruneOrphanedMetadata: async (etrx = undefined) => { + let trx; + try { + trx = etrx ? etrx : await Metadata.startTransaction(); + + const deletedMetadataIds = await Metadata.query(trx) + .allowGraph('versionMetadata') + .withGraphJoined('versionMetadata') + .select('metadata.id') + .whereNull('versionMetadata.metadataId'); + + const response = await Metadata.query(trx) + .delete() + .whereIn('id', deletedMetadataIds.map(({ id }) => id)); + + if (!etrx) await trx.commit(); + return Promise.resolve(response); + } catch (err) { + if (!etrx && trx) await trx.rollback(); + throw err; + } + }, + + /** + * @function searchMetadata + * Search and filter for specific metadata keys + * @param {object} [params.metadata] Optional object of metadata keys to filter on + * @param {string} [params.userId] Optional uuid representing a user + * @returns {Promise} The result of running the find operation + */ + searchMetadata: (params) => { + return Metadata.query() + .modify((query) => { + if (params.privacyMask) { + query + .select('key') + .modify('filterKey', { metadata: params.metadata }); + } + else { + query + .select('key', 'value') + .modify('filterKeyValue', { metadata: params.metadata }); + } + }); + }, +}; + +module.exports = service; diff --git a/comsapi/app/src/services/object.js b/comsapi/app/src/services/object.js new file mode 100644 index 00000000..736bb20b --- /dev/null +++ b/comsapi/app/src/services/object.js @@ -0,0 +1,218 @@ +const { NIL: SYSTEM_USER } = require('uuid'); + +const objectPermissionService = require('./objectPermission'); +const { Permissions } = require('../components/constants'); +const { ObjectModel } = require('../db/models'); + +/** + * The Object DB Service + */ +const service = { + /** + * @function create + * Create an object DB record and give the uploader (if authed) permissions + * @param {string} data.id The object uuid + * @param {string} data.userId The uploading user userId + * @param {string} data.path The relative S3 key/path of the object + * @param {boolean} [data.public] The optional public flag - defaults to true if undefined + * @param {boolean} [data.active] The optional active flag - defaults to true if undefined + * @param {string} [data.bucketId] The optional associated bucketId + * @param {object} [etrx=undefined] An optional Objection Transaction object + * @returns {Promise} The result of running the insert operation + * @throws The error encountered upon db transaction failure + */ + create: async (data, etrx = undefined) => { + let trx; + try { + trx = etrx ? etrx : await ObjectModel.startTransaction(); + + // Add object record to DB + const obj = { + id: data.id, + name: data.name, + path: data.path, + public: data.public, + active: data.active, + bucketId: data.bucketId, + createdBy: data.userId ?? SYSTEM_USER + }; + const response = await ObjectModel.query(trx).insert(obj).returning('*'); + + // Add all permission codes for the uploader + if (data.userId && data.userId !== SYSTEM_USER) { + const perms = Object.values(Permissions).map((p) => ({ + userId: data.userId, + permCode: p + })); + await objectPermissionService.addPermissions(data.id, perms, data.userId, trx); + } + + if (!etrx) await trx.commit(); + return Promise.resolve(response); + } catch (err) { + if (!etrx && trx) await trx.rollback(); + throw err; + } + }, + + /** + * @function delete + * Delete an object record + * @param {string} objId The object uuid to delete + * @param {object} [etrx=undefined] An optional Objection Transaction object + * @returns {Promise} The result of running the delete operation + * @throws The error encountered upon db transaction failure + */ + delete: async (objId, etrx = undefined) => { + let trx; + try { + trx = etrx ? etrx : await ObjectModel.startTransaction(); + + const response = await ObjectModel.query(trx) + .deleteById(objId) + .throwIfNotFound() + // Returns array of deleted rows instead of count + // https://vincit.github.io/objection.js/recipes/returning-tricks.html + .returning('*'); + + if (!etrx) await trx.commit(); + return response; + } catch (err) { + if (!etrx && trx) await trx.rollback(); + throw err; + } + }, + + /** + * @function getBucketKey + * Gets the associated key path for a specific object record + * @param {string} objId The object uuid to read + * @returns {object.key} A single object an attribute key + * @throws If there are no records found + */ + getBucketKey: (objId) => { + return ObjectModel.query() + .findById(objId) + .select('bucket.key') + .joinRelated('bucket') + .first() + .throwIfNotFound(); + }, + + /** + * @function searchObjects + * Search and filter for specific object records + * @param {string|string[]} [params.id] Optional string or array of uuids representing the object + * @param {string|string[]} [params.bucketId] Optional string or array of uuids representing bucket ids + * @param {string} [params.path] Optional canonical S3 path string to match on + * @param {boolean} [params.public] Optional boolean on object public status + * @param {boolean} [params.active] Optional boolean on object active + * @param {string} [params.userId] Optional uuid string representing the user + * @param {string} [params.mimeType] Optional mimeType string to match on + * @param {string} [params.name] Optional metadata name string to match on + * @param {object} [params.metadata] Optional object of metadata key/value pairs + * @param {object} [params.tag] Optional object of tag key/value pairs + * @param {object} [etrx=undefined] An optional Objection Transaction object + * @returns {Promise} The result of running the find operation + */ + searchObjects: async (params, etrx = undefined) => { + let trx; + try { + trx = etrx ? etrx : await ObjectModel.startTransaction(); + + const response = await ObjectModel.query(trx) + .allowGraph('version') + .modify('filterIds', params.id) + .modify('filterBucketIds', params.bucketId) + .modify('filterName', params.name) + .modify('filterPath', params.path) + .modify('filterPublic', params.public) + .modify('filterActive', params.active) + .modify('filterMimeType', params.mimeType) + .modify('filterDeleteMarker', params.deleteMarker) + .modify('filterLatest', params.latest) + .modify('filterMetadataTag', { + metadata: params.metadata, + tag: params.tag + }) + .modify('hasPermission', params.userId, 'READ') + // format result + .then(result => { + // just return object table records + const res = result.map(row => { + // eslint-disable-next-line no-unused-vars + const { objectPermission, bucketPermission, version, ...object } = row; + return object; + }); + // remove duplicates + return [...new Map(res.map(item => [item.id, item])).values()]; + }); + + if (!etrx) await trx.commit(); + return Promise.resolve(response); + } catch (err) { + if (!etrx && trx) await trx.rollback(); + throw err; + } + }, + + /** + * @function read + * Get an object db record + * @param {string} objId The object uuid to read + * @param {object} [etrx=undefined] An optional Objection Transaction object + * @returns {Promise} The result of running the read operation + * @throws If there are no records found + */ + read: async (objId, etrx = undefined) => { + let trx; + try { + trx = etrx ? etrx : await ObjectModel.startTransaction(); + + const response = await ObjectModel.query(trx) + .findById(objId) + .throwIfNotFound(); + + if (!etrx) await trx.commit(); + return Promise.resolve(response); + } catch (err) { + if (!etrx && trx) await trx.rollback(); + throw err; + } + }, + + /** + * @function update + * Update an object DB record + * @param {string} data.id The object uuid + * @param {string} data.userId The uploading user userId + * @param {string} data.path The relative S3 key/path of the object + * @param {boolean} [data.public] The optional public flag - defaults to true if undefined + * @param {boolean} [data.active] The optional active flag - defaults to true if undefined + * @param {object} [etrx=undefined] An optional Objection Transaction object + * @returns {Promise} The result of running the patch operation + * @throws The error encountered upon db transaction failure + */ + update: async (data, etrx = undefined) => { + let trx; + try { + trx = etrx ? etrx : await ObjectModel.startTransaction(); + + // Update object record in DB + const response = await ObjectModel.query(trx).patchAndFetchById(data.id, { + path: data.path, + public: data.public, + active: data.active, + updatedBy: data.userId ?? SYSTEM_USER + }); + + if (!etrx) await trx.commit(); + return response; + } catch (err) { + if (!etrx && trx) await trx.rollback(); + throw err; + } + }, +}; + +module.exports = service; diff --git a/comsapi/app/src/services/objectPermission.js b/comsapi/app/src/services/objectPermission.js new file mode 100644 index 00000000..e91ab0ca --- /dev/null +++ b/comsapi/app/src/services/objectPermission.js @@ -0,0 +1,152 @@ +const { v4: uuidv4, NIL: SYSTEM_USER } = require('uuid'); + +const { Permissions } = require('../components/constants'); +const { BucketPermission, ObjectPermission } = require('../db/models'); + +/** + * The Object Permission DB Service + */ +const service = { + /** + * @function addPermissions + * Grants object permissions to users + * @param {string} objId The objectId uuid + * @param {object[]} data Incoming array of `userId` and `permCode` tuples to add for this `objId` + * @param {string} [currentUserId=SYSTEM_USER] The optional userId uuid actor; defaults to system user if unspecified + * @param {object} [etrx=undefined] An optional Objection Transaction object + * @returns {Promise} The result of running the insert operation + * @throws The error encountered upon db transaction failure + */ + addPermissions: async (objId, data, currentUserId = SYSTEM_USER, etrx = undefined) => { + if (!objId) { + throw new Error('Invalid objId supplied'); + } + if (!data || !Array.isArray(data) || !data.length) { + throw new Error('Invalid data supplied'); + } + + let trx; + try { + trx = etrx ? etrx : await ObjectPermission.startTransaction(); + + // Get existing permissions for the current object + const currentPerms = await service.searchPermissions({ objId }); + const obj = data + // Ensure all codes are upper cased + .map(p => ({ ...p, code: p.permCode.toUpperCase().trim() })) + // Filter out any invalid code values + .filter(p => Object.values(Permissions).some(perm => perm === p.permCode)) + // Filter entry tuples that already exist + .filter(p => !currentPerms.some(cp => cp.userId === p.userId && cp.permCode === p.permCode)) + // Create DB objects to insert + .map(p => ({ + id: uuidv4(), + userId: p.userId, + objectId: objId, + permCode: p.permCode, + createdBy: currentUserId, + })); + + // Insert missing entries + let response = []; + if (obj.length) { + response = await ObjectPermission.query(trx).insertAndFetch(obj); + } + + if (!etrx) await trx.commit(); + return Promise.resolve(response); + } catch (err) { + if (!etrx && trx) await trx.rollback(); + throw err; + } + }, + + /** + * @function listInheritedObjectIds + * Get objects that are in bucket(s) with given permission(s) for given user(s) + * with given permission(s) for given user(s). + * @param {string[]} [params.userIds] Optional array of user id(s) + * @param {string[]} [params.bucketIds] Optional array of bucket id(s) + * @param {string[]} [params.permCodes] Optional array of PermCode(s) + * @returns {Promise} The result of running the find operation + */ + listInheritedObjectIds: async (userIds = [], bucketIds = [], permCodes = []) => { + return BucketPermission.query() + .distinct('object.id AS objectId') + .rightJoin('object', 'bucket_permission.bucketId', '=', 'object.bucketId') + .modify((query) => { + if (userIds.length) query.modify('filterUserId', userIds); + }) + .modify((query) => { + if (bucketIds.length) query.whereIn('bucket_permission.bucketId', bucketIds); + if (permCodes.length) query.whereIn('bucket_permission.permCode', permCodes); + }) + .then(response => response.map(entry => entry.objectId)); + }, + + /** + * @function removePermissions + * Deletes object permissions for a user + * @param {string} objId The objectId uuid + * @param {string[]} [userIds=undefined] Optional incoming array of user userId uuids to change + * @param {string[]} [permissions=undefined] An optional array of permission codes to remove; defaults to undefined + * @param {object} [etrx=undefined] An optional Objection Transaction object + * @returns {Promise} The result of running the delete operation + * @throws The error encountered upon db transaction failure + */ + removePermissions: async (objId, userIds = undefined, permissions = undefined, etrx = undefined) => { + if (!objId) { + throw new Error('Invalid objId supplied'); + } + + let trx; + try { + trx = etrx ? etrx : await ObjectPermission.startTransaction(); + + let perms = undefined; + if (permissions && Array.isArray(permissions) && permissions.length) { + const cleanPerms = permissions + // Ensure all codes are upper cased + .map(p => p.toUpperCase().trim()) + // Filter out any invalid code values + .filter(p => Object.values(Permissions).some(perm => perm === p)); + // Set as undefined if empty array + perms = (cleanPerms.length) ? cleanPerms : undefined; + } + + const response = await ObjectPermission.query(trx) + .delete() + .modify('filterUserId', userIds) + .modify('filterObjectId', objId) + .modify('filterPermissionCode', perms) + // Returns array of deleted rows instead of count + // https://vincit.github.io/objection.js/recipes/returning-tricks.html + .returning('*'); + + if (!etrx) await trx.commit(); + return response; + } catch (err) { + if (!etrx && trx) await trx.rollback(); + throw err; + } + }, + + /** + * @function searchPermissions + * Search and filter for specific object permissions + * @param {string|string[]} [params.bucketId] Optional string or array of uuids representing the bucket + * @param {string|string[]} [params.userId] Optional string or array of uuids representing the user + * @param {string|string[]} [params.objId] Optional string or array of uuids representing the object + * @param {string|string[]} [params.permCode] Optional string or array of permission codes + * @returns {Promise} The result of running the find operation + */ + searchPermissions: (params) => { + return ObjectPermission.query() + .modify('filterBucketId', params.bucketId) + .modify('filterUserId', params.userId) + .modify('filterObjectId', params.objId) + .modify('filterPermissionCode', params.permCode); + } +}; + +module.exports = service; diff --git a/comsapi/app/src/services/objectQueue.js b/comsapi/app/src/services/objectQueue.js new file mode 100644 index 00000000..b78cd820 --- /dev/null +++ b/comsapi/app/src/services/objectQueue.js @@ -0,0 +1,84 @@ +const { NIL: SYSTEM_USER } = require('uuid'); + +const { ObjectQueue } = require('../db/models'); + +/** + * The Object Queue DB Service + */ +const service = { + /** + * @function dequeue + * Removes a job from the object queue. + * @param {object} [etrx=undefined] Optional Objection Transaction object + * @returns {Promise} An ObjectQueue if available. + */ + async dequeue(etrx = undefined) { + let trx; + try { + trx = etrx ? etrx : await ObjectQueue.startTransaction(); + + const response = await ObjectQueue.query(trx) + .modify('findNextJob') + .delete() + // Returns array of deleted rows instead of count + // https://vincit.github.io/objection.js/recipes/returning-tricks.html + .returning('*'); + + if (!etrx) await trx.commit(); + return Promise.resolve(response); + } catch (err) { + if (!etrx && trx) await trx.rollback(); + throw err; + } + }, + + /** + * @function enqueue + * Adds a set of jobs to the object queue. If the same job already exists, the existing + * job will take precedence and will not be reinserted. + * @param {object[]} options.jobs An array of job objects typically containing `path` and `bucketId` attributes + * @param {boolean} [options.full=false] Optional boolean indicating whether to execute full recursive run + * @param {integer} [options.retries=0] Optional integer indicating how many previous retries this job has had + * @param {string} [options.createdBy=SYSTEM_USER] Optional uuid attributing which user added the job + * @param {object} [etrx=undefined] Optional Objection Transaction object + * @returns {Promise} Number of records added to the queue + */ + async enqueue({ jobs = [], full = false, retries = 0, createdBy = SYSTEM_USER } = {}, etrx = undefined) { + let trx; + try { + trx = etrx ? etrx : await ObjectQueue.startTransaction(); + + const jobsArray = jobs.map(job => ({ + bucketId: job.bucketId, + path: job.path, + full: full, + retries: retries, + createdBy: createdBy + })); + + // Short circuit when nothing to add or there are missing paths + if (!jobsArray.length || !jobsArray.every(job => !!job.path)) return Promise.resolve(0); + + // Only insert jobs in if it does not already exist + const response = await ObjectQueue.query(trx).insert(jobsArray).onConflict().ignore(); + + if (!etrx) await trx.commit(); + return Promise.resolve(response.reduce((acc, job) => job?.id ? acc + 1 : acc, 0)); + } catch (err) { + if (!etrx && trx) await trx.rollback(); + throw err; + } + }, + + /** + * @function queueSize + * Returns the number of jobs currently waiting for processing in the object queue. + * @returns {Promise} An integer representing how many jobs are in the queue. + */ + async queueSize() { + return ObjectQueue.query().count().first() + .then(response => parseInt(response.count)); + }, +}; + +module.exports = service; diff --git a/comsapi/app/src/services/storage.js b/comsapi/app/src/services/storage.js new file mode 100644 index 00000000..aa2c37b0 --- /dev/null +++ b/comsapi/app/src/services/storage.js @@ -0,0 +1,523 @@ +const { + CopyObjectCommand, + DeleteObjectCommand, + DeleteObjectTaggingCommand, + GetBucketEncryptionCommand, + GetBucketVersioningCommand, + GetObjectCommand, + GetObjectTaggingCommand, + HeadBucketCommand, + HeadObjectCommand, + ListObjectsV2Command, + ListObjectVersionsCommand, + PutObjectCommand, + PutBucketEncryptionCommand, + PutObjectTaggingCommand, + S3Client, +} = require('@aws-sdk/client-s3'); +const { Upload } = require('@aws-sdk/lib-storage'); +const { getSignedUrl } = require('@aws-sdk/s3-request-presigner'); +const config = require('config'); + +const { MetadataDirective, TaggingDirective } = require('../components/constants'); +const log = require('../components/log')(module.filename); +const utils = require('../components/utils'); + +const DELIMITER = '/'; + +// Get app configuration +const defaultTempExpiresIn = parseInt(config.get('server.defaultTempExpiresIn'), 10); + +/** + * The Core S3 Object Storage Service + * @see {@link https://docs.aws.amazon.com/AWSJavaScriptSDK/v3/latest/clients/client-s3/} + */ +const objectStorageService = { + /** + * @private + * @function _getS3Client + * The AWS S3Client used for interacting with S3 compatible storage + * @param {string} options.accessKeyId The S3 Bucket accessKeyId + * @param {string} options.endpoint The S3 Bucket endpoint + * @param {string} options.region The S3 Bucket region + * @param {string} options.secretAccessKey The S3 Bucket secretAccessKey + * @param {Readable} stream A readable stream object + * @returns {object} A pre-configured S3 Client object + */ + _getS3Client: ({ accessKeyId, endpoint, region, secretAccessKey } = {}) => { + if (!accessKeyId || !endpoint || !region || !secretAccessKey) { + log.error('Unable to generate S3Client due to missing arguments', { function: '_getS3Client' }); + } + + return new S3Client({ + credentials: { + accessKeyId: accessKeyId, + secretAccessKey: secretAccessKey + }, + endpoint: endpoint, + forcePathStyle: true, + logger: ['silly', 'debug'].includes(config.get('server.logLevel')) ? log : undefined, + region: region + }); + }, + + /** + * @function copyObject + * Creates a copy of the object at `copySource` for the same bucket + * @param {string} options.copySource Specifies the source object for the copy operation, excluding the bucket name + * @param {string} options.filePath The filePath of the object + * @param {object} [options.metadata] Optional metadata to store with the object + * @param {object} [options.tags] Optional tags to store with the object + * @param {string} [options.metadataDirective=COPY] Optional metadata operation directive + * @param {string} [options.taggingDirective=COPY] Optional tagging operation directive + * @param {string} [options.s3VersionId] Optional s3VersionId to copy from + * @param {string} [options.bucketId] Optional bucketId + * @returns {Promise} The response of the copy object operation + */ + async copyObject({ + copySource, + filePath, + metadata, + tags, + metadataDirective = MetadataDirective.COPY, + taggingDirective = TaggingDirective.COPY, + s3VersionId = undefined, + bucketId = undefined + }) { + const data = await utils.getBucket(bucketId); + const params = { + Bucket: data.bucket, + CopySource: `${data.bucket}/${copySource}`, + Key: filePath, + Metadata: metadata, + MetadataDirective: metadataDirective, + TaggingDirective: taggingDirective, + VersionId: s3VersionId + }; + + if (tags) { + params.Tagging = Object.entries(tags).map(([key, value]) => { + return `${key}=${encodeURIComponent(value)}`; + }).join('&'); + } + + return this._getS3Client(data).send(new CopyObjectCommand(params)); + }, + + /** + * @function deleteObject + * Deletes the object at `filePath` + * @param {string} options.filePath The filePath of the object + * @param {number} [options.s3VersionId] Optional specific s3VersionId for the object + * @param {string} [options.bucketId] Optional bucketId + * @returns {Promise} The response of the delete object operation + */ + async deleteObject({ filePath, s3VersionId = undefined, bucketId = undefined }) { + const data = await utils.getBucket(bucketId); + const params = { + Bucket: data.bucket, + Key: filePath, + VersionId: s3VersionId + }; + + return this._getS3Client(data).send(new DeleteObjectCommand(params)); + }, + + /** + * @function deleteObjectTagging + * Deletes the tags of the object at `filePath` + * @param {string} options.filePath The filePath of the object + * @param {number} [options.s3VersionId] Optional specific s3VersionId for the object + * @param {string} [options.bucketId] Optional bucketId + * @returns {Promise} The response of the delete object tagging operation + */ + async deleteObjectTagging({ filePath, s3VersionId = undefined, bucketId = undefined }) { + const data = await utils.getBucket(bucketId); + const params = { + Bucket: data.bucket, + Key: filePath, + VersionId: s3VersionId + }; + + return this._getS3Client(data).send(new DeleteObjectTaggingCommand(params)); + }, + + /** + * @function getBucketEncryption + * Checks if encryption of objects is enabled by default on bucket + * @param {string} [bucketId] Optional bucketId + * @returns {Promise} The response of the get bucket encryption operation + */ + async getBucketEncryption(bucketId = undefined) { + const data = await utils.getBucket(bucketId); + const params = { + Bucket: data.bucket + }; + + return this._getS3Client(data).send(new GetBucketEncryptionCommand(params)); + }, + + /** + * @function getBucketVersioning + * Checks if versioning of objects is enabled on bucket + * @param {string} [bucketId] Optional bucketId + * @returns {Promise} true if versioning enabled otherwise false + */ + async getBucketVersioning(bucketId = undefined) { + const data = await utils.getBucket(bucketId); + const params = { + Bucket: data.bucket + }; + const response = await this._getS3Client(data).send(new GetBucketVersioningCommand(params)); + return Promise.resolve(response.Status === 'Enabled'); + }, + + /** + * @function getObjectTagging + * Gets the tags of the object at `filePath` + * @param {string} options.filePath The filePath of the object + * @param {number} [options.s3VersionId=undefined] Optional specific s3VersionId for the object + * @param {string} [options.bucketId] Optional bucketId + * @returns {Promise} The response of the get object tagging operation + */ + async getObjectTagging({ filePath, s3VersionId = undefined, bucketId = undefined }) { + const data = await utils.getBucket(bucketId); + const params = { + Bucket: data.bucket, + Key: filePath, + VersionId: s3VersionId + }; + + return this._getS3Client(data).send(new GetObjectTaggingCommand(params)); + }, + + /** + * @function headBucket + * Checks if a bucket exists and if the S3Client has correct access permissions + * You must supply either a `bucketId`, or the full S3 client credentials to test against + * @param {string} [options.bucketId] Optional bucketId + * @param {string} [options.accessKeyId] Optional S3 accessKeyId + * @param {string} [options.bucket] Optional S3 bucket + * @param {string} [options.endpoint] Optional S3 endpoint + * @param {string} [options.key] Optional S3 key/prefix + * @param {string} [options.region] Optional S3 region + * @param {string} [options.secretAccessKey] Optional S3 secretAccessKey + * @returns {Promise} The response of the head bucket operation + */ + async headBucket(options = {}) { + const data = options.bucketId + ? await utils.getBucket(options.bucketId) + : options; + const params = { + Bucket: data.bucket, + }; + + return this._getS3Client(data).send(new HeadBucketCommand(params)); + }, + + /** + * @function headObject + * Gets the object headers for the object at `filePath` + * @param {string} options.filePath The filePath of the object + * @param {string} [options.s3VersionId] Optional version ID used to reference a speciific version of the object + * @param {string} [options.bucketId] Optional bucketId + * @returns {Promise} The response of the head object operation + * @throws If object is not found + */ + async headObject({ filePath, s3VersionId = undefined, bucketId = undefined }) { + const data = await utils.getBucket(bucketId); + const params = { + Bucket: data.bucket, + Key: filePath, + VersionId: s3VersionId + }; + return this._getS3Client(data).send(new HeadObjectCommand(params)); + }, + + /** + * @function listAllObjects + * Lists all objects in the bucket with the prefix of `filePath`. + * Performs pagination behind the scenes if required. + * @param {string} [options.filePath=undefined] Optional filePath of the objects + * @param {string} [options.bucketId=undefined] Optional bucketId + * @param {boolean} [options.precisePath=true] Optional boolean for filtering results based on the precise path + * @returns {Promise} An array of objects matching the criteria + */ + async listAllObjects({ filePath = undefined, bucketId = undefined, precisePath = true } = {}) { + const key = filePath ?? (await utils.getBucket(bucketId)).key; + const path = key !== DELIMITER ? key : ''; + + const objects = []; + + let incomplete = false; + let nextToken = undefined; + do { + const { Contents, IsTruncated, NextContinuationToken } = await this.listObjectsV2({ + filePath: path, + continuationToken: nextToken, + bucketId: bucketId + }); + + if (Contents) objects.push( + ...Contents.filter(object => !precisePath || utils.isAtPath(path, object.Key)) + ); + incomplete = IsTruncated; + nextToken = NextContinuationToken; + } while (incomplete); + + return Promise.resolve(objects); + }, + + /** + * @function listAllObjectVersions + * Lists all objects in the bucket with the prefix of `filePath`. + * Performs pagination behind the scenes if required. + * @param {string} [options.filePath=undefined] Optional filePath of the objects + * @param {string} [options.bucketId=undefined] Optional bucketId + * @param {boolean} [options.precisePath=true] Optional boolean for filtering results based on the precise path + * @param {boolean} [options.filterLatest=false] Optional boolean for filtering results to only entries with IsLatest being true + * @returns {Promise} An object containg an array of DeleteMarkers and Versions + */ + async listAllObjectVersions({ filePath = undefined, bucketId = undefined, precisePath = true, filterLatest = false } = {}) { + const key = filePath ?? (await utils.getBucket(bucketId)).key; + const path = key !== DELIMITER ? key : ''; + + const deleteMarkers = []; + const versions = []; + + let incomplete = false; + let nextKeyMarker = undefined; + do { + const { DeleteMarkers, Versions, IsTruncated, NextKeyMarker } = await this.listObjectVersion({ + filePath: path, + keyMarker: nextKeyMarker, + bucketId: bucketId + }); + + if (DeleteMarkers) deleteMarkers.push( + ...DeleteMarkers + .filter(object => !precisePath || utils.isAtPath(path, object.Key)) + .filter(object => !filterLatest || object.IsLatest === true) + ); + if (Versions) versions.push( + ...Versions + .filter(object => !precisePath || utils.isAtPath(path, object.Key)) + .filter(object => !filterLatest || object.IsLatest === true) + ); + incomplete = IsTruncated; + nextKeyMarker = NextKeyMarker; + } while (incomplete); + + return Promise.resolve({ DeleteMarkers: deleteMarkers, Versions: versions }); + }, + + /** + * @function listObjectsV2 + * Lists the objects in the bucket with the prefix of `filePath` + * @param {string} [options.filePath=undefined] Optional filePath of the objects + * @param {string} [options.continuationToken=undefined] Optional continuationtoken for pagination + * @param {number} [options.maxKeys=undefined] Optional maximum number of keys to return + * @param {string} [options.bucketId=undefined] Optional bucketId + * @returns {Promise} The response of the list objects v2 operation + */ + async listObjectsV2({ filePath = undefined, continuationToken = undefined, maxKeys = undefined, bucketId = undefined } = {}) { + const data = await utils.getBucket(bucketId); + const prefix = data.key !== DELIMITER ? data.key : ''; + const params = { + Bucket: data.bucket, + ContinuationToken: continuationToken, + MaxKeys: maxKeys, + Prefix: filePath ?? prefix // Must filter via "prefix" - https://stackoverflow.com/a/56569856 + }; + + return this._getS3Client(data).send(new ListObjectsV2Command(params)); + }, + + /** + * @function ListObjectVersion + * Lists the versions for the object at `filePath` + * @param {string} [options.filePath=undefined] Optional filePath of the objects + * @param {string} [options.keyMarker=undefined] Optional keyMarker for pagination + * @param {number} [options.maxKeys=undefined] Optional maximum number of keys to return + * @param {string} [options.bucketId=undefined] Optional bucketId + * @returns {Promise} The response of the list object version operation + */ + async listObjectVersion({ filePath = undefined, keyMarker = undefined, maxKeys = undefined, bucketId = undefined } = {}) { + const data = await utils.getBucket(bucketId); + const prefix = data.key !== DELIMITER ? data.key : ''; + const params = { + Bucket: data.bucket, + KeyMarker: keyMarker, + MaxKeys: maxKeys, + Prefix: filePath ?? prefix // Must filter via "prefix" - https://stackoverflow.com/a/56569856 + }; + + return this._getS3Client(data).send(new ListObjectVersionsCommand(params)); + }, + + /** + * @function presignUrl + * Generates a presigned url for the `command` with a limited expiration window + * @param {object} command The associated S3 command to generate a presigned URL for + * @param {number} [expiresIn=300] The number of seconds this signed url will be valid for. + * Defaults to expire after 5 minutes. + * @param {string} [bucketId] Optional bucketId + * @returns {Promise} A presigned url for the direct S3 REST `command` operation + */ + async presignUrl(command, expiresIn = defaultTempExpiresIn, bucketId = undefined) { + const data = await utils.getBucket(bucketId); + return getSignedUrl(this._getS3Client(data), command, { expiresIn }); + }, + + /** + * @function putBucketEncryption + * @param {string} [bucketId] Optional bucketId + * @returns {Promise} The response of the put bucket encryption operation + */ + async putBucketEncryption(bucketId = undefined) { + const data = await utils.getBucket(bucketId); + const params = { + Bucket: data.bucket, + ServerSideEncryptionConfiguration: { + Rules: [{ + ApplyServerSideEncryptionByDefault: { + SSEAlgorithm: 'AES256' + } + }] + } + }; + + return this._getS3Client(data).send(new PutBucketEncryptionCommand(params)); + }, + + /** + * @function putObject + * Puts the object `stream` at the `id` path + * @param {stream} options.stream The binary stream of the object + * @param {string} options.name The file name of the object + * @param {number} options.length The content length of the object + * @param {string} options.mimeType The mime type of the object + * @param {object} [options.metadata] Optional object containing key/value pairs for metadata + * @param {object} [options.tags] Optional object containing key/value pairs for tags + * @param {string} [options.bucketId] Optional bucketId + * @returns {Promise} The response of the put object operation + */ + async putObject({ stream, name, length, mimeType, metadata, tags, bucketId = undefined }) { + const data = await utils.getBucket(bucketId); + const params = { + Bucket: data.bucket, + Key: utils.joinPath(data.key, name), + Body: stream, + ContentLength: length, + ContentType: mimeType, + Metadata: metadata, + Tagging: Object.entries({ ...tags }).map(([key, value]) => `${key}=${encodeURIComponent(value)}`).join('&') + // TODO: Consider adding API param support for Server Side Encryption + // ServerSideEncryption: 'AES256' + }; + + return this._getS3Client(data).send(new PutObjectCommand(params)); + }, + + /** + * @function putObjectTagging + * Puts the tags of the object at `filePath` + * @param {string} options.filePath The filePath of the object + * @param {string} options.tags Array of key/value pairs (eg: `([{ Key: 'colour', Value: 'red' }]`) + * @param {number} [options.s3VersionId] Optional specific s3VersionId for the object + * @param {string} [options.bucketId] Optional bucketId + * @returns {Promise} The response of the put object tagging operation + */ + async putObjectTagging({ filePath, tags, s3VersionId = undefined, bucketId = undefined }) { + const data = await utils.getBucket(bucketId); + const params = { + Bucket: data.bucket, + Key: filePath, + Tagging: { + TagSet: tags + }, + VersionId: s3VersionId + }; + + return this._getS3Client(data).send(new PutObjectTaggingCommand(params)); + }, + + /** + * @function readObject + * Reads the object at `filePath` + * @param {string} options.filePath The filePath of the object + * @param {number} [options.s3VersionId] Optional specific s3VersionId for the object + * @param {string} [options.bucketId] Optional bucketId + * @returns {Promise} The response of the get object operation + */ + async readObject({ filePath, s3VersionId = undefined, bucketId = undefined }) { + const data = await utils.getBucket(bucketId); + const params = { + Bucket: data.bucket, + Key: filePath, + VersionId: s3VersionId + }; + + return this._getS3Client(data).send(new GetObjectCommand(params)); + }, + + /** + * @function readSignedUrl + * Yields a presigned url for the get object operation with a limited expiration window + * @param {string} options.filePath The filePath of the object + * @param {number} [options.expiresIn] The number of seconds this signed url will be valid for + * @param {number} [options.s3VersionId] Optional specific s3VersionId for the object + * @param {string} [options.bucketId] Optional bucketId + * @returns {Promise} A presigned url for the direct S3 REST `command` operation + */ + async readSignedUrl({ filePath, expiresIn, s3VersionId = undefined, bucketId = undefined }) { + const data = await utils.getBucket(bucketId); + const expires = expiresIn || defaultTempExpiresIn; + const params = { + Bucket: data.bucket, + Key: filePath, + VersionId: s3VersionId + }; + + return this.presignUrl(new GetObjectCommand(params), expires, bucketId); + }, + + /** + * @function upload + * Uploads the object `stream` at the `id` path + * @param {stream} options.stream The binary stream of the object + * @param {string} options.name The file name of the object + * @param {number} options.length The content length of the object + * @param {string} options.mimeType The mime type of the object + * @param {object} [options.metadata] Optional object containing key/value pairs for metadata + * @param {object} [options.tags] Optional object containing key/value pairs for tags + * @param {string} [options.bucketId] Optional bucketId + * @returns {Promise} The response of the put object operation + */ + async upload({ stream, name, length, mimeType, metadata, tags, bucketId = undefined }) { + const data = await utils.getBucket(bucketId); + + const upload = new Upload({ + client: this._getS3Client(data), + params: { + Bucket: data.bucket, + Key: utils.joinPath(data.key, name), + Body: stream, + ContentType: mimeType, + Metadata: metadata, + Tagging: Object.entries({ ...tags }).map(([key, value]) => `${key}=${encodeURIComponent(value)}`).join('&') + // TODO: Consider adding API param support for Server Side Encryption + // ServerSideEncryption: 'AES256' + }, + partSize: utils.calculatePartSize(length) + }); + + upload.on('httpUploadProgress', progress => { + log.debug(progress, { function: 'onhttpUploadProgress' }); + }); + + return upload.done(); + } +}; + +module.exports = objectStorageService; diff --git a/comsapi/app/src/services/sync.js b/comsapi/app/src/services/sync.js new file mode 100644 index 00000000..a0402214 --- /dev/null +++ b/comsapi/app/src/services/sync.js @@ -0,0 +1,470 @@ +const { NIL: SYSTEM_USER, v4: uuidv4, validate: uuidValidate } = require('uuid'); + +const log = require('../components/log')(module.filename); +const utils = require('../db/models/utils'); + +const { ObjectModel, Version } = require('../db/models'); +const { getKeyValue, getUniqueObjects, toLowerKeys } = require('../components/utils'); + +const metadataService = require('./metadata'); +const objectService = require('./object'); +const storageService = require('./storage'); +const tagService = require('./tag'); +const versionService = require('./version'); + +/** + * The Sync Service + * Synchronizes data between S3 object storage and the COMS database + */ +const service = { + /** + * @function _deriveObjectId + * Checks an S3 Object for any previous `coms-id` tag traces and returns it if found. + * Otherwise it writes a new `coms-id` to the S3 Object if none were previously found. + * @param {object | boolean} s3Object The result of an S3 HeadObject operation + * @param {string} path String representing the canonical path for the specified object + * @param {string | null} bucketId uuid of bucket or `null` if syncing object in default bucket + * @returns {Promise} Resolves to an existing objectId or creates a new one + */ + _deriveObjectId: async (s3Object, path, bucketId) => { + let objId = uuidv4(); + + if (typeof s3Object === 'object') { // If regular S3 Object + const TagSet = await storageService.getObjectTagging({ filePath: path, bucketId: bucketId }).then(result => result.TagSet ?? []); + const s3ObjectComsId = TagSet.find(obj => (obj.Key === 'coms-id'))?.Value; + + if (s3ObjectComsId && uuidValidate(s3ObjectComsId)) { + objId = s3ObjectComsId; + } else { // Update S3 Object if there is still remaining space in the TagSet + if (TagSet.length < 10) { // putObjectTagging replaces S3 tags so new TagSet must contain existing values + await storageService.putObjectTagging({ + filePath: path, + bucketId: bucketId, + tags: TagSet.concat([{ Key: 'coms-id', Value: objId }]) + }); + } + } + } else if (typeof s3Object === 'boolean') { // If soft-deleted S3 Object + const { Versions } = await storageService.listAllObjectVersions({ filePath: path, bucketId: bucketId }); + + for (const versionId of Versions.map(version => version.VersionId)) { + const TagSet = await storageService.getObjectTagging({ + filePath: path, + s3VersionId: versionId, + bucketId: bucketId + }).then(result => result.TagSet ?? []); + const oldObjId = TagSet.find(obj => obj.Key === 'coms-id')?.Value; + + if (oldObjId && uuidValidate(oldObjId)) { + objId = oldObjId; + break; // Stop iterating if a valid uuid was found + } + } + } + + return Promise.resolve(objId); + }, + + /** + * @function syncJob + * Orchestrates the synchronization of all aspects of a specified object + * Wraps all child processes in one db transaction + * @param {string} path String representing the canonical path for the specified object + * @param {string | null} bucketId uuid of bucket or `null` if syncing object in default bucket + * @param {boolean} [full=false] Optional boolean indicating whether to execute full recursive run + * @param {string} [userId=SYSTEM_USER] Optional uuid attributing which user added the job + * @returns {Promise} Resolves to object uuid or undefined when sync is completed + * @throws If the synchronization job encountered an error + */ + syncJob: async (path, bucketId, full = false, userId = SYSTEM_USER) => { + try { + if (!path) throw new Error('Path must be defined'); + + return await utils.trxWrapper(async (trx) => { + // 1. Sync Object + const object = await service.syncObject(path, bucketId, userId, trx) + .then(obj => obj.object); + + // 2. Sync Object Versions + let versions = []; + if (object) { + versions = await service.syncVersions(object, userId, trx); + } + + // 3. Sync Version Metadata & Tags + for (const v of versions) { + const tasks = [ // Always Update Tags regardless of modification state + service.syncTags(v.version, path, bucketId, userId, trx) + ]; + // Only Update Metadata if version has modifications or full mode + if (v.modified || full) { + tasks.push(service.syncMetadata(v.version, path, bucketId, userId, trx)); + } + + await Promise.all(tasks); + } + + return Promise.resolve(object?.id); + }); + } + catch (err) { + log.error(err, { function: 'syncJob' }); + throw err; + } + }, + + /** + * @function syncObject + * Synchronizes Object level data + * @param {string} path The path of object in sync job + * @param {string | null} bucketId The uuid bucketId of object in sync job + * @param {string} [userId=SYSTEM_USER] The uuid of a user that created the sync job + * @param {object} [etrx=undefined] An optional Objection Transaction object + * @returns {Promise} An object containing COMS object if applicable, and modified boolean on whether it was + * modified or not + */ + syncObject: async (path, bucketId, userId = SYSTEM_USER, etrx = undefined) => { + let trx; + try { + trx = etrx ? etrx : await ObjectModel.startTransaction(); + + let modified = false; + let response; + + // Check for COMS and S3 Object statuses + const [comsObject, s3Object] = await Promise.allSettled([ + // COMS Object + objectService.searchObjects({ path: path, bucketId: bucketId }, trx), + // S3 Object + storageService.headObject({ filePath: path, bucketId: bucketId }) + .catch((e) => { // return boolean true if object is soft-deleted in S3 + return !!e.$response.headers['x-amz-delete-marker']; + }) + ]).then(settled => settled.map(promise => Array.isArray(promise.value) ? promise.value[0] : promise.value)); + + // Case: already synced - record object only + if (comsObject && s3Object) response = comsObject; + + // Case: not in COMS - insert new COMS object + else if (!comsObject && s3Object) { + const objId = await service._deriveObjectId(s3Object, path, bucketId); + + response = await objectService.create({ + id: objId, + name: path.match(/(?!.*\/)(.*)$/)[0], // get `name` column + path: path, + bucketId: bucketId, + userId: userId + }, trx); + + modified = true; + } + + // Case: missing in S3 - drop COMS object + else if (comsObject && !s3Object) { + // Delete COMS Object and cascade all child records from COMS + await objectService.delete(comsObject.id, trx); + + // TODO: Relatively slow operations - determine if this can be optimized + // Prune metadata and tag records + await metadataService.pruneOrphanedMetadata(trx); + await tagService.pruneOrphanedTags(trx); + } + + if (!etrx) await trx.commit(); + return Promise.resolve({ modified: modified, object: response }); + } catch (err) { + if (!etrx && trx) await trx.rollback(); + throw err; + } + }, + + /** + * @function syncVersions + * Synchronizes Version level data + * @param {object | string} object The parent object or object uuid + * @param {string} [userId=SYSTEM_USER] The uuid of a user that created the sync job + * @param {object} [etrx=undefined] An optional Objection Transaction object + * @returns {Promise>} An array of tuples with the COMS versions if applicable, + * and boolean on whether it was modified or not + */ + syncVersions: async (object, userId = SYSTEM_USER, etrx = undefined) => { + let trx; + try { + trx = etrx ? etrx : await Version.startTransaction(); + + // Fetch COMS Object record if necessary + const comsObject = typeof object === 'object' ? object : await objectService.read(object, trx); + + // Check for COMS and S3 Version statuses + const [comsVersions, s3VersionsRaw] = await Promise.allSettled([ + versionService.list(comsObject.id, trx), + storageService.listAllObjectVersions({ filePath: comsObject.path, bucketId: comsObject.bucketId }) + ]).then(settled => settled.map(promise => promise.value)); + + // Combine S3 DeleteMarkers and Versions into one array + const s3Versions = s3VersionsRaw.DeleteMarkers + .map(dm => ({ DeleteMarker: true, ...dm })) + .concat(s3VersionsRaw.Versions); + + // delete versions from COMS that are not in S3 + // get list of unique coms versions + const uniqueCVIds = getUniqueObjects(comsVersions, 's3VersionId').map(v => v.id); + + // get COMS versions that are not in S3 (matching on s3VersionId) OR not + // in list of unique COMS versions (matching on id) + const cVsToDelete = comsVersions.filter(cv => { + const notInS3 = !s3Versions.some(s3v => (s3v.VersionId === String(cv.s3VersionId))); + const isDuplicate = !uniqueCVIds.includes(cv.id); + return notInS3 || isDuplicate; + }); + + if(cVsToDelete.length){ + await Version.query(trx) + .delete() + .where('objectId', comsObject.id) + .whereNotNull('s3VersionId') + .whereIn('id', cVsToDelete.map(cv => cv.id)); + } + // delete versions from comsVersions array for further comparisons + const comsVersionsToKeep = comsVersions.filter(cv => !cVsToDelete.some(v => cv.id === v.id)); + + // Add and Update versions in COMS + const response = await Promise.all(s3Versions.map(async s3Version => { + // S3 Object is in non-versioned bucket + if (s3Version.VersionId === 'null') { + const mimeType = await storageService.headObject({ + filePath: comsObject.path, + bucketId: comsObject.bucketId + }).then(obj => obj.ContentType); + + // Get existing version + const existingVersion = comsVersions[0]; + + // No existing version found + if (!existingVersion) { + const newVersion = await versionService.create({ + s3VersionId: null, + mimeType: mimeType, + id: comsObject.id, + etag: s3Version.ETag, + isLatest: true + }, userId, trx); + return { modified: true, version: newVersion }; + } + + // Latest version has different values + else if (existingVersion.mimeType !== mimeType || existingVersion.etag !== s3Version.ETag) { + const updatedVersion = await versionService.update({ + mimeType: mimeType, + id: comsObject.id, + etag: s3Version.ETag, + isLatest: true + }, userId, trx); + return { modified: true, version: updatedVersion }; + } + + // Version record not modified + else return { version: existingVersion }; + } + // S3 Object is in versioned bucket (ie: if VersionId is not 'null') + else { + const comsVersion = comsVersionsToKeep.find(cv => cv.s3VersionId === s3Version.VersionId); + + if (comsVersion) { // Version is in COMS + if (s3Version.IsLatest) { // Patch isLatest flags if changed + const updated = await versionService.updateIsLatest(comsObject.id, trx); + return { modified: true, version: updated }; + } else { // Version record not modified + return { version: comsVersion }; + } + } else { // Version is not in COMS + const mimeType = s3Version.DeleteMarker + ? undefined // Will default to 'application/octet-stream' + : await storageService.headObject({ + filePath: comsObject.path, + s3VersionId: s3Version.VersionId, + bucketId: comsObject.bucketId + }).then(obj => obj.ContentType); + + const newVersion = await versionService.create({ + s3VersionId: s3Version.VersionId, + mimeType: mimeType, + id: comsObject.id, + deleteMarker: s3Version.DeleteMarker, + etag: s3Version.ETag, + isLatest: s3Version.IsLatest + }, userId, trx); + // add to response with `newVersion` attribute, required for sync tags/meta logic + return { modified: true, version: newVersion }; + } + } + })); + + if (!etrx) await trx.commit(); + return Promise.resolve(response); + } catch (err) { + if (!etrx && trx) await trx.rollback(); + throw err; + } + }, + + /** + * @function syncTags + * Synchronizes Tag level data for a specific object version + * @param {object | string} version The parent version or version uuid + * @param {string} path String representing the canonical path for the specified object + * @param {string} [bucketId=undefined] Optional COMS uuid of bucket + * @param {string} [userId=SYSTEM_USER] The uuid of a user that created the sync job + * @param {object} [etrx=undefined] An optional Objection Transaction object + * returns array of synced versions + * @returns {Promise>} An array of synced tags that exist in both COMS and S3 + */ + syncTags: async (version, path, bucketId = undefined, userId = SYSTEM_USER, etrx = undefined) => { + let trx; + try { + trx = etrx ? etrx : await Version.startTransaction(); + let response = []; + + // Fetch COMS version record if necessary + const comsVersion = typeof version === 'object' ? version : await versionService.get({ versionId: version }, trx); + + // Short circuit if version is a delete marker + if (comsVersion.deleteMarker) return response; + + // Check for COMS and S3 Tag statuses + const [comsTagsForVersion, s3TagsForVersion] = await Promise.allSettled([ + tagService.fetchTagsForVersion({ versionIds: comsVersion.id }, trx), + storageService.getObjectTagging({ filePath: path, s3VersionId: comsVersion.s3VersionId, bucketId: bucketId }) + ]).then(settled => settled.map(promise => promise.value)); + + // COMS Tags + const comsTags = comsTagsForVersion[0]?.tagset ?? []; + // S3 Tags + const s3Tags = toLowerKeys(s3TagsForVersion?.TagSet ?? []); + /** + * Add coms-id tag to latest version in S3 if not already present + * NOTE: For a sync job the _deriveObjectId() function will have already added + * the coms-id to latest version. + * TODO: check if this version is still also the latest on corresponding version in S3 + */ + if (comsVersion.isLatest && s3Tags.length < 10 && !s3Tags.find(s3T => s3T.key === 'coms-id')) { + /** + * NOTE: adding tags to a specified version (passing a `VersionId` parameter) will affect `Last Modified` + * attribute of multiple versions on some s3 storage providors including Dell ECS + */ + await storageService.putObjectTagging({ + filePath: path, + tags: (s3TagsForVersion?.TagSet ?? []).concat([{ + Key: 'coms-id', + Value: comsVersion.objectId + }]), + bucketId: bucketId, + // s3VersionId: comsVersion.s3VersionId, + }); + // add to our arrays for comaprison + s3Tags.push({ key: 'coms-id', value: comsVersion.objectId }); + } + + // Dissociate Tags not in S3 + const oldTags = []; + for (const comsT of comsTags) { + if (!s3Tags.some(s3T => s3T.key === comsT.key && s3T.value === comsT.value)) { + oldTags.push(comsT); + } + } + if (oldTags.length > 0) { + await tagService.dissociateTags(comsVersion.id, oldTags, trx); + } + + // Associate new S3 Tags + const newTags = []; + for (const s3Tag of s3Tags) { + if (!comsTags.some(comsT => comsT.key === s3Tag.key && comsT.value === s3Tag.value)) { + newTags.push(s3Tag); + } else { + response.push(s3Tag); + } + } + if (newTags.length > 0) { + await tagService.associateTags(comsVersion.id, newTags, userId, trx); + response.push(...newTags); + } + + if (!etrx) await trx.commit(); + return Promise.resolve(response); + } catch (err) { + if (!etrx && trx) await trx.rollback(); + throw err; + } + }, + + /** + * @function syncMetadata + * Synchronizes Metadata level data for a specific object version + * @param {object | string} version The parent version or version uuid + * @param {string} path String representing the canonical path for the specified object + * @param {string} [bucketId=undefined] Optional COMS uuid of bucket + * @param {string} [userId=SYSTEM_USER] The uuid of a user that created the sync job + * @param {object} [etrx=undefined] An optional Objection Transaction object + * returns array of synced versions + * @returns {Promise>} An array of synced metadata that exist in both COMS and S3 + */ + syncMetadata: async (version, path, bucketId = undefined, userId = SYSTEM_USER, etrx = undefined) => { + let trx; + try { + trx = etrx ? etrx : await Version.startTransaction(); + let response = []; + + // Fetch COMS Object record if necessary + const comsVersion = typeof version === 'object' ? version : await versionService.get({ versionId: version }, trx); + + // Short circuit if version is a delete marker + if (comsVersion.deleteMarker) return response; + + // Check for COMS and S3 Metadata statuses + const [comsMetadataForVersion, s3ObjectHead] = await Promise.allSettled([ + metadataService.fetchMetadataForVersion({ versionIds: comsVersion.id }, trx), + storageService.headObject({ filePath: path, s3VersionId: comsVersion.s3VersionId, bucketId: bucketId }) + ]).then(settled => settled.map(promise => promise.value)); + + // COMS Metadata + const comsMetadata = comsMetadataForVersion[0]?.metadata ?? []; + // S3 Metadata + const s3Metadata = getKeyValue(s3ObjectHead?.Metadata ?? {}); + + // Dissociate Metadata not in S3 + const oldMetadata = []; + for (const comsM of comsMetadata) { + if (!s3Metadata.some(s3M => s3M.key === comsM.key && s3M.value === comsM.value)) { + oldMetadata.push(comsM); + } + } + if (oldMetadata.length > 0) { + await metadataService.dissociateMetadata(version.id, oldMetadata, trx); + } + + // Associate new S3 Metadata + const newMetadata = []; + for (const s3M of s3Metadata) { + if (!comsMetadata.some(comsM => comsM.key === s3M.key && comsM.value === s3M.value)) { + newMetadata.push(s3M); + } else { + response.push(s3M); + } + } + if (newMetadata.length > 0) { + await metadataService.associateMetadata(version.id, newMetadata, userId, trx); + response.push(...newMetadata); + } + + if (!etrx) await trx.commit(); + return Promise.resolve(response); + } catch (err) { + if (!etrx && trx) await trx.rollback(); + throw err; + } + } +}; + +module.exports = service; diff --git a/comsapi/app/src/services/tag.js b/comsapi/app/src/services/tag.js new file mode 100644 index 00000000..77e58fec --- /dev/null +++ b/comsapi/app/src/services/tag.js @@ -0,0 +1,346 @@ +const { NIL: SYSTEM_USER } = require('uuid'); +const { ObjectModel, Tag, VersionTag, Version } = require('../db/models'); +const { getObjectsByKeyValue } = require('../components/utils'); + +/** + * The Tag DB Service + */ +const service = { + + /** + * @function replaceTags + * Makes the incoming list of tags the definitive set associated with versionId + * @param {string} versionId The uuid id column from version table + * @param {object[]} tags Incoming array of tageset objects to add for this version (eg: [{ key: 'a', value: '1'}, {key: 'B', value: '2'}]) + * @param {string} [currentUserId=SYSTEM_USER] The optional userId uuid actor; defaults to system user if unspecified + * @param {object} [etrx=undefined] An optional Objection Transaction object + * @returns {Promise} The result of running the insert operation + * @throws The error encountered upon db transaction failure + */ + replaceTags: async (versionId, tags, currentUserId = SYSTEM_USER, etrx = undefined) => { + let trx; + try { + trx = etrx ? etrx : await Tag.startTransaction(); + let response = []; + if (tags && tags.length) { + + // get all currently associated tags (before update) + const current = await Tag.query(trx) + .joinRelated('versionTag') + .where('versionId', versionId); + + // dissociate tags that are no longer associated + const dissociateTags = current + .filter(({ key, value }) => !getObjectsByKeyValue(tags, key, value)); + if (dissociateTags.length) await service.dissociateTags(versionId, dissociateTags, trx); + + // associate tags + response = await service.associateTags(versionId, tags, currentUserId, trx); + } + + if (!etrx) await trx.commit(); + return Promise.resolve(response); + } catch (err) { + if (!etrx && trx) await trx.rollback(); + throw err; + } + }, + + /** + * @function associateTags + * calls createTags to create new Tag records + * associates new tags to the versionId + * @param {string} versionId The uuid id column from version table + * @param {object[]} tags array of tags (eg: [{ key: 'a', value: '1'}, {key: 'B', value: '2'}]) + * @param {string} [currentUserId=SYSTEM_USER] The optional userId uuid actor; defaults to system user if unspecified + * @param {object} [etrx=undefined] An optional Objection Transaction object + * @returns {Promise} array of all associated tags + * @throws The error encountered upon db transaction failure + */ + associateTags: async (versionId, tags, currentUserId = SYSTEM_USER, etrx = undefined) => { + let trx; + try { + trx = etrx ? etrx : await Tag.startTransaction(); + let response = []; + + if (tags && tags.length) { + + // create any new tags, gets id's of all input tags + const dbTags = await service.createTags(tags, trx); + // get all currently associated tags + const associatedTags = await VersionTag.query(trx) + .modify('filterVersionId', versionId); + + // associate remaining tags + const newJoins = dbTags.filter(({ id }) => { + return !associatedTags.some(({ tagId }) => tagId === id); + }); + + if (newJoins.length) { + await VersionTag.query(trx) + .insert(newJoins.map(({ id }) => ({ + versionId: versionId, + tagId: id, + createdBy: currentUserId + }))); + } + response = dbTags; + } + + if (!etrx) await trx.commit(); + return Promise.resolve(response); + } catch (err) { + if (!etrx && trx) await trx.rollback(); + throw err; + } + }, + + /** + * @function dissociateTags + * dissociates all provided tags from a versionId + * @param {string} versionId The uuid id column from version table + * @param {object[]} [tags=undefined] array of tags (eg: [{ key: 'a', value: '1'}, {key: 'B', value: ''}]) + * @param {object} [etrx=undefined] An optional Objection Transaction object + * @returns {Promise} The result of running the delete operation (number of rows deleted) + * @throws The error encountered upon db transaction failure + */ + dissociateTags: async (versionId, tags = undefined, etrx = undefined) => { + let trx; + try { + trx = etrx ? etrx : await Tag.startTransaction(); + let response = 0; + + await tags.forEach(async tag => { + + // match on key + const params = { 'tag.key': tag.key }; + // if tag has a value match key and value + if (tag.value && tag.value !== '') params['tag.value'] = tag.value; + + let count = 0; + count = await VersionTag.query(trx) + .allowGraph('tag') + .withGraphJoined('tag') + .where(params) + .modify('filterVersionId', versionId) + .delete(); + + if (count) response += count; + }); + + // delete all orphaned tags + await service.pruneOrphanedTags(trx); + + if (!etrx) await trx.commit(); + return Promise.resolve(response); + } catch (err) { + if (!etrx && trx) await trx.rollback(); + throw err; + } + }, + + /** + * @function deleteOrphanedTags + * deletes Tag records if they are no longer related to any versions + * @param {object} [etrx=undefined] An optional Objection Transaction object + * @returns {Promise} The result of running the delete operation (number of rows deleted) + * @throws The error encountered upon db transaction failure + */ + pruneOrphanedTags: async (etrx = undefined) => { + let trx; + try { + trx = etrx ? etrx : await Tag.startTransaction(); + + const deletedTagIds = await Tag.query(trx) + .allowGraph('versionTag') + .withGraphJoined('versionTag') + .select('tag.id') + .whereNull('versionTag.tagId'); + + const response = await Tag.query(trx) + .delete() + .whereIn('id', deletedTagIds.map(({ id }) => id)); + + if (!etrx) await trx.commit(); + return Promise.resolve(response); + } catch (err) { + if (!etrx && trx) await trx.rollback(); + throw err; + } + }, + + /** + * @function createTags + * Inserts any tag records if they dont already exist in db + * @param {object} tags Incoming object with `:` tags to add for this version + * @param {object} [etrx=undefined] An optional Objection Transaction object + * @returns {Promise} an array of all input tags + * @throws The error encountered upon db transaction failure + */ + createTags: async (tags, etrx = undefined) => { + let trx; + let response = []; + try { + trx = etrx ? etrx : await Tag.startTransaction(); + + // get all tags already in db + const allTags = await Tag.query(trx).select(); + const existingTags = []; + const newTags = []; + + tags.forEach(({ key, value }) => { + // if tag is already in db + if (getObjectsByKeyValue(allTags, key, value)) { + existingTags.push({ id: getObjectsByKeyValue(allTags, key, value).id, key: key, value: value }); + } + // else add to array for inserting + else { + newTags.push({ key: key, value: value }); + } + }); + + // insert new tags + if (newTags.length) { + const newTagset = await Tag.query(trx) + .insert(newTags) + .returning('*'); + // merge newTags with existing tags + response = existingTags.concat(newTagset); + } + else { + response = existingTags; + } + + if (!etrx) await trx.commit(); + return Promise.resolve(response); + } catch (err) { + if (!etrx && trx) await trx.rollback(); + throw err; + } + }, + + /** + * @function fetchTagsForObject + * Fetch matching tags on latest version of provided objects, optionally scoped to a user's object/bucket READ permission + * @param {string[]} [params.bucketIds] An array of uuids representing buckets + * @param {string[]} [params.objectIds] An array of uuids representing objects + * @param {object} [params.tagset] Optional object of tags key/value pairs + * @param {string} [params.userId] Optional uuid representing a user + * @returns {Promise} The result of running the database select + */ + fetchTagsForObject: (params) => { + return ObjectModel.query() + .select('object.id AS objectId', 'object.bucketId as bucketId') + .allowGraph('version.tag') + .withGraphJoined('version.tag') + // get latest version that isn't a delete marker by default + .modifyGraph('version', builder => { + builder + .select('version.id', 'version.objectId') + .orderBy([ + 'version.objectId', + { column: 'version.createdAt', order: 'desc' } + ]) + .where('deleteMarker', false) + .distinctOn('version.objectId'); + }) + // match on tagset parameter + .modifyGraph('version.tag', builder => { + builder + .select('key', 'value') + .modify('filterKeyValue', { tag: params.tagset }); + }) + // match on objectIds parameter + .modify('filterIds', params.objectIds) + // match on bucketIds parameter + .modify('filterBucketIds', params.bucketIds) + // scope to objects that user(s) has READ permission at object or bucket-level + .modify('hasPermission', params.userId, 'READ') + // re-structure result like: [{ objectId: abc, tagset: [{ key: a, value: b }] }] + .then(result => result.map(row => { + return { + objectId: row.objectId, + tagset: row.version[0].tag + }; + })); + }, + + /** + * @function fetchTagsForVersion + * Fetch tags for specific versions, optionally scoped to a user's object/bucket READ permission + * @param {string[]} [params.s3VersionIds] An array of S3 version identifiers + * @param {string[]} [params.versionIds] An array of uuids representing versions + * @param {object} [params.tags] Optional object of tags key/value pairs + * @param {string} [params.userId] Optional uuid representing a user + * @param {object} [etrx=undefined] An optional Objection Transaction object + * @returns {Promise} The result of running the database select + */ + fetchTagsForVersion: async (params, etrx = undefined) => { + let trx; + try { + trx = etrx ? etrx : await Tag.startTransaction(); + + const response = await Version.query(trx) + .select('version.id as versionId', 'version.s3VersionId') + .allowGraph('tag as tagset') + .withGraphJoined('tag as tagset') + .modifyGraph('tagset', builder => { + builder + .select('key', 'value') + .modify('filterKeyValue', { tag: params.tags }); + }) + .modify((query) => { + if (params.s3VersionIds) query.modify('filterS3VersionId', params.s3VersionIds); + else query.modify('filterId', params.versionIds); + }) + // filter by objects that user(s) has READ permission at object or bucket-level + .modify((query) => { + if (params.userId) { + query + .allowGraph('object') + .withGraphJoined('object') + .modifyGraph('object', query => { query.modify('hasPermission', params.userId, 'READ'); }) + .whereNotNull('object.id'); + } + }) + .orderBy('version.createdAt', 'desc') + .then(result => result.map(row => { + // eslint-disable-next-line no-unused-vars + const { object, ...data } = row; + return data; + })); + + if (!etrx) await trx.commit(); + return Promise.resolve(response); + } catch (err) { + if (!etrx && trx) await trx.rollback(); + throw err; + } + }, + + /** + * @function searchTags + * Search and filter for specific tag keys + * @param {object} [params.tag] Optional object of tag keys to filter on + * @param {string} [params.privacyMask] Optional, if results should ignore and hide value from result + * @returns {Promise} The result of running the find operation + */ + searchTags: (params) => { + return Tag.query() + .modify((query) => { + if (params.privacyMask) { + query + .select('key') + .modify('filterKey', { tag: params.tag }); + } + else { + query + .select('key', 'value') + .modify('filterKeyValue', { tag: params.tag }); + } + }); + }, + +}; + +module.exports = service; diff --git a/comsapi/app/src/services/user.js b/comsapi/app/src/services/user.js new file mode 100644 index 00000000..ad2be88b --- /dev/null +++ b/comsapi/app/src/services/user.js @@ -0,0 +1,281 @@ +const { v4: uuidv4, NIL: SYSTEM_USER } = require('uuid'); + +const { parseIdentityKeyClaims } = require('../components/utils'); + +const { IdentityProvider, User } = require('../db/models'); +const utils = require('../db/models/utils'); + +/** + * The User DB Service + */ +const service = { + /** + * @function _tokenToUser + * Transforms JWT payload contents into a User Model object + * @param {object} token The decoded JWT payload + * @returns {object} An equivalent User model object + */ + _tokenToUser: (token) => { + const identityId = parseIdentityKeyClaims() + .map(idKey => token[idKey]) + .filter(claims => claims) // Drop falsy values from array + .concat(undefined)[0]; // Set undefined as last element of array + + return { + identityId: identityId, + username: token.identity_provider_identity ? token.identity_provider_identity : token.preferred_username, + firstName: token.given_name, + fullName: token.name, + lastName: token.family_name, + email: token.email, + idp: token.identity_provider + }; + }, + + /** + * @function createIdp + * Create an identity provider record + * @param {string} idp The identity provider code + * @param {object} [etrx=undefined] An optional Objection Transaction object + * @returns {Promise} The result of running the insert operation + * @throws The error encountered upon db transaction failure + */ + createIdp: async (idp, etrx = undefined) => { + let trx; + try { + trx = etrx ? etrx : await IdentityProvider.startTransaction(); + + const obj = { + idp: idp, + createdBy: SYSTEM_USER + }; + + const response = await IdentityProvider.query(trx).insertAndFetch(obj); + if (!etrx) await trx.commit(); + return response; + } catch (err) { + if (!etrx && trx) await trx.rollback(); + throw err; + } + }, + + /** + * @function createUser + * Create a user DB record + * @param {object} data Incoming user data + * @param {object} [etrx=undefined] An optional Objection Transaction object + * @returns {Promise} The result of running the insert operation + * @throws The error encountered upon db transaction failure + */ + createUser: async (data, etrx = undefined) => { + let trx; + try { + let response; + trx = etrx ? etrx : await User.startTransaction(); + + const exists = await User.query(trx) + .where({ 'identityId': data.identityId, idp: data.idp }) + .first(); + + if (exists) { + response = exists; + } else { // else add new user + if (data.idp) { // add idp if not in db + const identityProvider = await service.readIdp(data.idp, trx); + if (!identityProvider) await service.createIdp(data.idp, trx); + } + + response = await User.query(trx) + .insert({ + userId: uuidv4(), + identityId: data.identityId, + username: data.username, + fullName: data.fullName, + email: data.email, + firstName: data.firstName, + lastName: data.lastName, + idp: data.idp, + createdBy: data.userId + }) + .returning('*'); + } + + if (!etrx) await trx.commit(); + return response; + } catch (err) { + if (!etrx && trx) await trx.rollback(); + throw err; + } + }, + + /** + * @function getCurrentUserId + * Gets userId (primary identifier of a user in COMS db) of currentUser. + * if request is basic auth returns `defaultValue` + * @param {object} identityId the identity field of the current user + * @param {string} [defaultValue=undefined] An optional default return value + * @returns {string} The current userId if applicable, or `defaultValue` + */ + getCurrentUserId: async (identityId, defaultValue = undefined) => { + // TODO: Consider conditionally skipping when identityId is undefined? + const user = await User.query() + .select('userId') + .where('identityId', identityId) + .first(); + + return user && user.userId ? user.userId : defaultValue; + }, + + /** + * @function listIdps + * Lists all known identity providers + * @param {boolean} [params.active] Optional boolean on user active status + * @returns {Promise} The result of running the find operation + */ + listIdps: (params) => { + return IdentityProvider.query() + .modify('filterActive', params.active) + .modify('orderDefault'); + }, + + /** + * @function login + * Parse the user token and update the user table if necessary + * @param {object} token The decoded JWT token payload + * @returns {Promise} The result of running the login operation + */ + login: async (token) => { + const newUser = service._tokenToUser(token); + // wrap with db transaction + return await utils.trxWrapper(async (trx) => { + // check if user exists in db + const oldUser = await User.query(trx) + .where({ 'identityId': newUser.identityId, idp: newUser.idp ? newUser.idp : null }) + .first(); + + if (!oldUser) { + // Add user to system + return await service.createUser(newUser, trx); + } else { + // Update user data if necessary + return await service.updateUser(oldUser.userId, newUser, trx); + } + }); + }, + + /** + * @function readIdp + * Gets an identity provider record + * @param {string} code The identity provider code + * @returns {Promise} The result of running the find operation + * @throws The error encountered upon db transaction failure + */ + readIdp: async (code, etrx = undefined) => { + let trx; + try { + trx = etrx ? etrx : await IdentityProvider.startTransaction(); + + const response = await IdentityProvider.query(trx).findById(code); + + if (!etrx) await trx.commit(); + return response; + } catch (err) { + if (!etrx && trx) await trx.rollback(); + throw err; + } + }, + + /** + * @function readUser + * Gets a user record + * @param {string} userId The userId uuid + * @returns {Promise} The result of running the find operation + * @throws If no record is found + */ + readUser: (userId) => { + return User.query() + .findById(userId) + .throwIfNotFound(); + }, + + /** + * @function searchUsers + * Search and filter for specific users + * @param {string|string[]} [params.userId] Optional string or array of uuids representing the user subject + * @param {string|string[]} [params.identityId] Optional string or array of uuids representing the user identity + * @param {string|string[]} [params.idp] Optional string or array of identity providers + * @param {string} [params.username] Optional username string to match on + * @param {string} [params.email] Optional email string to match on + * @param {string} [params.firstName] Optional firstName string to match on + * @param {string} [params.fullName] Optional fullName string to match on + * @param {string} [params.lastName] Optional lastName string to match on + * @param {boolean} [params.active] Optional boolean on user active status + * @param {string} [params.search] Optional search string to match on in username, email and fullName + * @returns {Promise} The result of running the find operation + */ + searchUsers: (params) => { + return User.query() + .modify('filterUserId', params.userId) + .modify('filterIdentityId', params.identityId) + .modify('filterIdp', params.idp) + .modify('filterUsername', params.username) + .modify('filterEmail', params.email) + .modify('filterFirstName', params.firstName) + .modify('filterFullName', params.fullName) + .modify('filterLastName', params.lastName) + .modify('filterActive', params.active) + .modify('filterSearch', params.search) + .whereNotNull('identityId') + .modify('orderLastFirstAscending'); + }, + + /** + * @function updateUser + * Updates a user record only if there are changed values + * @param {string} userId The userId uuid + * @param {object} data Incoming user data + * @param {object} [etrx=undefined] An optional Objection Transaction object + * @returns {Promise} The result of running the patch operation + * @throws The error encountered upon db transaction failure + */ + updateUser: async (userId, data, etrx = undefined) => { + let trx; + try { + // Check if any user values have changed + const oldUser = await service.readUser(userId); + const diff = Object.entries(data).some(([key, value]) => oldUser[key] !== value); + + if (diff) { // Patch existing user + trx = etrx ? etrx : await User.startTransaction(); + + if (data.idp) { + const identityProvider = await service.readIdp(data.idp, trx); + if (!identityProvider) await service.createIdp(data.idp, trx); + } + + const obj = { + identityId: data.identityId, + username: data.username, + fullName: data.fullName, + email: data.email, + firstName: data.firstName, + lastName: data.lastName, + idp: data.idp, + updatedBy: data.userId + }; + + // TODO: add support for updating userId primary key in the event it changes + const response = await User.query(trx).patchAndFetchById(userId, obj); + if (!etrx) await trx.commit(); + return response; + } else { // Nothing to update + return oldUser; + } + } catch (err) { + if (!etrx && trx) await trx.rollback(); + throw err; + } + } +}; + +module.exports = service; diff --git a/comsapi/app/src/services/version.js b/comsapi/app/src/services/version.js new file mode 100644 index 00000000..f0563433 --- /dev/null +++ b/comsapi/app/src/services/version.js @@ -0,0 +1,327 @@ +const { v4: uuidv4, NIL: SYSTEM_USER } = require('uuid'); +const { Version } = require('../db/models'); + +const objectService = require('./object'); +const storageService = require('./storage'); + +/** + * The Version DB Service + */ +const service = { + /** + * @function copy + * Creates a new Version DB record from an existing record + * @param {string} sourceVersionId S3 VersionId of source version + * @param {string} targetVersionId S3 VersionId of new version + * @param {string} objectId uuid of the object + * @param {string} targetEtag ETag of the new version + * @param {string} userId uuid of the current user + * @param {object} [etrx=undefined] An optional Objection Transaction object + * @returns {Promise} The Version created in database + * @throws The error encountered upon db transaction failure + */ + copy: async (sourceVersionId, targetVersionId, objectId, targetEtag, userId = SYSTEM_USER, etrx = undefined) => { + let trx; + try { + trx = etrx ? etrx : await Version.startTransaction(); + + // if sourceVersionId is undefined, copy latest version + const sourceVersion = sourceVersionId ? + await Version.query(trx) + .where({ + s3VersionId: sourceVersionId, + objectId: objectId + }) + .first() : + await Version.query(trx) + .where({ + objectId: objectId + }) + // TODO: use isLatest where possible + .orderBy([ + { column: 'createdAt', order: 'desc' }, + { column: 'updatedAt', order: 'desc', nulls: 'last' } + ]) + .first(); + + const response = await Version.query(trx) + .insert({ + id: uuidv4(), + s3VersionId: targetVersionId, + etag: targetEtag, + objectId: objectId, + mimeType: sourceVersion.mimeType, + deleteMarker: sourceVersion.deleteMarker, + isLatest: true, + createdBy: userId + }) + .returning('*'); + + // set all other versions to islatest: false + await service.removeDuplicateLatest(response.id, objectId, trx); + + if (!etrx) await trx.commit(); + return Promise.resolve(response); + } catch (err) { + if (!etrx && trx) await trx.rollback(); + throw err; + } + }, + + /** + * @function create + * Saves a version of an object + * @param {object[]} data an object with an `objectId` and version data + * @param {string} userId uuid of the current user + * @param {object} [etrx=undefined] An optional Objection Transaction object + * @returns {Promise} the Version object inserted into the database + * @throws The error encountered upon db transaction failure + */ + create: async (data = {}, userId = SYSTEM_USER, etrx = undefined) => { + let trx; + try { + trx = etrx ? etrx : await Version.startTransaction(); + const response = await Version.query(trx) + .insert({ + id: uuidv4(), + s3VersionId: data.s3VersionId, + mimeType: data.mimeType, + objectId: data.id, + createdBy: userId, + deleteMarker: data.deleteMarker, + etag: data.etag, + isLatest: data.isLatest + }) + .returning('*'); + + // if new version is latest, set all other versions to islatest: false + if (data.isLatest) await service.removeDuplicateLatest(response.id, data.id, trx); + + if (!etrx) await trx.commit(); + return Promise.resolve(response); + } catch (err) { + if (!etrx && trx) await trx.rollback(); + throw err; + } + }, + + /** + * @function delete + * Delete a version record of an object + * @param {string} objId The object uuid + * @param {string} s3VersionId The version ID or null if deleting an object + * @param {string} [userId=undefined] An optional uuid of a user + * @param {object} [etrx=undefined] An optional Objection Transaction object + * @returns {Promise} The number of remaining versions in db after the delete + * @throws The error encountered upon db transaction failure + */ + delete: async (objId, s3VersionId, etrx = undefined) => { + let trx; + try { + trx = etrx ? etrx : await Version.startTransaction(); + const response = await Version.query(trx) + .delete() + .where('objectId', objId) + .where('s3VersionId', s3VersionId) + // Returns array of deleted rows instead of count + // https://vincit.github.io/objection.js/recipes/returning-tricks.html + .returning('*') + .throwIfNotFound(); + + await service.updateIsLatest(objId, trx); + + if (!etrx) await trx.commit(); + return Promise.resolve(response); + } catch (err) { + if (!etrx && trx) await trx.rollback(); + throw err; + } + }, + + /** + * @function get + * Get a given version from the database. + * if s3VersionId and versionId are null or undefined, get latest version (excluding delete-makers) + * @param {object} options object containing s3VersionId, versionId, objectId + * @param {object} [etrx=undefined] An optional Objection Transaction object + * @returns {Promise} the Version object from the database + * @throws The error encountered upon db transaction failure + */ + get: async ({ s3VersionId, versionId, objectId }, etrx = undefined) => { + let trx; + try { + trx = etrx ? etrx : await Version.startTransaction(); + + let response = undefined; + if (s3VersionId) { + response = await Version.query(trx) + .where({ s3VersionId: s3VersionId, objectId: objectId }) + .first(); + } + else if (versionId) { + response = await Version.query(trx) + .where({ id: versionId, objectId: objectId }) + .first(); + } + else { + response = await Version.query(trx) + .where('objectId', objectId) + .andWhere('deleteMarker', false) + // TODO: use isLatest where possible + .orderBy('createdAt', 'desc') + .first(); + } + if (!etrx) await trx.commit(); + return Promise.resolve(response); + } catch (err) { + if (!etrx && trx) await trx.rollback(); + throw err; + } + }, + + /** + * @function list + * list versions of an object. + * @param {string} uuid of an object + * @param {object} [etrx=undefined] An optional Objection Transaction object + * @returns {Promise>} Array of rows returned from the database + * @throws The error encountered upon db transaction failure + */ + list: async (objId, etrx = undefined) => { + let trx; + try { + trx = etrx ? etrx : await Version.startTransaction(); + + const response = await Version.query(trx) + .modify('filterObjectId', objId) + .orderBy('createdAt', 'DESC'); + + if (!etrx) await trx.commit(); + return Promise.resolve(response); + } catch (err) { + if (!etrx && trx) await trx.rollback(); + throw err; + } + }, + + /** + * @function removeDuplicateLatest + * Ensures only the specified `versionId` has isLatest: true + * @param {string} versionId COMS version uuid + * @param {string} objectId COMS object uuid + * @param {object} [etrx=undefined] An optional Objection Transaction object + * @returns {Promise>} Array of versions that were updated + */ + removeDuplicateLatest: async(versionId, objectId, etrx = undefined) => { + let trx; + try { + trx = etrx ? etrx : await Version.startTransaction(); + + const allVersions = await Version.query(trx) + .where('objectId', objectId); + + let updated = []; + if (allVersions.reduce((acc, curr) => curr.isLatest ? acc + 1 : acc, 0) > 1) { + // set all other versions to islatest: false + updated = await Version.query(trx) + .update({ isLatest: false }) + .whereNot({ 'id': versionId }) + .andWhere('objectId', objectId) + .andWhere({ isLatest: true }); + } + + if (!etrx) await trx.commit(); + return Promise.resolve(updated); + } catch (err) { + if (!etrx && trx) await trx.rollback(); + throw err; + } + }, + + /** + * @function update + * Updates a version of an object. + * Typically happens when updating the 'null-version' created for an object + * on a bucket without versioning. + * @param {object[]} data array of version attributes + * @param {string} userId uuid of the current user + * @param {object} [etrx=undefined] An optional Objection Transaction object + * @returns {Promise} id of version updated in the database + * @throws The error encountered upon db transaction failure + */ + update: async (data = {}, userId = SYSTEM_USER, etrx = undefined) => { + let trx; + try { + trx = etrx ? etrx : await Version.startTransaction(); + // update version record + const s3VersionId = data.s3VersionId ? data.s3VersionId : null; + const version = await Version.query(trx) + .where({ objectId: data.id, s3VersionId: s3VersionId }) + .patch({ + objectId: data.id, + updatedBy: userId, + mimeType: data.mimeType, + etag: data.etag, + isLatest: data.isLatest + }) + .first() + .returning('*'); + + // TODO: consider updating metadata here instead of the controller + if (!etrx) await trx.commit(); + return Promise.resolve(version); + } catch (err) { + if (!etrx && trx) await trx.rollback(); + throw err; + } + }, + + /** + * @function updateIsLatest + * Set version as latest in COMS db. + * Determines latest by checking S3 and ensures only one version has isLatest: true + * @param {string} objectId COMS object uuid + * @param {object} [etrx=undefined] An optional Objection Transaction object + * @returns {object} Version model of latest version + */ + updateIsLatest: async (objectId, etrx = undefined) => { + // TODO: consider having accepting a `userId` argument for version.updatedBy when a version becomes 'latest' + let trx; + try { + trx = etrx ? etrx : await Version.startTransaction(); + + // get VersionId of latest version in S3 + const object = await objectService.read(objectId, trx); + const s3Versions = await storageService.listAllObjectVersions({ + filePath: object.path, + bucketId: object.bucketId + }); + const latestS3VersionId = s3Versions.DeleteMarkers + .concat(s3Versions.Versions) + .filter((v) => v.IsLatest)[0].VersionId; + + // get same version from COMS db + const current = await Version.query(trx) + .first() + .where({ objectId: objectId, s3VersionId: latestS3VersionId }) + .throwIfNotFound(); + let updated; + // update as latest if not already and fetch + if (!current.isLatest) { + updated = await Version.query(trx) + .updateAndFetchById(current.id, { isLatest: true }); + } + // set other versions in COMS db to isLatest=false + await service.removeDuplicateLatest(current.id, current.objectId, trx); + + if (!etrx) await trx.commit(); + return Promise.resolve(updated ?? current); + } catch (err) { + if (!etrx && trx) await trx.rollback(); + throw err; + } + } + +}; + +module.exports = service; diff --git a/comsapi/app/src/validators/bucket.js b/comsapi/app/src/validators/bucket.js new file mode 100644 index 00000000..212d3c02 --- /dev/null +++ b/comsapi/app/src/validators/bucket.js @@ -0,0 +1,80 @@ +const Joi = require('joi'); + +const { scheme, type } = require('./common'); +const { validate } = require('../middleware/validation'); + +const schema = { + createBucket: { + body: Joi.object().keys({ + bucketName: Joi.string().max(255).required(), + accessKeyId: Joi.string().max(255).required(), + bucket: Joi.string().max(255).required(), + endpoint: Joi.string().uri({ scheme: /https?/ }).max(255).required(), + key: Joi.string().trim().max(255), + secretAccessKey: Joi.string().max(255).required(), + region: Joi.string().max(255), + active: type.truthy + }).required(), + }, + + deleteBucket: { + params: Joi.object({ + bucketId: type.uuidv4 + }) + }, + + headBucket: { + params: Joi.object({ + bucketId: type.uuidv4.required() + }) + }, + + readBucket: { + params: Joi.object({ + bucketId: type.uuidv4.required() + }) + }, + + searchBuckets: { + query: Joi.object({ + bucketId: scheme.guid, + bucketName: Joi.string().max(255), + key: Joi.string().max(255), + active: type.truthy + }) + }, + + syncBucket: { + params: Joi.object({ + bucketId: type.uuidv4.required() + }) + }, + + updateBucket: { + body: Joi.object().keys({ + bucketName: Joi.string().max(255), + accessKeyId: Joi.string().max(255), + bucket: Joi.string().max(255), + endpoint: Joi.string().uri({ scheme: /https?/ }).max(255), + secretAccessKey: Joi.string().max(255), + region: Joi.string().max(255), + active: type.truthy + }), + params: Joi.object({ + bucketId: type.uuidv4 + }) + }, +}; + +const validator = { + createBucket: validate(schema.createBucket), + deleteBucket: validate(schema.deleteBucket), + headBucket: validate(schema.headBucket), + readBucket: validate(schema.readBucket), + syncBucket: validate(schema.readBucket), + searchBuckets: validate(schema.searchBuckets), + updateBucket: validate(schema.updateBucket) +}; + +module.exports = validator; +module.exports.schema = schema; diff --git a/comsapi/app/src/validators/bucketPermission.js b/comsapi/app/src/validators/bucketPermission.js new file mode 100644 index 00000000..60958a70 --- /dev/null +++ b/comsapi/app/src/validators/bucketPermission.js @@ -0,0 +1,68 @@ + +const Joi = require('joi'); + +const { scheme, type } = require('./common'); +const { Permissions } = require('../components/constants'); +const { validate } = require('../middleware/validation'); + +const schema = { + searchPermissions: { + query: Joi.object({ + bucketId: scheme.guid, + objectPerms: type.truthy, + permCode: scheme.permCode, + userId: Joi.alternatives() + .conditional('objectPerms', { + is: true, + then: type.uuidv4 + .required() + .messages({ + 'string.guid': 'One userId required when `objectPerms=true`', + }), + otherwise: scheme.guid + }) + }) + }, + + listPermissions: { + params: Joi.object({ + bucketId: scheme.guid + }), + query: Joi.object({ + userId: scheme.guid, + permCode: scheme.permCode + }) + }, + + addPermissions: { + params: Joi.object({ + bucketId: type.uuidv4 + }), + body: Joi.array().items( + Joi.object().keys({ + userId: type.uuidv4.required(), + permCode: Joi.string().required().valid(...Object.values(Permissions)), + }).required() + ).required(), + }, + + removePermissions: { + params: Joi.object({ + bucketId: type.uuidv4 + }), + query: Joi.object({ + userId: scheme.guid, + permCode: scheme.permCode, + }) + } +}; + +const validator = { + searchPermissions: validate(schema.searchPermissions), + listPermissions: validate(schema.listPermissions), + addPermissions: validate(schema.addPermissions), + removePermissions: validate(schema.removePermissions) +}; + +module.exports = validator; +module.exports.schema = schema; diff --git a/comsapi/app/src/validators/common.js b/comsapi/app/src/validators/common.js new file mode 100644 index 00000000..d4c2c174 --- /dev/null +++ b/comsapi/app/src/validators/common.js @@ -0,0 +1,88 @@ +const baseJoi = require('joi'); + +const { EMAILREGEX, Permissions } = require('../components/constants'); + +/** + * @constant Joi + * Extend Base Joi with a custom 'csvArray' parser + */ +const Joi = baseJoi.extend(joi => ({ + type: 'csvArray', + base: joi.array(), + coerce: (value) => ({ + value: value.split ? value.split(',').map(item => item.trim()) : value, + }) +})); + +/** + * @function oneOrMany + * Permits a single or array of comma separated models + * @param {any|any[]} param The model to process + * @returns {object} A Joi object + */ +function oneOrMany(param) { + return Joi.alternatives().try( + Joi.csvArray().items(param), + Joi.array().items(Joi.csvArray().items(param)), + ); +} + +/** + * @constant type + * Base Joi model definitions + */ +const type = { + alphanum: Joi.string().alphanum().max(255), + + truthy: Joi.boolean() + .truthy('true', 1, '1', 't', 'yes', 'y', 'false', 0, '0', 'f', 'no', 'n'), + + email: Joi.string().pattern(new RegExp(EMAILREGEX)).max(255), + + uuidv4: Joi.string().guid({ + version: 'uuidv4' + }), + + /** + * @function metadata + * custom metadata (object) type schema with parameters + * @param {number} [options.minKeyCount=0] Optional minimum number of metadata k/v pairs allowed + * @param {number} [options.minValueStringLength=1] Optional minimum string length of metadata value allowed, + * @returns {object} Joi object + */ + // TODO: Simplify by changing from arrow function to property + metadata: ({ minKeyCount = 0, minValueStringLength = 1 } = {}) => Joi.object() + .pattern(/^x-amz-meta-\S+$/i, Joi.string().min(minValueStringLength), { matches: Joi.array().min(minKeyCount) }) + .unknown(), + + /** + * @function tagset + * custom tagset (object) type schema with parameters + * @param {number} [options.maxKeyCount=9] Optional minimum number of tag k/v pairs allowed + * @param {number} [options.minKeyCount=0] Optional minimum number of tag k/v pairs allowed + * @param {number} [options.minValueStringLength=0] Optional minimum string length of tag value allowed, + * (default of 9 because COMS also adds a `coms-id` tag by default) + * @returns {object} Joi object + */ + // TODO: Simplify by changing from arrow function to property + tagset: ({ maxKeyCount = 9, minKeyCount = 0, minValueStringLength = 0 } = {}) => Joi.object() + .pattern( + /^(?!coms-id$).{1,255}$/, // don't allow key 'coms-id' + Joi.string().min(minValueStringLength).max(255), + { matches: Joi.array().min(minKeyCount).max(maxKeyCount) } + ) +}; + +/** + * @constant scheme + * Composite Joi model definitions + */ +const scheme = { + guid: oneOrMany(type.uuidv4), + + string: oneOrMany(Joi.string().max(255)), + + permCode: oneOrMany(Joi.string().valid(...Object.values(Permissions))) +}; + +module.exports = { oneOrMany, scheme, type }; diff --git a/comsapi/app/src/validators/index.js b/comsapi/app/src/validators/index.js new file mode 100644 index 00000000..4af176bd --- /dev/null +++ b/comsapi/app/src/validators/index.js @@ -0,0 +1,10 @@ +module.exports = { + bucketValidator: require('./bucket'), + bucketPermissionValidator: require('./bucketPermission'), + metadataValidator: require('./metadata'), + objectValidator: require('./object'), + objectPermissionValidator: require('./objectPermission'), + tagValidator: require('./tag'), + userValidator: require('./user'), + versionValidator: require('./version'), +}; diff --git a/comsapi/app/src/validators/metadata.js b/comsapi/app/src/validators/metadata.js new file mode 100644 index 00000000..4c318253 --- /dev/null +++ b/comsapi/app/src/validators/metadata.js @@ -0,0 +1,15 @@ +const { type } = require('./common'); +const { validate } = require('../middleware/validation'); + +const schema = { + searchMetadata: { + headers: type.metadata(), + } +}; + +const validator = { + searchMetadata: validate(schema.searchMetadata) +}; + +module.exports = validator; +module.exports.schema = schema; diff --git a/comsapi/app/src/validators/object.js b/comsapi/app/src/validators/object.js new file mode 100644 index 00000000..bfd66caa --- /dev/null +++ b/comsapi/app/src/validators/object.js @@ -0,0 +1,199 @@ +const Joi = require('joi'); + +const { scheme, type } = require('./common'); +const { DownloadMode } = require('../components/constants'); +const { validate } = require('../middleware/validation'); + +const schema = { + addMetadata: { + headers: type.metadata(), + params: Joi.object({ + objectId: type.uuidv4 + }), + query: Joi.object({ + s3VersionId: Joi.string(), + versionId: type.uuidv4 + }).nand('s3VersionId', 'versionId') + }, + + addTags: { + params: Joi.object({ + objectId: type.uuidv4 + }), + query: Joi.object({ + tagset: type.tagset(), + s3VersionId: Joi.string(), + versionId: type.uuidv4 + }).nand('s3VersionId', 'versionId') + }, + + createObject: { + headers: type.metadata(), + query: Joi.object({ + tagset: type.tagset(), + bucketId: type.uuidv4 + }) + }, + + deleteMetadata: { + headers: type.metadata(), + params: Joi.object({ + objectId: type.uuidv4 + }), + query: Joi.object({ + s3VersionId: Joi.string(), + versionId: type.uuidv4 + }).nand('s3VersionId', 'versionId') + }, + + deleteObject: { + params: Joi.object({ + objectId: type.uuidv4 + }), + query: Joi.object({ + s3VersionId: Joi.string(), + versionId: type.uuidv4 + }).nand('s3VersionId', 'versionId') + }, + + deleteTags: { + params: Joi.object({ + objectId: type.uuidv4 + }), + query: Joi.object({ + tagset: type.tagset(), + s3VersionId: Joi.string(), + versionId: type.uuidv4 + }).nand('s3VersionId', 'versionId') + }, + + fetchMetadata: { + headers: type.metadata(), + query: Joi.object({ + bucketId: scheme.guid, + objectId: scheme.guid + }) + }, + + headObject: { + params: Joi.object({ + objectId: type.uuidv4.required() + }), + query: Joi.object({ + s3VersionId: Joi.string(), + versionId: type.uuidv4 + }).nand('s3VersionId', 'versionId') + }, + + listObjectVersion: { + params: Joi.object({ + objectId: type.uuidv4 + }) + }, + + readObject: { + params: Joi.object({ + objectId: type.uuidv4 + }), + query: Joi.object({ + expiresIn: Joi.number(), + download: Joi.string().valid(...Object.values(DownloadMode)), + s3VersionId: Joi.string(), + versionId: type.uuidv4 + }).nand('s3VersionId', 'versionId') + }, + + replaceMetadata: { + headers: type.metadata(), + params: Joi.object({ + objectId: type.uuidv4 + }), + query: Joi.object({ + s3VersionId: Joi.string(), + versionId: type.uuidv4 + }).nand('s3VersionId', 'versionId') + }, + + replaceTags: { + params: Joi.object({ + objectId: type.uuidv4 + }), + query: Joi.object({ + tagset: type.tagset(), + s3VersionId: Joi.string(), + versionId: type.uuidv4 + }).nand('s3VersionId', 'versionId') + }, + + searchObjects: { + headers: type.metadata(), + query: Joi.object({ + objectId: scheme.guid, + bucketId: scheme.guid, + name: Joi.string(), + path: Joi.string().max(1024), + mimeType: Joi.string().max(255), + tagset: type.tagset(), + public: type.truthy, + active: type.truthy, + deleteMarker: type.truthy, + latest: type.truthy + }) + }, + + fetchTags: { + query: Joi.object({ + bucketId: scheme.guid, + objectId: scheme.guid, + tagset: type.tagset(), + }) + }, + + syncObject: { + params: Joi.object({ + objectId: type.uuidv4.required() + }) + }, + + togglePublic: { + params: Joi.object({ + objectId: type.uuidv4 + }), + query: Joi.object({ + public: type.truthy + }) + }, + + updateObject: { + headers: type.metadata(), + params: Joi.object({ + objectId: type.uuidv4 + }), + query: Joi.object({ + tagset: type.tagset(), + }) + }, +}; + +const validator = { + addMetadata: validate(schema.addMetadata), + addTags: validate(schema.addTags), + createObject: validate(schema.createObject), + deleteMetadata: validate(schema.deleteMetadata), + deleteObject: validate(schema.deleteObject), + deleteTags: validate(schema.deleteTags), + fetchMetadata: validate(schema.fetchMetadata), + fetchTags: validate(schema.fetchTags), + headObject: validate(schema.headObject), + listObjectVersion: validate(schema.listObjectVersion), + readObject: validate(schema.readObject), + replaceMetadata: validate(schema.replaceMetadata), + replaceTags: validate(schema.replaceTags), + searchObjects: validate(schema.searchObjects), + syncObject: validate(schema.syncObject), + togglePublic: validate(schema.togglePublic), + updateObject: validate(schema.updateObject) +}; + +module.exports = validator; +module.exports.schema = schema; diff --git a/comsapi/app/src/validators/objectPermission.js b/comsapi/app/src/validators/objectPermission.js new file mode 100644 index 00000000..3ce99b05 --- /dev/null +++ b/comsapi/app/src/validators/objectPermission.js @@ -0,0 +1,68 @@ +const Joi = require('joi'); + +const { scheme, type } = require('./common'); +const { Permissions } = require('../components/constants'); +const { validate } = require('../middleware/validation'); + +const schema = { + searchPermissions: { + query: Joi.object({ + bucketId: scheme.guid, + bucketPerms: type.truthy, + objectId: scheme.guid, + permCode: scheme.permCode, + userId: Joi.alternatives() + .conditional('bucketPerms', { + is: true, + then: type.uuidv4 + .required() + .messages({ + 'string.guid': 'One userId required when `bucketPerms=true`', + }), + otherwise: scheme.guid + }) + }) + }, + + listPermissions: { + params: Joi.object({ + objectId: scheme.guid + }), + query: Joi.object({ + userId: scheme.guid, + permCode: scheme.permCode + }) + }, + + addPermissions: { + params: Joi.object({ + objectId: type.uuidv4 + }), + body: Joi.array().items( + Joi.object().keys({ + userId: type.uuidv4.required(), + permCode: Joi.string().required().valid(...Object.values(Permissions)), + }).required() + ).required(), + }, + + removePermissions: { + params: Joi.object({ + objectId: type.uuidv4 + }), + query: Joi.object({ + userId: scheme.guid, + permCode: scheme.permCode, + }) + } +}; + +const validator = { + searchPermissions: validate(schema.searchPermissions), + listPermissions: validate(schema.listPermissions), + addPermissions: validate(schema.addPermissions), + removePermissions: validate(schema.removePermissions) +}; + +module.exports = validator; +module.exports.schema = schema; diff --git a/comsapi/app/src/validators/tag.js b/comsapi/app/src/validators/tag.js new file mode 100644 index 00000000..d0186045 --- /dev/null +++ b/comsapi/app/src/validators/tag.js @@ -0,0 +1,20 @@ +const Joi = require('joi'); + +const { type } = require('./common'); +const { validate } = require('../middleware/validation'); + + +const schema = { + searchTags: { + query: Joi.object({ + tagset: type.tagset(), + }) + } +}; + +const validator = { + searchTags: validate(schema.searchTags) +}; + +module.exports = validator; +module.exports.schema = schema; diff --git a/comsapi/app/src/validators/user.js b/comsapi/app/src/validators/user.js new file mode 100644 index 00000000..61143918 --- /dev/null +++ b/comsapi/app/src/validators/user.js @@ -0,0 +1,35 @@ +const Joi = require('joi'); + +const { scheme, type } = require('./common'); +const { validate } = require('../middleware/validation'); + +const schema = { + searchUsers: { + query: Joi.object({ + userId: scheme.guid, + identityId: scheme.string, + idp: scheme.string, + username: type.alphanum, + email: type.email, + firstName: type.alphanum, + fullName: Joi.string().pattern(/^[\w\-\s]+$/).max(255), + lastName: type.alphanum, + active: type.truthy, + search: Joi.string() + }).min(1) + }, + + listIdps: { + query: Joi.object({ + active: type.truthy + }) + } +}; + +const validator = { + searchUsers: validate(schema.searchUsers), + listIdps: validate(schema.listIdps) +}; + +module.exports = validator; +module.exports.schema = schema; diff --git a/comsapi/app/src/validators/version.js b/comsapi/app/src/validators/version.js new file mode 100644 index 00000000..73263907 --- /dev/null +++ b/comsapi/app/src/validators/version.js @@ -0,0 +1,31 @@ +const Joi = require('joi'); + +const { scheme, type } = require('./common'); +const { validate } = require('../middleware/validation'); + + +const schema = { + fetchMetadata: { + headers: type.metadata(), + query: Joi.object({ + s3VersionId: scheme.string, + versionId: scheme.guid + }).nand('s3VersionId', 'versionId') + }, + + fetchTags: { + query: Joi.object({ + tagset: type.tagset(), + s3VersionId: scheme.string, + versionId: scheme.guid + }).nand('s3VersionId', 'versionId') + } +}; + +const validator = { + fetchMetadata: validate(schema.fetchMetadata), + fetchTags: validate(schema.fetchTags) +}; + +module.exports = validator; +module.exports.schema = schema; diff --git a/comsapi/app/tests/common/helper.js b/comsapi/app/tests/common/helper.js new file mode 100644 index 00000000..89ec5dac --- /dev/null +++ b/comsapi/app/tests/common/helper.js @@ -0,0 +1,69 @@ +const express = require('express'); +const Problem = require('api-problem'); + +/** + * @class helper + * Provides helper utilities that are commonly used in tests + */ +const helper = { + /** + * @function expressHelper + * Creates a stripped-down simple Express server object + * @param {string} basePath The path to mount the `router` on + * @param {object} router An express router object to mount + * @returns {object} A simple express server object with `router` mounted to `basePath` + */ + expressHelper: (basePath, router) => { + const app = express(); + + app.use(express.json()); + app.use(express.urlencoded({ + extended: false + })); + app.use(basePath, router); + + // Handle 404 + app.use((req, _res) => { // eslint-disable-line no-unused-vars + throw new Problem(404, { instance: req.originalUrl }); + }); + + // Handle 500 + // eslint-disable-next-line no-unused-vars + app.use((err, req, res, _next) => { + if (err instanceof Problem) { + err.send(res); + } else { + new Problem(500, { detail: err.message ?? err, instance: req.originalUrl }).send(res); + } + }); + + return app; + }, + + /** + * @function resetModel + * Resets a mock objection model + * @param {object} obj A mock objection model + * @param {object} trx A mock transaction object + */ + resetModel: (obj, trx) => { + // Set all jest functions to return itself + Object.keys(obj).forEach((f) => { + if (jest.isMockFunction(obj[f])) obj[f].mockReturnThis(); + }); + obj.startTransaction.mockImplementation(() => trx); + obj.then.mockImplementation((resolve) => resolve(this)); + }, + + /** + * @function trxBuilder + * Returns a mock transaction object + * @returns {object} A mock transaction object + */ + trxBuilder: () => ({ + commit: jest.fn(), + rollback: jest.fn() + }) +}; + +module.exports = helper; diff --git a/comsapi/app/tests/unit/components/crypt.spec.js b/comsapi/app/tests/unit/components/crypt.spec.js new file mode 100644 index 00000000..f5018497 --- /dev/null +++ b/comsapi/app/tests/unit/components/crypt.spec.js @@ -0,0 +1,121 @@ +const config = require('config'); +const crypt = require('../../../src/components/crypt'); + +// Mock config library - @see {@link https://stackoverflow.com/a/64819698} +jest.mock('config'); + +beforeEach(() => { + jest.resetAllMocks(); +}); + +afterAll(() => { + jest.restoreAllMocks(); +}); + +// General testing constants +const passphrase = 'passphrase'; + +describe('encrypt', () => { + describe('without a passphrase', () => { + beforeEach(() => { + config.has.mockReturnValueOnce(false); // server.passphrase + }); + + it.each([ + [''], + ['foobar'], + ['1bazbam'] + ])('should return the same string given %j', (plaintext) => { + const result = crypt.encrypt(plaintext); + expect(typeof result).toEqual('string'); + expect(result).not.toContain(':'); + expect(result).toEqual(plaintext); + }); + }); + + describe('with a passphrase', () => { + beforeEach(() => { + config.has.mockReturnValueOnce(true); // server.passphrase + config.get.mockReturnValueOnce(passphrase); // server.passphrase + }); + + it.each([ + [''], + ['foobar'], + ['1bazbam'], + ['ZOWviyZrzhIhjSo38rtpag==:4Lq5+MmjDjK5JE7J1osSJA=='], + ['V8nPQy/0b+Rj2NefzaY+rg==:9p1TC9i4jFZX7OXcUgUSrA=='], + ['WnF471CEo8U4BVcHXAe2bA==:2iER6Is+gtPPCVCoAYzkOw=='] + ])('should return a string given %j', (plaintext) => { + const result = crypt.encrypt(plaintext); + expect(result).toBeTruthy(); + expect(typeof result).toEqual('string'); + expect(result).toContain(':'); + + const [iv, encrypted] = result.split(':'); + expect(iv).toEqual(expect.any(String)); + expect(encrypted).toEqual(expect.any(String)); + }); + }); +}); + +describe('decrypt', () => { + describe('without a passphrase', () => { + beforeEach(() => { + config.has.mockReturnValueOnce(false); // server.passphrase + }); + + it.each([ + ['', ''], + ['foobar', 'foobar'], + ['1bazbam', '1bazbam'], + ['ZOWviyZrzhIhjSo38rtpag==:4Lq5+MmjDjK5JE7J1osSJA==', 'ZOWviyZrzhIhjSo38rtpag==:4Lq5+MmjDjK5JE7J1osSJA=='], + ['V8nPQy/0b+Rj2NefzaY+rg==:9p1TC9i4jFZX7OXcUgUSrA==', 'V8nPQy/0b+Rj2NefzaY+rg==:9p1TC9i4jFZX7OXcUgUSrA=='], + ['WnF471CEo8U4BVcHXAe2bA==:2iER6Is+gtPPCVCoAYzkOw==', 'WnF471CEo8U4BVcHXAe2bA==:2iER6Is+gtPPCVCoAYzkOw=='] + ])('should return %j given %j', (plaintext, ciphertext) => { + const result = crypt.decrypt(ciphertext); + expect(typeof result).toEqual('string'); + expect(result).toMatch(plaintext); + }); + }); + + describe('with a passphrase', () => { + beforeEach(() => { + config.has.mockReturnValueOnce(true); // server.passphrase + config.get.mockReturnValueOnce(passphrase); // server.passphrase + }); + + it.each([ + ['', ''], + ['foobar', 'foobar'], + ['1bazbam', '1bazbam'], + ['', 'ZOWviyZrzhIhjSo38rtpag==:4Lq5+MmjDjK5JE7J1osSJA=='], + ['foobar', 'V8nPQy/0b+Rj2NefzaY+rg==:9p1TC9i4jFZX7OXcUgUSrA=='], + ['1bazbam', 'WnF471CEo8U4BVcHXAe2bA==:2iER6Is+gtPPCVCoAYzkOw=='] + ])('should return %j given %j', (plaintext, ciphertext) => { + const result = crypt.decrypt(ciphertext); + expect(typeof result).toEqual('string'); + expect(result).toMatch(plaintext); + }); + }); +}); + +describe('isEncrypted', () => { + it.each([ + [false, undefined], + [false, true], + [false, 2], + [false, {}], + [false, ''], + [false, 'foobar'], + [false, '1bazbam'], + [false, 'InvalidIVLengthGarbage:4Lq5+MmjDjK5JE7J1osSJA=='], + [true, 'ZOWviyZrzhIhjSo38rtpag==:4Lq5+MmjDjK5JE7J1osSJA=='], + [true, 'V8nPQy/0b+Rj2NefzaY+rg==:9p1TC9i4jFZX7OXcUgUSrA=='], + [true, 'WnF471CEo8U4BVcHXAe2bA==:2iER6Is+gtPPCVCoAYzkOw=='] + ])('should return %j given %j', (expected, content) => { + const result = crypt.isEncrypted(content); + expect(typeof result).toEqual('boolean'); + expect(result).toEqual(expected); + }); +}); diff --git a/comsapi/app/tests/unit/components/errorToProblem.spec.js b/comsapi/app/tests/unit/components/errorToProblem.spec.js new file mode 100644 index 00000000..c0a5daff --- /dev/null +++ b/comsapi/app/tests/unit/components/errorToProblem.spec.js @@ -0,0 +1,116 @@ +const Problem = require('api-problem'); + +const errorToProblem = require('../../../src/components/errorToProblem'); + +const SERVICE = 'TESTSERVICE'; + +describe('errorToProblem', () => { + it('should return a 422 problem given a problem', () => { + const msg = 'errMsg'; + const e = new Problem(422, { detail: msg }); + const result = errorToProblem(SERVICE, e); + + expect(result).toBeTruthy(); + expect(result instanceof Problem).toBeTruthy(); + expect(result.title).toMatch('Unprocessable Entity'); + expect(result.status).toBe(422); + expect(result.detail).toMatch(msg); + expect(result.errors).toBeUndefined(); + }); + + it('should return a 422 problem given an error', () => { + const e = { + response: { + data: { detail: 'detail' }, + status: 422 + } + }; + const result = errorToProblem(SERVICE, e); + + expect(result).toBeTruthy(); + expect(result instanceof Problem).toBeTruthy(); + expect(result.title).toMatch('Unprocessable Entity'); + expect(result.status).toBe(422); + expect(result.detail).toMatch(e.response.data.detail); + expect(result.errors).toBeUndefined(); + }); + + it('should return a 409 problem given an error', () => { + const e = { + response: { + data: { detail: 'detail' }, + status: 409 + } + }; + const result = errorToProblem(SERVICE, e); + + expect(result).toBeTruthy(); + expect(result instanceof Problem).toBeTruthy(); + expect(result.title).toMatch('Conflict'); + expect(result.status).toBe(409); + expect(result.detail).toEqual(expect.objectContaining(e.response.data)); + expect(result.errors).toBeUndefined(); + }); + + it('should return a problem given an error with statusCode', () => { + const e = { + statusCode: 404, + message: 'NotFoundError' + }; + const result = errorToProblem(SERVICE, e); + + expect(result).toBeTruthy(); + expect(result instanceof Problem).toBeTruthy(); + expect(result.title).toMatch('Not Found'); + expect(result.status).toBe(404); + expect(result.detail).toMatch(e.message); + expect(result.errors).toBeUndefined(); + }); + + it('should return a problem given an error with s3 metadata', () => { + const e = { + $metadata: { + httpStatusCode: 404, + }, + message: 'NotFoundError' + }; + const result = errorToProblem(SERVICE, e); + + expect(result).toBeTruthy(); + expect(result instanceof Problem).toBeTruthy(); + expect(result.title).toMatch('Not Found'); + expect(result.status).toBe(404); + expect(result.detail).toEqual(expect.objectContaining({ message: e.message })); + expect(result.errors).toBeUndefined(); + }); + + it('should return a 422 problem with a supplied string response', () => { + const e = { + response: { + data: '{ "detail": "d" }', + status: 422 + } + }; + const result = errorToProblem(SERVICE, e); + + expect(result).toBeTruthy(); + expect(result instanceof Problem).toBeTruthy(); + expect(result.title).toMatch('Unprocessable Entity'); + expect(result.status).toBe(422); + expect(result.detail).toMatch('d'); + expect(result.errors).toBeUndefined(); + }); + + it('should throw a 500 problem', () => { + const e = { + message: 'msg' + }; + const result = errorToProblem(SERVICE, e); + + expect(result).toBeTruthy(); + expect(result instanceof Problem).toBeTruthy(); + expect(result.title).toMatch('Internal Server Error'); + expect(result.status).toBe(500); + expect(result.detail).toMatch(e.message); + }); +}); diff --git a/comsapi/app/tests/unit/components/log.spec.js b/comsapi/app/tests/unit/components/log.spec.js new file mode 100644 index 00000000..b8d4e3e6 --- /dev/null +++ b/comsapi/app/tests/unit/components/log.spec.js @@ -0,0 +1,36 @@ +const config = require('config'); + +const getLogger = require('../../../src/components/log'); +const httpLogger = require('../../../src/components/log').httpLogger; + +describe('getLogger', () => { + const assertLogger = (log) => { + expect(log).toBeTruthy(); + expect(typeof log).toBe('object'); + expect(typeof log.pipe).toBe('function'); + expect(log.exitOnError).toBeFalsy(); + expect(log.format).toBeTruthy(); + expect(log.level).toBe(config.get('server.logLevel')); + expect(log.transports.length).toBeGreaterThanOrEqual(1); + }; + + it('should return a winston logger', () => { + const result = getLogger(); + assertLogger(result); + }); + + it('should return a child winston logger with metadata overrides', () => { + const result = getLogger('test'); + assertLogger(result); + }); +}); + +describe('httpLogger', () => { + it('should return a winston middleware function', () => { + const result = httpLogger; + + expect(result).toBeTruthy(); + expect(typeof result).toBe('function'); + expect(result.length).toBe(3); + }); +}); diff --git a/comsapi/app/tests/unit/components/queueManager.spec.js b/comsapi/app/tests/unit/components/queueManager.spec.js new file mode 100644 index 00000000..58869d90 --- /dev/null +++ b/comsapi/app/tests/unit/components/queueManager.spec.js @@ -0,0 +1,333 @@ +const config = require('config'); + +const QueueManager = require('../../../src/components/queueManager'); +const { objectQueueService, syncService } = require('../../../src/services'); + +// Mock config library - @see {@link https://stackoverflow.com/a/64819698} +jest.mock('config'); + +beforeEach(() => { + jest.clearAllMocks(); +}); + +describe('constructor', () => { + it('should return a queue manager instance', () => { + const qm = new QueueManager(); + + expect(qm).toBeTruthy(); + expect(qm.isBusy).toBeFalsy(); + expect(qm.toClose).toBeFalsy(); + }); +}); + +describe('isBusy', () => { + const qm = new QueueManager(); + + beforeEach(() => { + qm._cb = undefined; + qm._toClose = false; + }); + + it('should not invoke callback when true and not closing', () => { + qm.isBusy = true; + + expect(qm).toBeTruthy(); + expect(qm.isBusy).toBeTruthy(); + expect(qm.toClose).toBeFalsy(); + }); + + it('should not invoke callback when false and closing', () => { + qm._toClose = true; + + qm.isBusy = false; + + expect(qm).toBeTruthy(); + expect(qm.isBusy).toBeFalsy(); + expect(qm.toClose).toBeTruthy(); + expect(qm._cb).toBeUndefined(); + }); + + it('should invoke callback when false and closing', () => { + qm._cb = jest.fn(); + qm._toClose = true; + + qm.isBusy = false; + + expect(qm).toBeTruthy(); + expect(qm.isBusy).toBeFalsy(); + expect(qm.toClose).toBeTruthy(); + expect(qm._cb).toHaveBeenCalledTimes(1); + }); +}); + +describe('checkQueue', () => { + const qm = new QueueManager(); + + const processNextJobSpy = jest.spyOn(qm, 'processNextJob'); + const queueSizeSpy = jest.spyOn(objectQueueService, 'queueSize'); + + beforeEach(() => { + qm._isBusy = false; + qm._toClose = false; + + processNextJobSpy.mockReset(); + queueSizeSpy.mockReset(); + }); + + afterAll(() => { + processNextJobSpy.mockRestore(); + queueSizeSpy.mockRestore(); + }); + + it('should call nothing when busy', () => { + qm._isBusy = true; + + qm.checkQueue(); + + expect(qm).toBeTruthy(); + expect(qm.isBusy).toBeTruthy(); + expect(qm.toClose).toBeFalsy(); + expect(queueSizeSpy).toHaveBeenCalledTimes(0); + expect(processNextJobSpy).toHaveBeenCalledTimes(0); + }); + + it('should call nothing when closing', () => { + qm._toClose = true; + + qm.checkQueue(); + + expect(qm).toBeTruthy(); + expect(qm.isBusy).toBeFalsy(); + expect(qm.toClose).toBeTruthy(); + expect(queueSizeSpy).toHaveBeenCalledTimes(0); + expect(processNextJobSpy).toHaveBeenCalledTimes(0); + }); + + it('should not call processNextJob when there are no jobs', () => { + queueSizeSpy.mockResolvedValue(0); + + qm.checkQueue(); + + expect(qm).toBeTruthy(); + expect(qm.isBusy).toBeFalsy(); + expect(qm.toClose).toBeFalsy(); + expect(queueSizeSpy).toHaveBeenCalledTimes(1); + expect(processNextJobSpy).toHaveBeenCalledTimes(0); + }); + + it('should call processNextJob when there are jobs', () => { + processNextJobSpy.mockReturnValue(); + queueSizeSpy.mockResolvedValue(1); + + qm.checkQueue(); + + expect(qm).toBeTruthy(); + expect(qm.isBusy).toBeFalsy(); + expect(qm.toClose).toBeFalsy(); + expect(queueSizeSpy).toHaveBeenCalledTimes(1); + // TODO: This is definitely being called, but call count is not incrementing for some reason + // expect(processNextJobSpy).toHaveBeenCalledTimes(1); + }); + + it('should not throw when there is a failure', () => { + queueSizeSpy.mockRejectedValue('error'); + + qm.checkQueue(); + + expect(qm).toBeTruthy(); + expect(qm.isBusy).toBeFalsy(); + expect(qm.toClose).toBeFalsy(); + expect(queueSizeSpy).toHaveBeenCalledTimes(1); + expect(processNextJobSpy).toHaveBeenCalledTimes(0); + }); +}); + +describe('close', () => { + const qm = new QueueManager(); + + beforeEach(() => { + qm._cb = undefined; + qm._isBusy = false; + qm._toClose = false; + }); + + it('should store but not run the callback when busy', () => { + const cb = jest.fn(() => { }); + qm._isBusy = true; + + qm.close(cb); + + expect(qm).toBeTruthy(); + expect(qm.isBusy).toBeTruthy(); + expect(qm.toClose).toBeTruthy(); + expect(qm._cb).toBe(cb); + expect(cb).toHaveBeenCalledTimes(0); + }); + + it('should not run the callback when undefined and not busy', () => { + qm.close(); + + expect(qm).toBeTruthy(); + expect(qm.isBusy).toBeFalsy(); + expect(qm.toClose).toBeTruthy(); + }); + + it('should run the callback when not busy', () => { + const cb = jest.fn(() => { }); + + qm.close(cb); + + expect(qm).toBeTruthy(); + expect(qm.isBusy).toBeFalsy(); + expect(qm.toClose).toBeTruthy(); + expect(cb).toHaveBeenCalledTimes(1); + }); +}); + +describe('processNextJob', () => { + const qm = new QueueManager(); + + const checkQueueSpy = jest.spyOn(qm, 'checkQueue'); + const enqueueSpy = jest.spyOn(objectQueueService, 'enqueue'); + const dequeueSpy = jest.spyOn(objectQueueService, 'dequeue'); + const syncJobSpy = jest.spyOn(syncService, 'syncJob'); + + const job = { + bucketId: 'bucketId', + createdBy: 'createdBy', + full: false, + id: 'id', + path: 'path', + retries: 0 + }; + + beforeEach(() => { + qm._cb = undefined; + qm._isBusy = false; + qm._toClose = false; + + config.get.mockReturnValueOnce('3'); // server.maxRetries + + checkQueueSpy.mockReset(); + enqueueSpy.mockReset(); + dequeueSpy.mockReset(); + syncJobSpy.mockReset(); + }); + + afterAll(() => { + checkQueueSpy.mockRestore(); + enqueueSpy.mockRestore(); + dequeueSpy.mockRestore(); + syncJobSpy.mockRestore(); + }); + + it('should do nothing if queue is empty', async () => { + dequeueSpy.mockResolvedValue([]); + + await qm.processNextJob(); + + expect(qm).toBeTruthy(); + expect(qm.isBusy).toBeFalsy(); + expect(qm.toClose).toBeFalsy(); + expect(checkQueueSpy).toHaveBeenCalledTimes(0); + expect(enqueueSpy).toHaveBeenCalledTimes(0); + expect(dequeueSpy).toHaveBeenCalledTimes(1); + expect(syncJobSpy).toHaveBeenCalledTimes(0); + }); + + it('should do the next syncJob successfully and check queue', async () => { + dequeueSpy.mockResolvedValue([job]); + syncJobSpy.mockResolvedValue('objectId'); + + await qm.processNextJob(); + + expect(qm).toBeTruthy(); + expect(qm.isBusy).toBeFalsy(); + expect(qm.toClose).toBeFalsy(); + expect(checkQueueSpy).toHaveBeenCalledTimes(1); + expect(enqueueSpy).toHaveBeenCalledTimes(0); + expect(dequeueSpy).toHaveBeenCalledTimes(1); + expect(syncJobSpy).toHaveBeenCalledTimes(1); + expect(syncJobSpy).toHaveBeenCalledWith(job.path, job.bucketId, job.full, job.createdBy); + }); + + it('should do the next syncJob successfully and not check queue when toClose', async () => { + qm._toClose = true; + dequeueSpy.mockResolvedValue([job]); + syncJobSpy.mockResolvedValue('objectId'); + + await qm.processNextJob(); + + expect(qm).toBeTruthy(); + expect(qm.isBusy).toBeFalsy(); + expect(qm.toClose).toBeTruthy(); + expect(checkQueueSpy).toHaveBeenCalledTimes(0); + expect(enqueueSpy).toHaveBeenCalledTimes(0); + expect(dequeueSpy).toHaveBeenCalledTimes(1); + expect(syncJobSpy).toHaveBeenCalledTimes(1); + expect(syncJobSpy).toHaveBeenCalledWith(job.path, job.bucketId, job.full, job.createdBy); + }); + + it('should re-enqueue a failed job when less than max retries', async () => { + enqueueSpy.mockResolvedValue(1); + dequeueSpy.mockResolvedValue([job]); + syncJobSpy.mockImplementation(() => { throw new Error('error'); }); + + await qm.processNextJob(); + + expect(qm).toBeTruthy(); + expect(qm.isBusy).toBeFalsy(); + expect(qm.toClose).toBeFalsy(); + expect(checkQueueSpy).toHaveBeenCalledTimes(0); + expect(enqueueSpy).toHaveBeenCalledTimes(1); + expect(enqueueSpy).toHaveBeenCalledWith(expect.objectContaining({ + jobs: expect.arrayContaining([{ bucketId: job.bucketId, path: job.path }]), + full: job.full, + retries: job.retries + 1, + createdBy: job.createdBy + })); + expect(dequeueSpy).toHaveBeenCalledTimes(1); + expect(syncJobSpy).toHaveBeenCalledTimes(1); + expect(syncJobSpy).toHaveBeenCalledWith(job.path, job.bucketId, job.full, job.createdBy); + }); + + it('should re-enqueue a failed job when less than max retries and fail gracefully', async () => { + enqueueSpy.mockRejectedValue('error'); + dequeueSpy.mockResolvedValue([job]); + syncJobSpy.mockImplementation(() => { throw new Error('error'); }); + + await qm.processNextJob(); + + expect(qm).toBeTruthy(); + expect(qm.isBusy).toBeFalsy(); + expect(qm.toClose).toBeFalsy(); + expect(checkQueueSpy).toHaveBeenCalledTimes(0); + expect(enqueueSpy).toHaveBeenCalledTimes(1); + expect(enqueueSpy).toHaveBeenCalledWith(expect.objectContaining({ + jobs: expect.arrayContaining([{ bucketId: job.bucketId, path: job.path }]), + full: job.full, + retries: job.retries + 1, + createdBy: job.createdBy + })); + expect(dequeueSpy).toHaveBeenCalledTimes(1); + expect(syncJobSpy).toHaveBeenCalledTimes(1); + expect(syncJobSpy).toHaveBeenCalledWith(job.path, job.bucketId, job.full, job.createdBy); + }); + + it('should not re-enqueue a failed job when at max retries', async () => { + dequeueSpy.mockResolvedValue([{ ...job, retries: 3 }]); + syncJobSpy.mockImplementation(() => { throw new Error('error'); }); + + await qm.processNextJob(); + + expect(qm).toBeTruthy(); + expect(qm.isBusy).toBeFalsy(); + expect(qm.toClose).toBeFalsy(); + expect(checkQueueSpy).toHaveBeenCalledTimes(0); + expect(enqueueSpy).toHaveBeenCalledTimes(0); + expect(dequeueSpy).toHaveBeenCalledTimes(1); + expect(syncJobSpy).toHaveBeenCalledTimes(1); + expect(syncJobSpy).toHaveBeenCalledWith(job.path, job.bucketId, job.full, job.createdBy); + }); +}); + diff --git a/comsapi/app/tests/unit/components/utils.spec.js b/comsapi/app/tests/unit/components/utils.spec.js new file mode 100644 index 00000000..fb07cd49 --- /dev/null +++ b/comsapi/app/tests/unit/components/utils.spec.js @@ -0,0 +1,577 @@ +const config = require('config'); + +const { AuthMode, AuthType } = require('../../../src/components/constants'); +const { bucketService } = require('../../../src/services'); +const utils = require('../../../src/components/utils'); +const Problem = require('api-problem'); + +// Mock config library - @see {@link https://stackoverflow.com/a/64819698} +jest.mock('config'); + +const DEFAULTREGION = 'us-east-1'; // Need to specify valid AWS region or it'll explode ('us-east-1' is default, 'ca-central-1' for Canada) + +beforeEach(() => { + jest.resetAllMocks(); +}); + +afterAll(() => { + jest.restoreAllMocks(); +}); + +describe('addDashesToUuid', () => { + it.each([ + [undefined, undefined], + [null, null], + [123, 123], + [{}, {}], + ['123456789012345678901234567890', '123456789012345678901234567890'], + ['e0603b59-2edc-45f7-acc7-b0cccd6656e1', 'e0603b592edc45f7acc7b0cccd6656e1'], + ['e0603b59-2edc-45f7-acc7-b0cccd6656e1', 'E0603B592EDC45F7ACC7B0CCCD6656E1'] + ])('should return %o given %j', (expected, str) => { + expect(utils.addDashesToUuid(str)).toEqual(expected); + }); +}); + +describe('calculatePartSize', () => { + it.each([ + [undefined, undefined], + [undefined, null], + [undefined, 'foo'], + [undefined, []], + [undefined, {}], + [undefined, 0], + [5242880, 1], + [5242880, 5242880], // 5MB + [5242880, 5368709120], // 5GB + [5242880, 52428800000], // ~50GB + [10485760, 52428800001], // ~50GB + [10485760, 104857600000], // ~100GB + [15728640, 104857600001], // ~100GB + [550502400, 5497558138880], // 5TB + ])('should return %o given %j', (expected, length) => { + expect(utils.calculatePartSize(length)).toEqual(expected); + }); +}); + +describe('delimit', () => { + beforeAll(() => { + if (jest.isMockFunction(utils.delimit)) { + utils.delimit.mockRestore(); + } + }); + + it.each([ + // Should return empty string if falsy input + ['', undefined], + ['', null], + ['', ''], + // Strings with trailing delimiters should remain unchanged + ['1234/', '1234/'], + ['/', '/'], + // Strings without trailing delimiters should have delimiter appended + ['1234/', '1234'], + [' /', ' '] + ])('should return %o given %j', (expected, str) => { + expect(utils.delimit(str)).toEqual(expected); + }); +}); + +describe('getAppAuthMode', () => { + it.each([ + [AuthMode.NOAUTH, false, false], + [AuthMode.BASICAUTH, true, false], + [AuthMode.OIDCAUTH, false, true], + [AuthMode.FULLAUTH, true, true] + ])('should return %s when basicAuth.enabled %s and keycloak.enabled %s', (expected, basicAuth, keycloak) => { + config.has + .mockReturnValueOnce(basicAuth) // basicAuth.enabled + .mockReturnValueOnce(keycloak); // keycloak.enabled + + const result = utils.getAppAuthMode(); + + expect(result).toEqual(expected); + expect(config.has).toHaveBeenCalledTimes(2); + }); +}); + +describe('getBucket', () => { + const cdata = { + accessKeyId: 'accessKeyId', + bucket: 'bucket', + endpoint: 'https://endpoint.com', + key: 'filePath', + region: DEFAULTREGION, + secretAccessKey: 'secretAccessKey' + }; + const ddata = { + accessKeyId: 'foo', + bucket: 'bar', + endpoint: 'https://baz.com', + key: 'koo', + region: DEFAULTREGION, + secretAccessKey: 'soo' + }; + const readBucketSpy = jest.spyOn(bucketService, 'read'); + + it('should return config data when it exists, given no bucketId', async () => { + config.has.mockReturnValue(true); + config.get + .mockReturnValueOnce(cdata.accessKeyId) // objectStorage.accessKeyId + .mockReturnValueOnce(cdata.bucket) // objectStorage.bucket + .mockReturnValueOnce(cdata.endpoint) // objectStorage.endpoint + .mockReturnValueOnce(cdata.key) // objectStorage.key + .mockReturnValueOnce(cdata.secretAccessKey) // objectStorage.secretAccessKey + .mockReturnValueOnce(cdata.region); // objectStorage.region + + const result = await utils.getBucket(); + + expect(result).toBeTruthy(); + expect(result).toEqual(cdata); + expect(result).toHaveProperty('accessKeyId', cdata.accessKeyId); + expect(result).toHaveProperty('bucket', cdata.bucket); + expect(result).toHaveProperty('endpoint', cdata.endpoint); + expect(result).toHaveProperty('key', cdata.key); + expect(result).toHaveProperty('region', cdata.region); + expect(result).toHaveProperty('secretAccessKey', cdata.secretAccessKey); + expect(readBucketSpy).toHaveBeenCalledTimes(0); + }); + + it('should throw when no config data exists, given no bucketId', async () => { + config.has.mockReturnValue(false); + + const result = (() => utils.getBucket(undefined))(); + + expect(result).rejects.toThrow(); + expect(readBucketSpy).toHaveBeenCalledTimes(0); + }); + + it('should return database data given a good bucketId', async () => { + readBucketSpy.mockResolvedValue(ddata); + + const result = await utils.getBucket('bucketId'); + + expect(result).toBeTruthy(); + expect(result).toEqual(ddata); + expect(result).toHaveProperty('bucket', ddata.bucket); + expect(result).toHaveProperty('endpoint', ddata.endpoint); + expect(result).toHaveProperty('key', ddata.key); + expect(result).toHaveProperty('region', ddata.region); + expect(result).toHaveProperty('secretAccessKey', ddata.secretAccessKey); + expect(readBucketSpy).toHaveBeenCalledTimes(1); + expect(readBucketSpy).toHaveBeenCalledWith('bucketId'); + }); + + it('should throw given a bad bucketId', () => { + readBucketSpy.mockImplementation(() => { throw new Problem(422); }); + + const result = (() => utils.getBucket('bad bucketId'))(); + + expect(result).rejects.toThrow(); + expect(readBucketSpy).toHaveBeenCalledTimes(1); + expect(readBucketSpy).toHaveBeenCalledWith('bad bucketId'); + }); +}); + +describe('getCurrentIdentity', () => { + const getCurrentTokenClaimSpy = jest.spyOn(utils, 'getCurrentTokenClaim'); + const parseIdentityKeyClaimsSpy = jest.spyOn(utils, 'parseIdentityKeyClaims'); + + const idirClaim = 'idir_user_guid'; + const subClaim = 'sub'; + + beforeEach(() => { + getCurrentTokenClaimSpy.mockReset().mockImplementation(() => { }); + parseIdentityKeyClaimsSpy.mockReset(); + }); + + it.each([ + [undefined, [subClaim]], + [undefined, [idirClaim, subClaim]], + [null, [subClaim]], + [null, [idirClaim, subClaim]], + ['', [subClaim]], + ['', [idirClaim, subClaim]], + [[], [subClaim]], + [[], [idirClaim, subClaim]], + [{}, [subClaim]], + [{}, [idirClaim, subClaim]] + ])('should call functions correctly given %j', (currentUser, idKeys) => { + parseIdentityKeyClaimsSpy.mockReturnValue(idKeys); + + utils.getCurrentIdentity(currentUser); + + expect(getCurrentTokenClaimSpy).toHaveBeenCalledTimes(idKeys.length); + if (idKeys.length > 1) expect(getCurrentTokenClaimSpy).toHaveBeenCalledWith(currentUser, idirClaim, undefined); + expect(getCurrentTokenClaimSpy).toHaveBeenCalledWith(currentUser, subClaim, undefined); + expect(parseIdentityKeyClaimsSpy).toHaveBeenCalledTimes(1); + expect(parseIdentityKeyClaimsSpy).toHaveBeenCalledWith(); + }); + + it.each([ + [undefined, [subClaim]], + [undefined, [idirClaim, subClaim]], + [null, [subClaim]], + [null, [idirClaim, subClaim]], + ['', [subClaim]], + ['', [idirClaim, subClaim]], + [[], [subClaim]], + [[], [idirClaim, subClaim]], + [{}, [subClaim]], + [{}, [idirClaim, subClaim]] + ])('should call functions correctly given %j and defaultValue \'default\'', (currentUser, idKeys) => { + const defaultValue = 'default'; + parseIdentityKeyClaimsSpy.mockReturnValue(idKeys); + + utils.getCurrentIdentity(currentUser, defaultValue); + + expect(getCurrentTokenClaimSpy).toHaveBeenCalledTimes(idKeys.length); + if (idKeys.length > 1) expect(getCurrentTokenClaimSpy).toHaveBeenCalledWith(currentUser, idirClaim, undefined); + expect(getCurrentTokenClaimSpy).toHaveBeenCalledWith(currentUser, subClaim, undefined); + expect(parseIdentityKeyClaimsSpy).toHaveBeenCalledTimes(1); + expect(parseIdentityKeyClaimsSpy).toHaveBeenCalledWith(); + }); +}); + +describe('getCurrentSubject', () => { + const getCurrentTokenClaimSpy = jest.spyOn(utils, 'getCurrentTokenClaim'); + + beforeEach(() => { + getCurrentTokenClaimSpy.mockReset().mockImplementation(() => { }); + }); + + it.each([undefined, null, '', [], {}])('should call getCurrentTokenClaim correctly given %j', (currentUser) => { + utils.getCurrentSubject(currentUser); + + expect(getCurrentTokenClaimSpy).toHaveBeenCalledTimes(1); + expect(getCurrentTokenClaimSpy).toHaveBeenCalledWith(currentUser, 'sub', undefined); + }); + + it.each([undefined, null, '', [], {}])('should call getCurrentTokenClaim correctly given %j and defaultValue \'default\'', (currentUser) => { + const defaultValue = 'default'; + utils.getCurrentSubject(currentUser, defaultValue); + + expect(getCurrentTokenClaimSpy).toHaveBeenCalledTimes(1); + expect(getCurrentTokenClaimSpy).toHaveBeenCalledWith(currentUser, 'sub', defaultValue); + }); +}); + +describe('getCurrentTokenClaim', () => { + const defaultValue = 'default'; + + beforeAll(() => { + if (jest.isMockFunction(utils.getCurrentTokenClaim)) { + utils.getCurrentTokenClaim.mockRestore(); + } + }); + + it.each([ + // Should return defaultValue if no currentUser + [undefined, undefined, undefined], + [undefined, undefined, 'bad'], + [undefined, undefined, 'sub'], + [undefined, null, undefined], + [undefined, null, 'bad'], + [undefined, null, 'sub'], + // Should return defaultValue if invalid currentUser + [undefined, '', undefined], + [undefined, '', 'bad'], + [undefined, '', 'sub'], + [undefined, {}, undefined], + [undefined, {}, 'bad'], + [undefined, {}, 'sub'], + [undefined, { a: 1 }, undefined], + [undefined, { a: 1 }, 'bad'], + [undefined, { a: 1 }, 'sub'], + // Should return defaultValue if not authType Bearer + [undefined, { authType: AuthType.BASIC }, undefined], + [undefined, { authType: AuthType.BASIC }, 'bad'], + [undefined, { authType: AuthType.BASIC }, 'sub'], + // Should return claim value if authType Bearer + [undefined, { authType: AuthType.BEARER, tokenPayload: { sub: 'foo' } }, undefined], + [undefined, { authType: AuthType.BEARER, tokenPayload: { sub: 'foo' } }, 'bad'], + ['foo', { authType: AuthType.BEARER, tokenPayload: { sub: 'foo' } }, 'sub'], + ])('should return %j given currentUser %j and claim %j', (expected, currentUser, claim) => { + expect(utils.getCurrentTokenClaim(currentUser, claim)).toBe(expected); + }); + + it.each([ + // Should return defaultValue if no currentUser + [defaultValue, undefined, undefined], + [defaultValue, undefined, 'bad'], + [defaultValue, undefined, 'sub'], + [defaultValue, null, undefined], + [defaultValue, null, 'bad'], + [defaultValue, null, 'sub'], + // Should return defaultValue if invalid currentUser + [defaultValue, '', undefined], + [defaultValue, '', 'bad'], + [defaultValue, '', 'sub'], + [defaultValue, {}, undefined], + [defaultValue, {}, 'bad'], + [defaultValue, {}, 'sub'], + [defaultValue, { a: 1 }, undefined], + [defaultValue, { a: 1 }, 'bad'], + [defaultValue, { a: 1 }, 'sub'], + // Should return defaultValue if not authType Bearer + [defaultValue, { authType: AuthType.BASIC }, undefined], + [defaultValue, { authType: AuthType.BASIC }, 'bad'], + [defaultValue, { authType: AuthType.BASIC }, 'sub'], + // Should return claim value if authType Bearer + [undefined, { authType: AuthType.BEARER, tokenPayload: { sub: 'foo' } }, undefined], + [undefined, { authType: AuthType.BEARER, tokenPayload: { sub: 'foo' } }, 'bad'], + ['foo', { authType: AuthType.BEARER, tokenPayload: { sub: 'foo' } }, 'sub'], + ])('should return %j given currentUser %j, claim %j and defaultValue \'default\'', (expected, currentUser, claim) => { + expect(utils.getCurrentTokenClaim(currentUser, claim, defaultValue)).toBe(expected); + }); +}); + +describe('getGitRevision', () => { + expect(typeof utils.getGitRevision()).toBe('string'); +}); + +describe('getKeyValue', () => { + it.each([ + [[], null], + [[], undefined], + [[], []], + [[], {}], + [[{ key: 'foo', value: 'bar' }], { foo: 'bar' }], + [[{ key: 'k1', value: 'v1' }, { key: 'k2', value: 'v2' }], { k1: 'v1', k2: 'v2' }], + ])('should yield %j when given %j', (expected, input) => { + expect(utils.getKeyValue(input)).toEqual(expected); + }); +}); + +describe('getMetadata', () => { + it.each([ + [undefined, {}], + [undefined, { 'Content-Length': 1234 }], + [{ foo: 'bar' }, { 'Content-Length': 1234, 'x-amz-meta-foo': 'bar' }], + [{ foo: 'bar', baz: 'quz' }, { 'Content-Length': 1234, 'x-amz-meta-foo': 'bar', 'x-amz-meta-baz': 'quz' }], + [{ bam: 'blam', run: 'ran' }, { 'Content-Length': 1234, 'X-Amz-Meta-Bam': 'blam', 'x-AmZ-mEtA-rUn': 'ran' }], + ])('should yield %j when given %j', (expected, input) => { + expect(utils.getMetadata(input)).toEqual(expected); + }); +}); + +describe('getObjectsByKeyValue', () => { + it.each([ + [undefined, [], undefined, undefined], + [undefined, [], 'foo', 'bar'], + [{ key: 'a', value: '1' }, [{ key: 'a', value: '1' }, { key: 'b', value: '1' }], 'a', '1'], + [{ key: 'b', value: '1' }, [{ key: 'a', value: '1' }, { key: 'b', value: '1' }], 'b', '1'], + ])('should yield %j when given array %j, key %s and value %s', (expected, array, key, value) => { + expect(utils.getObjectsByKeyValue(array, key, value)).toEqual(expected); + }); +}); + +describe('groupByObject', () => { + const test1 = { foo: 'baz', blah: 'test' }; + const test2 = { foo: 'baz', blah: 'test2' }; + const test3 = { foo: 'free', blah: 'test3' }; + + it.each([ + [[], 'foo', 'bar', []], + [[{ foo: 'baz', bar: [test1] }], 'foo', 'bar', [test1]], + [[{ foo: 'baz', bar: [test1, test2] }], 'foo', 'bar', [test1, test2]], + [[{ foo: 'baz', bar: [test1, test2] }, { foo: 'free', bar: [test3] }], 'foo', 'bar', [test1, test2, test3]] + ])('should return %j given property %s, group %s and objectArray %j', (expected, property, group, objectArray) => { + expect(utils.groupByObject(property, group, objectArray)).toEqual(expected); + }); +}); + +describe('isAtPath', () => { + it.each([ + [false, undefined, undefined], + [false, null, null], + [true, '', ''], // Root level empty string identities should technically be true + [true, '', 'file'], + [false, '', 'file/bleep'], + [true, '/', 'file'], + [false, '/', 'file/bleep'], + [true, 'foo', 'foo'], // Root level file identities should be true + [false, 'foo', 'bar'], // Non-matching root level path and prefix should be false + [true, 'foo', 'foo/bar'], + [true, 'foo', '/foo/bar'], + [true, '/foo', 'foo/bar'], + [true, '/foo', '/foo/bar'], + [true, 'a/b', 'a/b/foo.jpg'], + [false, 'a', 'a/b/'], // Trailing slashes references the folder and should be excluded + [false, 'a/b', 'a/b/'], // Trailing slashes references the folder and should be excluded + [false, 'a/b', 'a/b/z/deep.jpg'], + [false, 'a/b', 'a/b/y/z/deep.jpg'], + [false, 'a/b', 'a/b/c/'], // Trailing slashes references the folder and should be excluded + [false, 'a/b/c', 'a/b/c/'], // Trailing slashes references the folder and should be excluded + [false, 'a/b/c', 'a/bar.png'], + [false, 'c/b/a', 'a/b/c/bar.png'], + [false, 'c/a/b', 'a/b/c/bar.png'], + [false, 'a/b/c', 'a/c/b/bar.png'], + [true, 'a/b/c', 'a/b/c/bar.png'], + ])('should return %j given prefix %j and path %j', (expected, prefix, path) => { + expect(utils.isAtPath(prefix, path)).toEqual(expected); + }); +}); + +describe('isTruthy', () => { + it('should return undefined given undefined', () => { + expect(utils.isTruthy(undefined)).toBeUndefined(); + }); + + it.each([ + true, 1, 'true', 'TRUE', 't', 'T', 'yes', 'yEs', 'y', 'Y', '1', new String('true') + ])('should return true given %j', (value) => { + expect(utils.isTruthy(value)).toBeTruthy(); + }); + + it.each([ + false, 0, 'false', 'FALSE', 'f', 'F', 'no', 'nO', 'n', 'N', '0', new String('false'), {} + ])('should return false given %j', (value) => { + expect(utils.isTruthy(value)).toBeFalsy(); + }); +}); + +describe('joinPath', () => { + beforeAll(() => { + if (jest.isMockFunction(utils.joinPath)) { + utils.joinPath.mockRestore(); + } + }); + + it('should return blank if nothing supplied', () => { + expect(utils.joinPath()).toEqual(''); + }); + + it('should return multiple parts joined with the delimiter', () => { + expect(utils.joinPath('my', 'file', 'path')).toEqual('my/file/path'); + expect(utils.joinPath('my', '', 'path')).toEqual('my/path'); + expect(utils.joinPath('my', 'file/path/123', 'abc')).toEqual('my/file/path/123/abc'); + }); + + it('should handle no-length sections', () => { + expect(utils.joinPath('my', 'file//123', 'abc')).toEqual('my/file/123/abc'); + }); +}); + +describe('mixedQueryToArray', () => { + it('should return undefined if no param', () => { + expect(utils.mixedQueryToArray()).toBeUndefined(); + expect(utils.mixedQueryToArray(null)).toBeUndefined(); + expect(utils.mixedQueryToArray(undefined)).toBeUndefined(); + expect(utils.mixedQueryToArray('')).toBeUndefined(); + expect(utils.mixedQueryToArray(false)).toBeUndefined(); + }); + + it('should return the undefined for an empty array', () => { + expect(utils.mixedQueryToArray([])).toBeUndefined(); + }); + + it('should return a one item array for a single string', () => { + expect(utils.mixedQueryToArray('word')).toEqual(['word']); + expect(utils.mixedQueryToArray('more than than one word word')).toEqual(['more than than one word word']); + expect(utils.mixedQueryToArray(['word'])).toEqual(['word']); + }); + + it('should return an array with the appropriate set when there are multiples', () => { + expect(utils.mixedQueryToArray('there,are,duplicates,here,yes,here,there,is,here')).toEqual(['there', 'are', 'duplicates', 'here', 'yes', 'is']); + }); + + it('should return an array with the appropriate set when there are multiples and spaces', () => { + expect(utils.mixedQueryToArray('there, are, duplicates, here ,yes ,here ,there,is,here ')).toEqual(['there', 'are', 'duplicates', 'here', 'yes', 'is']); + }); + + it('should return an array with the appropriate set when there are multiples and spaces', () => { + expect(utils.mixedQueryToArray(['there', ' are', ' duplicates', ' here ', 'yes ', 'here ', 'there', 'is', 'here '])).toEqual(['there', 'are', 'duplicates', 'here', 'yes', 'is']); + }); +}); + +describe('parseCSV', () => { + it.each([ + // Should return back input if not a string + [undefined, undefined], + [12, 12], + [null, null], + [['a', 'b'], ['a', 'b']], + [{ a: 'a', b: 'b' }, { a: 'a', b: 'b' }], + // Should return an array of split trimmed strings for blanks + [[''], ''], + [['', ''], ' , '], + // Should return an array of split trimmed strings + [['this', 'is', 'a', 'test'], 'this, is , a,test '] + ])('should return %j given %j', (expected, value) => { + expect(utils.parseCSV(value)).toEqual(expected); + }); +}); + +describe('parseIdentityKeyClaims', () => { + beforeAll(() => { + if (jest.isMockFunction(utils.parseIdentityKeyClaims)) { + utils.parseIdentityKeyClaims.mockRestore(); + } + }); + + it('should return array containing just "sub" when no identityKey', () => { + config.has.mockReturnValueOnce(false); // keycloak.identityKey + expect(utils.parseIdentityKeyClaims()).toEqual(['sub']); + }); + + it.each([ + [['foo', 'sub'], 'foo'], + [['foo', 'bar', 'sub'], 'foo,bar'] + ])('should return %j when identityKey is %j', (expected, value) => { + config.has.mockReturnValueOnce(true); // keycloak.identityKey + config.get.mockReturnValueOnce(value); // keycloak.identityKey + expect(utils.parseIdentityKeyClaims()).toEqual(expected); + }); +}); + +describe('streamToBuffer', () => { + it('should reject on a non-stream input', () => { + expect(utils.streamToBuffer()).rejects.toThrow(); + expect(utils.streamToBuffer(123)).rejects.toThrow(); + }); + + it('should return a buffer', () => { + const Readable = require('stream').Readable; + const s = new Readable(); + s._read = () => { }; // redundant? see update below + s.push('your text here'); + s.push(null); + + const result = utils.streamToBuffer(s); + + expect(result).resolves.toBeTruthy(); + expect(result).resolves.toBeInstanceOf(Buffer); + }); +}); + +describe('stripDelimit', () => { + it.each([ + // Should return empty string if falsy input + ['', undefined], + ['', null], + ['', ''], + // Strings without trailing delimiters should remain unchanged + ['1234', '1234'], + ['foo', 'foo'], + ['bar\\', 'bar\\'], + // Strings with trailing delimiters should have the delimiter removed + ['1234', '1234/'], + [' ', ' /'], + ['', '/'], + ['', '//'], + ])('should return %o given %j', (expected, str) => { + expect(utils.stripDelimit(str)).toEqual(expected); + }); +}); + +describe('toLowerKeys', () => { + it.each([ + [undefined, undefined], + [undefined, 1], + [undefined, {}], + [[{ key: 'k1', value: 'V1' }], [{ Key: 'k1', Value: 'V1' }]], + [[{ key: 'k1', value: 'V1' }, { key: 'k2', value: 'V2' }], [{ Key: 'k1', Value: 'V1' }, { Key: 'k2', Value: 'V2' }]] + ])('should return %j given %j', (expected, value) => { + expect(utils.toLowerKeys(value)).toEqual(expected); + }); +}); diff --git a/comsapi/app/tests/unit/controllers/bucket.spec.js b/comsapi/app/tests/unit/controllers/bucket.spec.js new file mode 100644 index 00000000..f33b6b06 --- /dev/null +++ b/comsapi/app/tests/unit/controllers/bucket.spec.js @@ -0,0 +1,411 @@ +const Problem = require('api-problem'); +const { UniqueViolationError } = require('objection'); +const { NIL: SYSTEM_USER } = require('uuid'); + +const controller = require('../../../src/controllers/bucket'); +const { + bucketService, + storageService, + userService, +} = require('../../../src/services'); +const utils = require('../../../src/components/utils'); + +const mockResponse = () => { + const res = {}; + res.status = jest.fn().mockReturnValue(res); + res.json = jest.fn().mockReturnValue(res); + res.end = jest.fn().mockReturnValue(res); + return res; +}; +// Mock config library - @see {@link https://stackoverflow.com/a/64819698} +jest.mock('config'); +// Mock out utils library and use a spy to observe behavior +jest.mock('../../../src/components/utils'); + +let res = undefined; +beforeEach(() => { + res = mockResponse(); +}); + +afterEach(() => { + jest.resetAllMocks(); +}); + +const CURRENT_USER = { authType: 'BEARER' }; +const REQUEST_BUCKET = 'abcxyz'; +const REQUEST_BUCKET_ID = 'ec4ef891-6304-4976-90b8-0f6a6fca6fac'; + +describe('createBucket', () => { + // mock service calls + const createSpy = jest.spyOn(bucketService, 'create'); + const checkGrantPermissionsSpy = jest.spyOn(bucketService, 'checkGrantPermissions'); + const getCurrentIdentitySpy = jest.spyOn(utils, 'getCurrentIdentity'); + const getCurrentUserIdSpy = jest.spyOn(userService, 'getCurrentUserId'); + const headBucketSpy = jest.spyOn(storageService, 'headBucket'); + + const next = jest.fn(); + + it('should return a 201 if all good', async () => { + // request object + const req = { + body: { bucket: REQUEST_BUCKET, region: 'test' }, + currentUser: CURRENT_USER, + headers: {}, + query: {}, + }; + + const USR_IDENTITY = 'xxxy'; + const USR_ID = 'abc-123'; + + createSpy.mockReturnValue(true); + getCurrentIdentitySpy.mockReturnValue(USR_IDENTITY); + getCurrentUserIdSpy.mockReturnValue(USR_ID); + headBucketSpy.mockReturnValue(true); + + await controller.createBucket(req, res, next); + + expect(headBucketSpy).toHaveBeenCalledTimes(1); + expect(headBucketSpy).toHaveBeenCalledWith(expect.objectContaining(req.body)); + expect(getCurrentIdentitySpy).toHaveBeenCalledTimes(1); + expect(getCurrentIdentitySpy).toHaveBeenCalledWith( + CURRENT_USER, + SYSTEM_USER + ); + expect(getCurrentUserIdSpy).toHaveBeenCalledTimes(1); + expect(getCurrentUserIdSpy).toHaveBeenCalledWith(USR_IDENTITY); + expect(createSpy).toHaveBeenCalledTimes(1); + expect(createSpy).toHaveBeenCalledWith({ ...req.body, userId: USR_ID }); + + expect(res.status).toHaveBeenCalledWith(201); + }); + + it('redacts secrets in the response', async () => { + // request object + const req = { + body: { bucket: REQUEST_BUCKET, region: 'test' }, + }; + + const CREATE_RES = { + accessKeyId: 'no no no', + secretAccessKey: 'absolutely not', + other: 'some field', + xyz: 123, + }; + createSpy.mockReturnValue(CREATE_RES); + getCurrentUserIdSpy.mockReturnValue(true); + headBucketSpy.mockReturnValue(true); + + await controller.createBucket(req, res, next); + expect(res.status).toHaveBeenCalledWith(201); + expect(res.json).toHaveBeenCalledWith( + expect.objectContaining({ + accessKeyId: 'REDACTED', + secretAccessKey: 'REDACTED', + other: 'some field', + xyz: 123, + }) + ); + }); + + it('responds with error when invalid bucket head', async () => { + // request object + const req = { + body: { bucket: REQUEST_BUCKET, region: 'test' }, + currentUser: CURRENT_USER, + headers: {}, + query: {}, + }; + + const USR_IDENTITY = 'xxxy'; + const USR_ID = 'abc-123'; + + createSpy.mockReturnValue(true); + getCurrentIdentitySpy.mockReturnValue(USR_IDENTITY); + getCurrentUserIdSpy.mockReturnValue(USR_ID); + headBucketSpy.mockImplementationOnce(() => { + throw new Error(); + }); + + await controller.createBucket(req, res, next); + + expect(headBucketSpy).toHaveBeenCalledTimes(1); + expect(headBucketSpy).toHaveBeenCalledWith(expect.objectContaining(req.body)); + expect(getCurrentIdentitySpy).toHaveBeenCalledTimes(0); + expect(getCurrentUserIdSpy).toHaveBeenCalledTimes(0); + expect(createSpy).toHaveBeenCalledTimes(0); + expect(next).toHaveBeenCalledTimes(1); + expect(next).toHaveBeenCalledWith(expect.any(Problem)); + }); + + it('nexts an error if the bucket service fails to create', async () => { + // request object + const req = { + body: { bucket: REQUEST_BUCKET, region: 'test' }, + currentUser: CURRENT_USER, + headers: {}, + query: {}, + }; + + const USR_IDENTITY = 'xxxy'; + const USR_ID = 'abc-123'; + + createSpy.mockImplementationOnce(() => { + throw new Error(); + }); + getCurrentIdentitySpy.mockReturnValue(USR_IDENTITY); + getCurrentUserIdSpy.mockReturnValue(USR_ID); + headBucketSpy.mockReturnValue(true); + + await controller.createBucket(req, res, next); + + expect(headBucketSpy).toHaveBeenCalledTimes(1); + expect(headBucketSpy).toHaveBeenCalledWith(expect.objectContaining(req.body)); + expect(getCurrentIdentitySpy).toHaveBeenCalledTimes(1); + expect(getCurrentIdentitySpy).toHaveBeenCalledWith( + CURRENT_USER, + SYSTEM_USER + ); + expect(getCurrentUserIdSpy).toHaveBeenCalledTimes(1); + expect(getCurrentUserIdSpy).toHaveBeenCalledWith(USR_IDENTITY); + expect(createSpy).toHaveBeenCalledTimes(1); + expect(createSpy).toHaveBeenCalledWith({ ...req.body, userId: USR_ID }); + + expect(next).toHaveBeenCalledTimes(1); + expect(next).toHaveBeenCalledWith(new Problem(500, 'Internal Server Error')); + }); + + // Skipping until someone can figure out the instanceof issue in the catch block + it.skip('handles unique violation errors if perms are ok', async () => { + // request object + const req = { + body: { bucket: REQUEST_BUCKET, region: 'test' }, + currentUser: CURRENT_USER, + headers: {}, + query: {}, + }; + + const USR_IDENTITY = 'xxxy'; + const USR_ID = 'abc-123'; + + createSpy.mockImplementationOnce(() => { + // This doesn't seem to work for me arghh + // See the `if (e instanceof UniqueViolationError) {` + // part of the catch block in the createBucket methdod + throw new UniqueViolationError(); + }); + checkGrantPermissionsSpy.mockReturnValue(true); + getCurrentIdentitySpy.mockReturnValue(USR_IDENTITY); + getCurrentUserIdSpy.mockReturnValue(USR_ID); + headBucketSpy.mockReturnValue(true); + + await controller.createBucket(req, res, next); + + expect(headBucketSpy).toHaveBeenCalledTimes(1); + expect(headBucketSpy).toHaveBeenCalledWith(expect.objectContaining(req.body)); + expect(getCurrentIdentitySpy).toHaveBeenCalledTimes(1); + expect(getCurrentIdentitySpy).toHaveBeenCalledWith( + CURRENT_USER, + SYSTEM_USER + ); + expect(getCurrentUserIdSpy).toHaveBeenCalledTimes(1); + expect(getCurrentUserIdSpy).toHaveBeenCalledWith(USR_IDENTITY); + expect(createSpy).toHaveBeenCalledTimes(1); + expect(createSpy).toHaveBeenCalledWith({ ...req.body, userId: USR_ID }); + expect(checkGrantPermissionsSpy).toHaveBeenCalledTimes(1); + expect(checkGrantPermissionsSpy).toHaveBeenCalledWith(expect.objectContaining({ ...req.body, userId: USR_ID })); + + expect(res.status).toHaveBeenCalledWith(201); + }); +}); + +describe('deleteBucket', () => { + // mock service calls + const addDashesToUuidSpy = jest.spyOn(utils, 'addDashesToUuid'); + const deleteSpy = jest.spyOn(bucketService, 'delete'); + + const next = jest.fn(); + + it('should return a 204 if all good', async () => { + // request object + const req = { + params: { bucketId: REQUEST_BUCKET_ID }, + headers: {}, + query: {}, + }; + + addDashesToUuidSpy.mockReturnValue(REQUEST_BUCKET_ID); + deleteSpy.mockReturnValue(true); + + await controller.deleteBucket(req, res, next); + + expect(addDashesToUuidSpy).toHaveBeenCalledTimes(1); + expect(addDashesToUuidSpy).toHaveBeenCalledWith(REQUEST_BUCKET_ID); + expect(deleteSpy).toHaveBeenCalledWith(REQUEST_BUCKET_ID); + + expect(res.status).toHaveBeenCalledWith(204); + }); + + it('return a problem if bucket service breaks', async () => { + // request object + const req = { + params: { bucketId: REQUEST_BUCKET_ID }, + headers: {}, + query: {}, + }; + + addDashesToUuidSpy.mockReturnValue(REQUEST_BUCKET_ID); + deleteSpy.mockImplementationOnce(() => { + throw new Problem(502, 'Unknown BucketService Error'); + }); + + await controller.deleteBucket(req, res, next); + + expect(addDashesToUuidSpy).toHaveBeenCalledTimes(1); + expect(addDashesToUuidSpy).toHaveBeenCalledWith(REQUEST_BUCKET_ID); + expect(deleteSpy).toHaveBeenCalledWith(REQUEST_BUCKET_ID); + + expect(next).toHaveBeenCalledTimes(1); + expect(next).toHaveBeenCalledWith( + new Problem(502, 'Unknown BucketService Error') + ); + }); +}); + +describe('headBucket', () => { + // mock service calls + const addDashesToUuidSpy = jest.spyOn(utils, 'addDashesToUuid'); + const headBucketSpy = jest.spyOn(storageService, 'headBucket'); + + const next = jest.fn(); + + it('should return a 204 if all good', async () => { + // request object + const req = { + params: { bucketId: REQUEST_BUCKET_ID }, + headers: {}, + query: {}, + }; + + addDashesToUuidSpy.mockReturnValue(REQUEST_BUCKET_ID); + headBucketSpy.mockReturnValue(true); + + await controller.headBucket(req, res, next); + + expect(addDashesToUuidSpy).toHaveBeenCalledTimes(1); + expect(addDashesToUuidSpy).toHaveBeenCalledWith(REQUEST_BUCKET_ID); + expect(headBucketSpy).toHaveBeenCalledWith({ + bucketId: REQUEST_BUCKET_ID, + }); + + expect(res.status).toHaveBeenCalledWith(204); + }); + + it('return a problem if storage service breaks', async () => { + // request object + const req = { + params: { bucketId: REQUEST_BUCKET_ID }, + headers: {}, + query: {}, + }; + + addDashesToUuidSpy.mockReturnValue(REQUEST_BUCKET_ID); + headBucketSpy.mockImplementationOnce(() => { + throw new Problem(502, 'Unknown BucketService Error'); + }); + + await controller.headBucket(req, res, next); + + expect(addDashesToUuidSpy).toHaveBeenCalledTimes(1); + expect(addDashesToUuidSpy).toHaveBeenCalledWith(REQUEST_BUCKET_ID); + expect(headBucketSpy).toHaveBeenCalledWith({ + bucketId: REQUEST_BUCKET_ID, + }); + + expect(next).toHaveBeenCalledTimes(1); + expect(next).toHaveBeenCalledWith( + new Problem(502, 'Unknown BucketService Error') + ); + }); +}); + +describe('readBucket', () => { + // mock service calls + const addDashesToUuidSpy = jest.spyOn(utils, 'addDashesToUuid'); + const readSpy = jest.spyOn(bucketService, 'read'); + + const next = jest.fn(); + + it('should return a 204 if all good', async () => { + // request object + const req = { + params: { bucketId: REQUEST_BUCKET_ID }, + headers: {}, + query: {}, + }; + + addDashesToUuidSpy.mockReturnValue(REQUEST_BUCKET_ID); + readSpy.mockReturnValue(true); + + await controller.readBucket(req, res, next); + + expect(addDashesToUuidSpy).toHaveBeenCalledTimes(1); + expect(addDashesToUuidSpy).toHaveBeenCalledWith(REQUEST_BUCKET_ID); + expect(readSpy).toHaveBeenCalledWith(REQUEST_BUCKET_ID); + + expect(res.status).toHaveBeenCalledWith(200); + }); + + it('return a problem if storage service breaks', async () => { + // request object + const req = { + params: { bucketId: REQUEST_BUCKET_ID }, + headers: {}, + query: {}, + }; + + addDashesToUuidSpy.mockReturnValue(REQUEST_BUCKET_ID); + readSpy.mockImplementationOnce(() => { + throw new Problem(502, 'Unknown BucketService Error'); + }); + + await controller.readBucket(req, res, next); + + expect(addDashesToUuidSpy).toHaveBeenCalledTimes(1); + expect(addDashesToUuidSpy).toHaveBeenCalledWith(REQUEST_BUCKET_ID); + expect(readSpy).toHaveBeenCalledWith(REQUEST_BUCKET_ID); + + expect(next).toHaveBeenCalledTimes(1); + expect(next).toHaveBeenCalledWith( + new Problem(502, 'Unknown BucketService Error') + ); + }); + + it('redacts secrets in the response', async () => { + // request object + const req = { + params: { bucketId: REQUEST_BUCKET_ID }, + headers: {}, + query: {}, + }; + + const READ_RES = { + accessKeyId: 'no no no', + secretAccessKey: 'absolutely not', + other: 'some field', + xyz: 123, + }; + readSpy.mockReturnValue(READ_RES); + + await controller.readBucket(req, res, next); + expect(res.status).toHaveBeenCalledWith(200); + expect(res.json).toHaveBeenCalledWith( + expect.objectContaining({ + accessKeyId: 'REDACTED', + secretAccessKey: 'REDACTED', + other: 'some field', + xyz: 123, + }) + ); + }); + +}); diff --git a/comsapi/app/tests/unit/controllers/metadata.spec.js b/comsapi/app/tests/unit/controllers/metadata.spec.js new file mode 100644 index 00000000..78854b97 --- /dev/null +++ b/comsapi/app/tests/unit/controllers/metadata.spec.js @@ -0,0 +1,78 @@ +const controller = require('../../../src/controllers/metadata'); +const { metadataService } = require('../../../src/services'); + +const mockResponse = () => { + const res = {}; + res.status = jest.fn().mockReturnValue(res); + res.json = jest.fn().mockReturnValue(res); + res.end = jest.fn().mockReturnValue(res); + return res; +}; +// Mock config library - @see {@link https://stackoverflow.com/a/64819698} +jest.mock('config'); + +let res = undefined; +beforeEach(() => { + res = mockResponse(); +}); + +afterEach(() => { + jest.resetAllMocks(); +}); + +describe('searchMetadata', () => { + // mock service calls + const metadataSearchMetadataSpy = jest.spyOn(metadataService, 'searchMetadata'); + const next = jest.fn(); + + it('should return all metadata with no params', async () => { + // request object + const req = { + currentUser: { authType: 'BEARER' }, + headers: {} + }; + + const GoodResponse = [{ + key: 'foo', + value: 'bar' + }, + { + key: 'baz', + value: 'quz' + }]; + + metadataSearchMetadataSpy.mockReturnValue(GoodResponse); + + await controller.searchMetadata(req, res, next); + + expect(metadataSearchMetadataSpy).toHaveBeenCalledWith({ + metadata: undefined, + }); + + expect(res.json).toHaveBeenCalledWith(GoodResponse); + expect(res.status).toHaveBeenCalledWith(200); + }); + + it('should return only matching metadata', async () => { + // request object + const req = { + currentUser: { authType: 'BEARER' }, + headers: { 'x-amz-meta-foo': '' } + }; + + const GoodResponse = [{ + key: 'foo', + value: 'bar' + }]; + + metadataSearchMetadataSpy.mockReturnValue(GoodResponse); + + await controller.searchMetadata(req, res, next); + + expect(metadataSearchMetadataSpy).toHaveBeenCalledWith({ + metadata: { foo: '' }, + }); + expect(res.json).toHaveBeenCalledWith(GoodResponse); + expect(res.status).toHaveBeenCalledWith(200); + }); +}); diff --git a/comsapi/app/tests/unit/controllers/object.spec.js b/comsapi/app/tests/unit/controllers/object.spec.js new file mode 100644 index 00000000..49f06b06 --- /dev/null +++ b/comsapi/app/tests/unit/controllers/object.spec.js @@ -0,0 +1,619 @@ +const Problem = require('api-problem'); +const { MAXCOPYOBJECTLENGTH, MetadataDirective, TaggingDirective } = require('../../../src/components/constants'); + +const utils = require('../../../src/db/models/utils'); + +const controller = require('../../../src/controllers/object'); +const { storageService, objectService, metadataService, tagService, versionService, userService } = require('../../../src/services'); + +const mockResponse = () => { + const res = {}; + res.status = jest.fn().mockReturnValue(res); + res.json = jest.fn().mockReturnValue(res); + res.end = jest.fn().mockReturnValue(res); + return res; +}; +// Mock config library - @see {@link https://stackoverflow.com/a/64819698} +jest.mock('config'); + +let res = undefined; +beforeEach(() => { + res = mockResponse(); +}); + +afterEach(() => { + jest.resetAllMocks(); +}); + +describe('addMetadata', () => { + // mock service calls + const storageGetObjectTaggingSpy = jest.spyOn(storageService, 'getObjectTagging'); + const storageHeadObjectSpy = jest.spyOn(storageService, 'headObject'); + const storageCopyObjectSpy = jest.spyOn(storageService, 'copyObject'); + const versionCopySpy = jest.spyOn(versionService, 'copy'); + const metadataAssociateMetadataSpy = jest.spyOn(metadataService, 'associateMetadata'); + const tagAssociateTagsSpy = jest.spyOn(tagService, 'associateTags'); + const trxWrapperSpy = jest.spyOn(utils, 'trxWrapper'); + const setHeadersSpy = jest.spyOn(controller, '_processS3Headers'); + + const next = jest.fn(); + + // response from S3 + const GoodResponse = { + ContentLength: 1234, + CopyObjectResultETag: { ETag: '"abcd"' }, + Metadata: { foo: 'bar' }, + VersionId: '5678' + }; + const BadResponse = { + MontentLength: MAXCOPYOBJECTLENGTH + 1 + }; + + it('should error when Content-Length is greater than 5GB', async () => { + // request object + const req = {}; + + storageHeadObjectSpy.mockReturnValue(BadResponse); + await controller.addMetadata(req, res, next); + expect(next).toHaveBeenCalledWith(new Problem(500, 'Internal Server Error')); + }); + + it('should add the metadata', async () => { + // request object + const req = { + currentObject: { + path: 'xyz-789' + }, + headers: { 'x-amz-meta-baz': 'quz' }, + params: { objectId: 'xyz-789' }, + query: {} + }; + + storageHeadObjectSpy.mockResolvedValue(GoodResponse); + storageGetObjectTaggingSpy.mockResolvedValue({ TagSet: [] }); + storageCopyObjectSpy.mockResolvedValue(GoodResponse); + trxWrapperSpy.mockImplementation(callback => callback({})); + versionCopySpy.mockResolvedValue({ id: '5dad1ec9-d3c0-4b0f-8ead-cb4d9fa98987' }); + metadataAssociateMetadataSpy.mockResolvedValue({}); + setHeadersSpy.mockImplementation(x => x); + + await controller.addMetadata(req, res, next); + + expect(storageCopyObjectSpy).toHaveBeenCalledWith({ + copySource: 'xyz-789', + filePath: 'xyz-789', + metadata: { + foo: 'bar', + baz: 'quz' + }, + tags: { + 'coms-id': 'xyz-789' + }, + metadataDirective: MetadataDirective.REPLACE, + taggingDirective: TaggingDirective.REPLACE, + s3VersionId: undefined + }); + + expect(trxWrapperSpy).toHaveBeenCalledTimes(1); + expect(versionCopySpy).toHaveBeenCalledTimes(1); + expect(metadataAssociateMetadataSpy).toHaveBeenCalledTimes(1); + expect(tagAssociateTagsSpy).toHaveBeenCalledTimes(1); + expect(res.status).toHaveBeenCalledWith(204); + }); +}); + +describe('addTags', () => { + // mock service calls + const storageGetObjectTaggingSpy = jest.spyOn(storageService, 'getObjectTagging'); + const storagePutObjectTaggingSpy = jest.spyOn(storageService, 'putObjectTagging'); + const getCurrentUserIdSpy = jest.spyOn(userService, 'getCurrentUserId'); + + + const next = jest.fn(); + + it('should add the new tags', async () => { + // response from S3 + const getObjectTaggingResponse = { TagSet: []}; + + // request object + const req = { + currentObject: { bucketId: 'bid-123', path: 'xyz-789' }, + params: { objectId: 'xyz-789' }, + query: { + tagset: { foo: 'bar', baz: 'bam' }, + s3VersionId: '123' + } + }; + getCurrentUserIdSpy.mockReturnValue('user-123'); + storageGetObjectTaggingSpy.mockResolvedValue(getObjectTaggingResponse); + storagePutObjectTaggingSpy.mockResolvedValue({}); + + await controller.addTags(req, res, next); + + expect(res.status).toHaveBeenCalledWith(204); + expect(storagePutObjectTaggingSpy).toHaveBeenCalledWith({ + bucketId: 'bid-123', + filePath: 'xyz-789', + tags: [ + { Key: 'foo', Value: 'bar' }, + { Key: 'baz', Value: 'bam' }, + { Key: 'coms-id', Value: 'xyz-789' } + ], + s3VersionId: '123' + }); + }); + + // TODO: enable this test after a re-factor error reporting + it.skip('responds 409 when total tags is greater than 10', async () => { + // response from S3 + const getObjectTaggingResponse = { + TagSet: [ + { Key: 'coms-id', Value: 'xyz-789' } + ] + }; + + // request object + const req = { + params: { objectId: 'xyz-789' }, + query: { + tagset: { a: '1', b: '2', c: '3', d: '4', e: '5', f: '6', g: '7', h: '8', i: '9', j: '10'} + } + }; + + storageGetObjectTaggingSpy.mockResolvedValue(getObjectTaggingResponse); + + await controller.addTags(req, res, next); + + // expect(next).toHaveReturned(new Problem(422, 'User-defined Tag count limit is 9')); + }); + + it('should concatenate the new tags', async () => { + // response from S3 + const getObjectTaggingResponse = { + TagSet: [{ Key: 'abc', Value: '123' }] + }; + + // request object + const req = { + currentObject: { bucketId: 'bid-123', path: 'xyz-789' }, + params: { objectId: 'xyz-789' }, + query: { + tagset: { foo: 'bar', baz: 'bam' } + } + }; + + storageGetObjectTaggingSpy.mockResolvedValue(getObjectTaggingResponse); + storagePutObjectTaggingSpy.mockResolvedValue({}); + + await controller.addTags(req, res, next); + + expect(res.status).toHaveBeenCalledWith(204); + expect(storagePutObjectTaggingSpy).toHaveBeenCalledWith({ + bucketId: 'bid-123', + filePath: 'xyz-789', + tags: [ + { Key: 'foo', Value: 'bar' }, + { Key: 'baz', Value: 'bam' }, + { Key: 'abc', Value: '123' }, + { Key: 'coms-id', Value: 'xyz-789' } + ], + s3VersionId: undefined + }); + }); +}); + +describe('deleteMetadata', () => { + // mock service calls + const storageHeadObjectSpy = jest.spyOn(storageService, 'headObject'); + const storageCopyObjectSpy = jest.spyOn(storageService, 'copyObject'); + const storageGetObjectTaggingSpy = jest.spyOn(storageService, 'getObjectTagging'); + + const next = jest.fn(); + + // response from S3 + const GoodResponse = { + ContentLength: 1234, + Metadata: { foo: 'bar', baz: 'quz' } + }; + const BadResponse = { + ContentLength: MAXCOPYOBJECTLENGTH + 1 + }; + const getObjectTaggingResponse = { + TagSet: [{ Key: 'abc', Value: '123' }] + }; + + it('should error when Content-Length is greater than 5GB', async () => { + // request object + const req = {}; + + storageHeadObjectSpy.mockReturnValue(BadResponse); + await controller.deleteMetadata(req, res, next); + expect(next).toHaveBeenCalledWith(new Problem(500, 'Internal Server Error')); + }); + + it('should delete the requested metadata', async () => { + // request object + const req = { + currentObject: { bucketId: 'bid-123', path: 'xyz-789' }, + headers: { 'x-amz-meta-foo': 'bar' }, + params: { objectId: 'xyz-789' }, + query: {} + }; + + storageHeadObjectSpy.mockReturnValue(GoodResponse); + storageCopyObjectSpy.mockReturnValue({}); + storageGetObjectTaggingSpy.mockResolvedValue(getObjectTaggingResponse); + + await controller.deleteMetadata(req, res, next); + + expect(res.status).toHaveBeenCalledWith(204); + expect(storageCopyObjectSpy).toHaveBeenCalledWith({ + bucketId: 'bid-123', + copySource: 'xyz-789', + filePath: 'xyz-789', + metadata: { + baz: 'quz' + }, + metadataDirective: MetadataDirective.REPLACE, + taggingDirective: TaggingDirective.REPLACE, + tags: { + abc: '123', + 'coms-id': 'xyz-789' + }, + s3VersionId: undefined + }); + }); + + it('should delete all the metadata when none provided', async () => { + // request object + const req = { + currentObject: { bucketId: 'bid-123', path: 'xyz-789' }, + headers: {}, + params: { objectId: 'xyz-789' }, + query: {} + }; + + storageHeadObjectSpy.mockReturnValue(GoodResponse); + storageCopyObjectSpy.mockReturnValue({}); + storageGetObjectTaggingSpy.mockResolvedValue(getObjectTaggingResponse); + + await controller.deleteMetadata(req, res, next); + + expect(res.status).toHaveBeenCalledWith(204); + expect(storageCopyObjectSpy).toHaveBeenCalledWith({ + bucketId: 'bid-123', + copySource: 'xyz-789', + filePath: 'xyz-789', + metadata: undefined, + metadataDirective: MetadataDirective.REPLACE, + taggingDirective: TaggingDirective.REPLACE, + tags: { + abc: '123', + 'coms-id': 'xyz-789' + }, + s3VersionId: undefined + }); + }); +}); + +describe('deleteObject', () => { + // mock service calls + const getCurrentUserIdSpy = jest.spyOn(userService, 'getCurrentUserId'); + const storageDeleteObjectSpy = jest.spyOn(storageService, 'deleteObject'); + const objectDeleteSpy = jest.spyOn(objectService, 'delete'); + const versionCreateSpy = jest.spyOn(versionService, 'create'); + const versionDeleteSpy = jest.spyOn(versionService, 'delete'); + const versionListSpy = jest.spyOn(versionService, 'list'); + const pruneOrphanedMetadataSpy = jest.spyOn(metadataService, 'pruneOrphanedMetadata'); + const pruneOrphanedTagsSpy = jest.spyOn(tagService, 'pruneOrphanedTags'); + + // request object + const req = { + params: { objectId: 'xyz-789' }, + }; + const next = jest.fn(); + + // response from S3 + const DeleteMarker = { + DeleteMarker: true, + VersionId: '1234' + }; + + it('should call version service to create a delete marker in db', async () => { + // request is to delete an object (no s3VersionId query parameter passed) + req.query = {}; + getCurrentUserIdSpy.mockReturnValue('user-123'); + // storage response is a DeleteMarker + storageDeleteObjectSpy.mockReturnValue(DeleteMarker); + + await controller.deleteObject(req, res, next); + + expect(versionCreateSpy).toHaveBeenCalledTimes(1); + expect(versionCreateSpy).toHaveBeenCalledWith({ + id: 'xyz-789', + isLatest: true, + deleteMarker: true, + s3VersionId: '1234', + }, 'user-123'); + }); + + it('should delete object if versioning not enabled', async () => { + req.query = {}; + // storage response has no version properties + storageDeleteObjectSpy.mockReturnValue({}); + + await controller.deleteObject(req, res, next); + + expect(versionCreateSpy).toHaveBeenCalledTimes(0); + expect(objectDeleteSpy).toHaveBeenCalledTimes(1); + expect(objectDeleteSpy).toHaveBeenCalledWith('xyz-789'); + }); + + it('should return the storage service response', async () => { + req.query = {}; + storageDeleteObjectSpy.mockReturnValue(DeleteMarker); + versionCreateSpy.mockReturnValue({}); + + await controller.deleteObject(req, res, next); + + expect(res.status).toHaveBeenCalledWith(200); + expect(res.json).toHaveBeenCalledWith(DeleteMarker); + expect(next).toHaveBeenCalledTimes(0); + }); + + it('should call version service to delete a version', async () => { + // version delete request includes s3VersionId query param + req.query = { s3VersionId: '123' }; + getCurrentUserIdSpy.mockResolvedValue('456'); + // S3 returns version that was deleted + storageDeleteObjectSpy.mockReturnValue({ + VersionId: '123' + }); + + await controller.deleteObject(req, res, next); + expect(versionDeleteSpy).toHaveBeenCalledTimes(1); + expect(versionDeleteSpy).toHaveBeenCalledWith('xyz-789', '123'); + }); + + it('should delete object if object has no other remaining versions', async () => { + req.query = { s3VersionId: '123' }; + getCurrentUserIdSpy.mockReturnValue('user-123'); + storageDeleteObjectSpy.mockReturnValue({ + VersionId: '123' + }); + versionDeleteSpy.mockReturnValue({}); + pruneOrphanedMetadataSpy.mockReturnValue({}); + pruneOrphanedTagsSpy.mockReturnValue({}); + // list all versions returns empty array + versionListSpy.mockReturnValue([]); + + await controller.deleteObject(req, res, next); + + expect(versionListSpy).toHaveBeenCalledTimes(1); + expect(objectDeleteSpy).toHaveBeenCalledTimes(1); + expect(objectDeleteSpy).toHaveBeenCalledWith('xyz-789'); + }); + + it('should return a problem if an exception happens', async () => { + storageDeleteObjectSpy.mockImplementationOnce(() => { throw new Error(); }); + + await controller.deleteObject(req, res, next); + expect(storageDeleteObjectSpy).toHaveBeenCalledTimes(1); + expect(next).toHaveBeenCalledTimes(1); + expect(next).toHaveBeenCalledWith(new Problem(500, 'Internal Server Error')); + }); +}); + +describe('deleteTags', () => { + // mock service calls + const storageGetObjectTaggingSpy = jest.spyOn(storageService, 'getObjectTagging'); + const storagePutObjectTaggingSpy = jest.spyOn(storageService, 'putObjectTagging'); + const storageDeleteObjectTaggingSpy = jest.spyOn(storageService, 'deleteObjectTagging'); + + const next = jest.fn(); + + it('should delete all tags when no query keys are present', async () => { + // response from S3 + const getObjectTaggingResponse = {}; + + // request object + const req = { + currentObject: { bucketId: 'bid-123', path: 'xyz-789' }, + params: { objectId: 'xyz-789' }, + query: { foo: 'bar', baz: 'bam' } + }; + + storageGetObjectTaggingSpy.mockReturnValue(getObjectTaggingResponse); + storagePutObjectTaggingSpy.mockResolvedValue({}); + + await controller.deleteTags(req, res, next); + + expect(res.status).toHaveBeenCalledWith(204); + expect(storageDeleteObjectTaggingSpy).toHaveBeenCalledTimes(0); + expect(storagePutObjectTaggingSpy).toHaveBeenCalledTimes(1); + expect(storagePutObjectTaggingSpy).toHaveBeenCalledWith({ + bucketId: 'bid-123', + filePath: 'xyz-789', + tags: [{ Key: 'coms-id', Value: 'xyz-789' }], + s3VersionId: undefined + }); + }); + + it('should delete the requested tags', async () => { + // response from S3 + const getObjectTaggingResponse = { + TagSet: [ + { Key: 'foo', Value: 'bar' }, + { Key: 'baz', Value: 'bam' }, + { Key: 'abc', Value: '123' }] + }; + + // request object + const req = { + currentObject: { bucketId: 'bid-123', path: 'xyz-789' }, + params: { objectId: 'xyz-789' }, + query: { + tagset: { foo: '', baz: '' } + } + }; + + storageGetObjectTaggingSpy.mockReturnValue(getObjectTaggingResponse); + storagePutObjectTaggingSpy.mockResolvedValue({}); + + await controller.deleteTags(req, res, next); + + expect(res.status).toHaveBeenCalledWith(204); + expect(storagePutObjectTaggingSpy).toHaveBeenCalledWith({ + bucketId: 'bid-123', + filePath: 'xyz-789', + tags: [ + { Key: 'abc', Value: '123' }, + { Key: 'coms-id', Value: 'xyz-789' } + ], + s3VersionId: undefined + }); + expect(storageDeleteObjectTaggingSpy).toHaveBeenCalledTimes(0); + }); +}); + +describe('listObjectVersions', () => { + // mock service calls + const versionListSpy = jest.spyOn(versionService, 'list'); + const next = jest.fn(); + // mock request parameters + const req = { + params: { objectId: 'abc' }, + }; + + it('should call version Service list', async () => { + versionListSpy.mockReturnValue({}); + + await controller.listObjectVersion(req, res, next); + + expect(versionListSpy).toHaveBeenCalledWith('abc'); + expect(res.status).toHaveBeenCalledWith(200); + }); +}); + +describe('replaceMetadata', () => { + // mock service calls + const storageGetObjectTaggingSpy = jest.spyOn(storageService, 'getObjectTagging'); + const storageHeadObjectSpy = jest.spyOn(storageService, 'headObject'); + const storageCopyObjectSpy = jest.spyOn(storageService, 'copyObject'); + + const next = jest.fn(); + + // response from S3 + const GoodResponse = { + ContentLength: 1234, + Metadata: { 'coms-id': 1, 'coms-name': 'test', foo: 'bar' } + }; + const BadResponse = { + ContentLength: MAXCOPYOBJECTLENGTH + 1 + }; + + it('should error when Content-Length is greater than 5GB', async () => { + // request object + const req = {}; + + storageHeadObjectSpy.mockReturnValue(BadResponse); + await controller.replaceMetadata(req, res, next); + expect(next).toHaveBeenCalledWith(new Problem(500, 'Internal Server Error')); + }); + + it('should replace the metadata', async () => { + // request object + const req = { + currentObject: { bucketId: 'bid-123', path: 'xyz-789' }, + headers: { 'x-amz-meta-baz': 'quz' }, + params: { objectId: 'xyz-789' }, + query: {} + }; + + storageHeadObjectSpy.mockReturnValue(GoodResponse); + storageGetObjectTaggingSpy.mockResolvedValue({ TagSet: [] }); + storageCopyObjectSpy.mockReturnValue({}); + + await controller.replaceMetadata(req, res, next); + + expect(res.status).toHaveBeenCalledWith(204); + expect(storageCopyObjectSpy).toHaveBeenCalledWith({ + bucketId: 'bid-123', + copySource: 'xyz-789', + filePath: 'xyz-789', + metadata: { baz: 'quz' }, + metadataDirective: MetadataDirective.REPLACE, + taggingDirective: TaggingDirective.REPLACE, + tags: { 'coms-id': 'xyz-789' }, + s3VersionId: undefined + }); + }); + + it('should replace replace the name', async () => { + // request object + const req = { + currentObject: { bucketId: 'bid-123', path: 'xyz-789' }, + headers: { 'x-amz-meta-coms-name': 'newName', 'x-amz-meta-baz': 'quz' }, + params: { objectId: 'xyz-789' }, + query: {} + }; + + storageHeadObjectSpy.mockReturnValue(GoodResponse); + storageGetObjectTaggingSpy.mockResolvedValue({ TagSet: [] }); + storageCopyObjectSpy.mockReturnValue({}); + + await controller.replaceMetadata(req, res, next); + + expect(res.status).toHaveBeenCalledWith(204); + expect(storageCopyObjectSpy).toHaveBeenCalledWith({ + bucketId: 'bid-123', + copySource: 'xyz-789', + filePath: 'xyz-789', + metadata: { baz: 'quz', 'coms-name': 'newName' }, + metadataDirective: MetadataDirective.REPLACE, + taggingDirective: TaggingDirective.REPLACE, + tags: { 'coms-id': 'xyz-789' }, + s3VersionId: undefined + }); + }); +}); + +describe('replaceTags', () => { + // mock service calls + const storageGetObjectTaggingSpy = jest.spyOn(storageService, 'getObjectTagging'); + const storagePutObjectTaggingSpy = jest.spyOn(storageService, 'putObjectTagging'); + + const next = jest.fn(); + + it('should add the new tags', async () => { + // response from S3 + const getObjectTaggingResponse = {}; + + // request object + const req = { + currentObject: { bucketId: 'bid-123', path: 'xyz-789' }, + params: { objectId: 'xyz-789' }, + query: { + tagset: { foo: 'bar', baz: 'bam' } + } + }; + + storageGetObjectTaggingSpy.mockReturnValue(getObjectTaggingResponse); + storagePutObjectTaggingSpy.mockReturnValue({}); + + await controller.replaceTags(req, res, next); + + expect(res.status).toHaveBeenCalledWith(204); + expect(storagePutObjectTaggingSpy).toHaveBeenCalledWith({ + bucketId: 'bid-123', + filePath: 'xyz-789', + tags: [ + { Key: 'foo', Value: 'bar' }, + { Key: 'baz', Value: 'bam' }, + { Key: 'coms-id', Value: 'xyz-789' } + ], + s3VersionId: undefined + }); + }); +}); + diff --git a/comsapi/app/tests/unit/controllers/objectPermission.spec.js b/comsapi/app/tests/unit/controllers/objectPermission.spec.js new file mode 100644 index 00000000..40001770 --- /dev/null +++ b/comsapi/app/tests/unit/controllers/objectPermission.spec.js @@ -0,0 +1,161 @@ +const Problem = require('api-problem'); + +const controller = require('../../../src/controllers/objectPermission'); +const { objectPermissionService, userService } = require('../../../src/services'); +const utils = require('../../../src/components/utils'); + +const mockResponse = () => { + const res = {}; + res.status = jest.fn().mockReturnValue(res); + res.json = jest.fn().mockReturnValue(res); + return res; +}; + +// Mock config library - @see {@link https://stackoverflow.com/a/64819698} +jest.mock('config'); + +describe('searchPermissions', () => { + afterEach(() => { + jest.resetAllMocks(); + }); + + const searchPermissionsSpy = jest.spyOn(objectPermissionService, 'searchPermissions'); + const groupByObjectSpy = jest.spyOn(utils, 'groupByObject'); + + const req = { + query: { bucketId: 'abc', objectId: 'xyz-789', userId: 'oid-1d', permCode: 'pc' } + }; + const next = jest.fn(); + + it('should return the permission service searchPermissions result', async () => { + searchPermissionsSpy.mockReturnValue({ res: 123 }); + groupByObjectSpy.mockReturnValue([]); + + const res = mockResponse(); + await controller.searchPermissions(req, res, next); + expect(searchPermissionsSpy).toHaveBeenCalledTimes(1); + expect(searchPermissionsSpy).toHaveBeenCalledWith({ bucketId: [req.query.bucketId], objId: [req.query.objectId], userId: [req.query.userId], permCode: [req.query.permCode] }); + expect(res.status).toHaveBeenCalledWith(200); + expect(res.json).toHaveBeenCalledWith([]); + expect(next).toHaveBeenCalledTimes(0); + }); + + it('should return a problem if an exception happens', async () => { + searchPermissionsSpy.mockImplementationOnce(() => { throw new Error(); }); + + const res = mockResponse(); + await controller.searchPermissions(req, res, next); + expect(searchPermissionsSpy).toHaveBeenCalledTimes(1); + expect(next).toHaveBeenCalledTimes(1); + expect(next).toHaveBeenCalledWith(new Problem(500, 'Internal Server Error')); + }); +}); + +describe('listPermissions', () => { + afterEach(() => { + jest.resetAllMocks(); + }); + + const searchPermissionsSpy = jest.spyOn(objectPermissionService, 'searchPermissions'); + + const req = { + params: { objectId: 'xyz-789' }, + query: { userId: 'oid-1d', permCode: 'pc' } + }; + const next = jest.fn(); + + it('should return the permission service listPermissions result', async () => { + searchPermissionsSpy.mockReturnValue({ res: 123 }); + + const res = mockResponse(); + await controller.listPermissions(req, res, next); + expect(searchPermissionsSpy).toHaveBeenCalledTimes(1); + expect(searchPermissionsSpy).toHaveBeenCalledWith({ objId: req.params.objectId, userId: [req.query.userId], permCode: [req.query.permCode] }); + expect(res.status).toHaveBeenCalledWith(200); + expect(res.json).toHaveBeenCalledWith({ res: 123 }); + expect(next).toHaveBeenCalledTimes(0); + }); + + it('should return a problem if an exception happens', async () => { + searchPermissionsSpy.mockImplementationOnce(() => { throw new Error(); }); + + const res = mockResponse(); + await controller.listPermissions(req, res, next); + expect(searchPermissionsSpy).toHaveBeenCalledTimes(1); + expect(next).toHaveBeenCalledTimes(1); + expect(next).toHaveBeenCalledWith(new Problem(500, 'Internal Server Error')); + }); +}); + +describe('addPermissions', () => { + afterEach(() => { + jest.resetAllMocks(); + }); + + const addPermissionsSpy = jest.spyOn(objectPermissionService, 'addPermissions'); + const getCurrentUserIdSpy = jest.spyOn(userService, 'getCurrentUserId'); + + const req = { + body: ['READ'], + params: { objectId: 'xyz-789' } + }; + const next = jest.fn(); + + it('should return the permission service addPermissions result', async () => { + addPermissionsSpy.mockReturnValue({ res: 123 }); + getCurrentUserIdSpy.mockReturnValue('user-123'); + + const res = mockResponse(); + await controller.addPermissions(req, res, next); + expect(addPermissionsSpy).toHaveBeenCalledTimes(1); + expect(addPermissionsSpy).toHaveBeenCalledWith(req.params.objectId, req.body, 'user-123'); + expect(res.status).toHaveBeenCalledWith(201); + expect(res.json).toHaveBeenCalledWith({ res: 123 }); + expect(next).toHaveBeenCalledTimes(0); + }); + + it('should return a problem if an exception happens', async () => { + addPermissionsSpy.mockImplementationOnce(() => { throw new Error(); }); + + const res = mockResponse(); + await controller.addPermissions(req, res, next); + expect(addPermissionsSpy).toHaveBeenCalledTimes(1); + expect(next).toHaveBeenCalledTimes(1); + expect(next).toHaveBeenCalledWith(new Problem(500, 'Internal Server Error')); + }); +}); + +describe('removePermissions', () => { + afterEach(() => { + jest.resetAllMocks(); + }); + + const removePermissionsSpy = jest.spyOn(objectPermissionService, 'removePermissions'); + const req = { + params: { objectId: 'xyz-789' }, + query: { userId: 'oid-1d,oid-2d', permCode: 'pc' } + }; + const next = jest.fn(); + + it('should return the permission service removePermissions result', async () => { + removePermissionsSpy.mockReturnValue({ res: 123 }); + + const res = mockResponse(); + await controller.removePermissions(req, res, next); + expect(removePermissionsSpy).toHaveBeenCalledTimes(1); + expect(removePermissionsSpy).toHaveBeenCalledWith(req.params.objectId, ['oid-1d', 'oid-2d'], [req.query.permCode]); + expect(res.status).toHaveBeenCalledWith(200); + expect(res.json).toHaveBeenCalledWith({ res: 123 }); + expect(next).toHaveBeenCalledTimes(0); + }); + + it('should return a problem if an exception happens', async () => { + removePermissionsSpy.mockImplementationOnce(() => { throw new Error(); }); + + const res = mockResponse(); + await controller.removePermissions(req, res, next); + expect(removePermissionsSpy).toHaveBeenCalledTimes(1); + expect(next).toHaveBeenCalledTimes(1); + expect(next).toHaveBeenCalledWith(new Problem(500, 'Internal Server Error')); + }); +}); diff --git a/comsapi/app/tests/unit/controllers/sync.spec.js b/comsapi/app/tests/unit/controllers/sync.spec.js new file mode 100644 index 00000000..edbaa08e --- /dev/null +++ b/comsapi/app/tests/unit/controllers/sync.spec.js @@ -0,0 +1,136 @@ +const controller = require('../../../src/controllers/sync'); +const { objectService, objectQueueService, storageService } = require('../../../src/services'); + +const mockResponse = () => { + const res = {}; + res.status = jest.fn().mockReturnValue(res); + res.json = jest.fn().mockReturnValue(res); + res.end = jest.fn().mockReturnValue(res); + return res; +}; + +const bucketId = 'bucketId'; +const path = 'path'; + +let res = undefined; + +beforeEach(() => { + res = mockResponse(); +}); + +afterEach(() => { + jest.resetAllMocks(); +}); + +describe('syncBucket', () => { + const enqueueSpy = jest.spyOn(objectQueueService, 'enqueue'); + const listAllObjectVersionsSpy = jest.spyOn(storageService, 'listAllObjectVersions'); + const searchObjectsSpy = jest.spyOn(objectService, 'searchObjects'); + const next = jest.fn(); + + it('should enqueue all objects in a bucket', async () => { + const req = { + params: bucketId + }; + enqueueSpy.mockResolvedValue(1); + listAllObjectVersionsSpy.mockResolvedValue({ + DeleteMarkers: [{ Key: path }], + Versions: [{ Key: path }] + }); + searchObjectsSpy.mockResolvedValue([{ path: path }]); + + await controller.syncBucket(req, res, next); + + expect(enqueueSpy).toHaveBeenCalledTimes(1); + expect(listAllObjectVersionsSpy).toHaveBeenCalledTimes(1); + expect(searchObjectsSpy).toHaveBeenCalledTimes(1); + expect(res.json).toHaveBeenCalledWith(1); + expect(res.status).toHaveBeenCalledWith(202); + expect(next).toHaveBeenCalledTimes(0); + }); + + it('should handle unexpected errors', async () => { + const req = { + params: bucketId + }; + listAllObjectVersionsSpy.mockImplementation(() => { throw new Error('error'); }); + searchObjectsSpy.mockResolvedValue([{ path: path }]); + + await controller.syncBucket(req, res, next); + + expect(enqueueSpy).toHaveBeenCalledTimes(0); + expect(listAllObjectVersionsSpy).toHaveBeenCalledTimes(1); + expect(searchObjectsSpy).toHaveBeenCalledTimes(1); + expect(next).toHaveBeenCalledTimes(1); + }); +}); + +describe('syncObject', () => { + const enqueueSpy = jest.spyOn(objectQueueService, 'enqueue'); + const next = jest.fn(); + + it('should enqueue an object', async () => { + const req = { + currentObject: { + bucketId: bucketId, + path: path + } + }; + enqueueSpy.mockResolvedValue(1); + + await controller.syncObject(req, res, next); + + expect(enqueueSpy).toHaveBeenCalledTimes(1); + expect(enqueueSpy).toHaveBeenCalledWith(expect.objectContaining({ + jobs: expect.arrayContaining([{ bucketId: bucketId, path: path }]) + })); + expect(res.json).toHaveBeenCalledWith(1); + expect(res.status).toHaveBeenCalledWith(202); + expect(next).toHaveBeenCalledTimes(0); + }); + + it('should handle unexpected errors', async () => { + const req = { + currentObject: { + bucketId: bucketId, + path: path + } + }; + enqueueSpy.mockImplementation(() => { throw new Error('error'); }); + + await controller.syncObject(req, res, next); + + expect(enqueueSpy).toHaveBeenCalledTimes(1); + expect(enqueueSpy).toHaveBeenCalledWith(expect.objectContaining({ + jobs: expect.arrayContaining([{ bucketId: bucketId, path: path }]) + })); + expect(next).toHaveBeenCalledTimes(1); + }); +}); + +describe('syncStatus', () => { + const queueSizeSpy = jest.spyOn(objectQueueService, 'queueSize'); + const next = jest.fn(); + + it('should return the current sync queue size', async () => { + const req = {}; + queueSizeSpy.mockResolvedValue(0); + + await controller.syncStatus(req, res, next); + + expect(queueSizeSpy).toHaveBeenCalledTimes(1); + expect(res.json).toHaveBeenCalledWith(0); + expect(res.status).toHaveBeenCalledWith(200); + expect(next).toHaveBeenCalledTimes(0); + }); + + it('should handle unexpected errors', async () => { + const req = {}; + queueSizeSpy.mockImplementation(() => { throw new Error('error'); }); + + await controller.syncStatus(req, res, next); + + expect(queueSizeSpy).toHaveBeenCalledTimes(1); + expect(next).toHaveBeenCalledTimes(1); + }); +}); diff --git a/comsapi/app/tests/unit/controllers/tag.spec.js b/comsapi/app/tests/unit/controllers/tag.spec.js new file mode 100644 index 00000000..27614581 --- /dev/null +++ b/comsapi/app/tests/unit/controllers/tag.spec.js @@ -0,0 +1,86 @@ +const controller = require('../../../src/controllers/tag'); +const { tagService } = require('../../../src/services'); + +const mockResponse = () => { + const res = {}; + res.status = jest.fn().mockReturnValue(res); + res.json = jest.fn().mockReturnValue(res); + res.end = jest.fn().mockReturnValue(res); + return res; +}; +// Mock config library - @see {@link https://stackoverflow.com/a/64819698} +jest.mock('config'); + +let res = undefined; +beforeEach(() => { + res = mockResponse(); +}); + +afterEach(() => { + jest.resetAllMocks(); +}); + + +describe('searchTags', () => { + // mock service calls + const tagSearchTagsSpy = jest.spyOn(tagService, 'searchTags'); + + const next = jest.fn(); + + it('should return all tags with no params', async () => { + // request object + const req = { + currentUser: { authType: 'BEARER' }, + headers: {}, + query: {} + }; + + const GoodResponse = [{ + key: 'foo', + value: 'bar' + }, + { + key: 'baz', + value: 'quz' + }]; + + tagSearchTagsSpy.mockReturnValue(GoodResponse); + + await controller.searchTags(req, res, next); + + expect(tagSearchTagsSpy).toHaveBeenCalledWith({ + tags: undefined, + }); + + expect(res.json).toHaveBeenCalledWith(GoodResponse); + expect(res.status).toHaveBeenCalledWith(200); + }); + + it('should return only matching tags', async () => { + // request object + const req = { + currentUser: { authType: 'BEARER' }, + headers: {}, + query: { + tagset: { + foo: '' + } + } + }; + + const GoodResponse = [{ + key: 'foo', + value: 'bar' + }]; + + tagSearchTagsSpy.mockReturnValue(GoodResponse); + + await controller.searchTags(req, res, next); + + expect(tagSearchTagsSpy).toHaveBeenCalledWith({ + tag: { foo: '' }, + }); + expect(res.json).toHaveBeenCalledWith(GoodResponse); + expect(res.status).toHaveBeenCalledWith(200); + }); +}); diff --git a/comsapi/app/tests/unit/db/models/utils.spec.js b/comsapi/app/tests/unit/db/models/utils.spec.js new file mode 100644 index 00000000..028ed392 --- /dev/null +++ b/comsapi/app/tests/unit/db/models/utils.spec.js @@ -0,0 +1,133 @@ +const { + filterOneOrMany, + filterILike, + inArrayClause, + inArrayFilter, + redactSecrets, + toArray +} = require('../../../../src/db/models/utils'); + +describe('filterOneOrMany', () => { + it('should do nothing if there is no value specified', () => { + const where = jest.fn(); + const whereIn = jest.fn(); + + filterOneOrMany({ where: where, whereIn: whereIn }, undefined, 'column'); + + expect(where).toHaveBeenCalledTimes(0); + expect(whereIn).toHaveBeenCalledTimes(0); + }); + + it('should do a wherein query if value is a non-empty string array', () => { + const where = jest.fn(); + const whereIn = jest.fn(); + + filterOneOrMany({ where: where, whereIn: whereIn }, ['foo'], 'column'); + + expect(where).toHaveBeenCalledTimes(0); + expect(whereIn).toHaveBeenCalledTimes(1); + expect(whereIn).toHaveBeenCalledWith('column', ['foo']); + }); + + it('should do a where query if value is a string', () => { + const where = jest.fn(); + const whereIn = jest.fn(); + + filterOneOrMany({ where: where, whereIn: whereIn }, 'foo', 'column'); + + expect(where).toHaveBeenCalledTimes(1); + expect(where).toHaveBeenCalledWith('column', 'foo'); + expect(whereIn).toHaveBeenCalledTimes(0); + }); +}); + +describe('filterILike', () => { + it('should perform an ilike search on the specified column if there is a value', () => { + const where = jest.fn(); + + filterILike({ where: where }, 'value', 'column'); + + expect(where).toHaveBeenCalledTimes(1); + expect(where).toHaveBeenCalledWith('column', 'ilike', '%value%'); + }); + + it('should do nothing if there is no value specified', () => { + const where = jest.fn(); + + filterILike({ where: where }, undefined, 'column'); + + expect(where).toHaveBeenCalledTimes(0); + }); +}); + +describe('inArrayClause', () => { + it('should return the desired clause for a single values', () => { + const col = 'user'; + const vals = ['1']; + expect(inArrayClause(col, vals)).toEqual('\'1\' = ANY("user")'); + }); + + it('should return the desired clause for multiple values joined with OR', () => { + const col = 'user'; + const vals = ['1', '2', '3']; + expect(inArrayClause(col, vals)).toEqual('\'1\' = ANY("user") or \'2\' = ANY("user") or \'3\' = ANY("user")'); + }); + + it('should return a blank string for a blank array', () => { + const col = 'user'; + const vals = []; + expect(inArrayClause(col, vals)).toEqual(''); + }); +}); + +describe('inArrayFilter', () => { + it('should return the desired clause for multiple values joined with OR', () => { + const col = 'user'; + const vals = ['1', '2', '3']; + expect(inArrayFilter(col, vals)).toEqual('(array_length("user", 1) > 0 and (\'1\' = ANY("user") or \'2\' = ANY("user") or \'3\' = ANY("user")))'); + }); +}); + +describe('redactSecrets', () => { + const data = { + foo: 'foo', + bar: 'bar', + baz: 'baz' + }; + + it('should do nothing if fields is undefined', () => { + expect(redactSecrets(data, undefined)).toEqual(expect.objectContaining(data)); + }); + + it('should do nothing if fields is empty array', () => { + expect(redactSecrets(data, [])).toEqual(expect.objectContaining(data)); + }); + + it('should redact the specified fields if they exist', () => { + expect(redactSecrets(data, ['bar', 'garbage'])) + .toEqual(expect.objectContaining({ ...data, bar: 'REDACTED' })); + }); +}); + +describe('toArray', () => { + it('should return blank array if nothing specified', () => { + expect(toArray()).toEqual([]); + expect(toArray(undefined)).toEqual([]); + expect(toArray(null)).toEqual([]); + expect(toArray(false)).toEqual([]); + }); + + it('should return an array if one is specified', () => { + const arr = ['1', '2', '3']; + expect(toArray(arr)).toEqual(arr); + }); + + it('should return an array with trimmed blank values', () => { + const arr = ['1', '', '3', ' ', '4']; + expect(toArray(arr)).toEqual(['1', '3', '4']); + }); + + it('should convert to an array', () => { + expect(toArray('hello')).toEqual(['hello']); + }); +}); diff --git a/comsapi/app/tests/unit/middleware/authentication.spec.js b/comsapi/app/tests/unit/middleware/authentication.spec.js new file mode 100644 index 00000000..75447a7d --- /dev/null +++ b/comsapi/app/tests/unit/middleware/authentication.spec.js @@ -0,0 +1,251 @@ +const Problem = require('api-problem'); +const config = require('config'); +const jwt = require('jsonwebtoken'); + +const mw = require('../../../src/middleware/authentication'); +const { AuthType } = require('../../../src/components/constants'); +const { userService } = require('../../../src/services'); + +// Mock config library - @see {@link https://stackoverflow.com/a/64819698} +jest.mock('config'); +// We need to create a higher order mock to properly intercept this library +jest.mock('express-basic-auth', () => { + function buildMiddleware() { + return jest.fn(); + } + buildMiddleware.safeCompare = jest.requireActual('express-basic-auth').safeCompare; + return buildMiddleware; +}); + +beforeEach(() => { + jest.resetAllMocks(); +}); + +afterAll(() => { + jest.restoreAllMocks(); +}); + +describe('_basicAuthConfig', () => { + describe('authorizer', () => { + const baduser = 'bad username'; + const badpw = 'bad password'; + const gooduser = 'good username'; + const goodpw = 'good password'; + + beforeEach(() => { + config.get + .mockReturnValueOnce(gooduser) // basicAuth.username + .mockReturnValueOnce(goodpw); // basicAuth.password + }); + + it.each([ + [1, gooduser, goodpw], + [0, baduser, goodpw], + [0, gooduser, badpw], + [0, baduser, badpw] + ])('returns %s with %s and %s', (expected, user, pw) => { + expect(mw._basicAuthConfig.authorizer(user, pw)).toBe(expected); + expect(config.get).toHaveBeenCalledTimes(2); + expect(config.get).toHaveBeenNthCalledWith(1, 'basicAuth.username'); + expect(config.get).toHaveBeenNthCalledWith(2, 'basicAuth.password'); + }); + }); + + describe('unauthorizedResponse', () => { + it('returns a problem', () => { + const result = mw._basicAuthConfig.unauthorizedResponse(); + + expect(result).toBeTruthy(); + expect(result).toBeInstanceOf(Problem); + expect(result.status).toEqual(401); + expect(result.detail).toMatch('Invalid authorization credentials'); + }); + }); +}); + +describe('_checkBasicAuth', () => { + it('is a function', () => { + expect(mw._checkBasicAuth).toBeTruthy(); + expect(typeof mw._checkBasicAuth).toBe('function'); + }); +}); + +describe('_spkiWrapper', () => { + it('returns the PEM format we expect', () => { + const spki = 'MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA4CcG7WPTCF4YLHxT3bs9ilcQ6SS+A2e/PiZ9hqR0noelBCsdW0SQGOhjE7nhl2lrZ0W/o80YKMzNZ42Hmc7p0sHU3RN95OCTHvyCazC/CKM2i+gD+cAspP/Ns+hOqNmxC/XIsgD3bZ2zobNMhNy3jgDaAsbs3kOGPIwkdo/vWeo7N6fZPxOgSp6JoGBDtehuyhQ/4y2f7TnyicIvHMuc2d7Bz4GalQ/ra+GspmZ/HqL93A6c8sDHa8fqC8O+gnzpBNsCOxJcq/i3NOaGrOFMCiJwsNVc2dUcY8epcW3pwakIRLlC6D7oawbxv7c3UsXoCt4XSC0hdjwXg5kxVXHoDQIDAQAB'; + + const result = mw._spkiWrapper(spki); + + expect(result).toBeTruthy(); + expect(typeof result).toBe('string'); + expect(result).toEqual(`-----BEGIN PUBLIC KEY-----\n${spki}\n-----END PUBLIC KEY-----`); + }); +}); + +describe('currentUser', () => { + const checkBasicAuthSpy = jest.spyOn(mw, '_checkBasicAuth'); + const jwtVerifySpy = jest.spyOn(jwt, 'verify'); + const loginSpy = jest.spyOn(userService, 'login'); + + let req, res, next; + + beforeEach(() => { + checkBasicAuthSpy.mockImplementation(() => { + return jest.fn(); + }); + + req = { get: jest.fn() }; + res = {}; + next = jest.fn(); + }); + + describe('No Authorization', () => { + it.each([ + [undefined], + [''], + ['garbage'] + ])('sets authType to NONE with authorization header "%s"', (authorization) => { + req.get.mockReturnValueOnce(authorization); + + mw.currentUser(req, res, next); + + expect(req.currentUser).toBeTruthy(); + expect(req.currentUser).toHaveProperty('authType', AuthType.NONE); + expect(req.get).toHaveBeenCalledTimes(1); + expect(req.get).toHaveBeenCalledWith('Authorization'); + expect(checkBasicAuthSpy).toHaveBeenCalledTimes(0); + expect(next).toHaveBeenCalledTimes(1); + expect(next).toHaveBeenCalledWith(); + }); + }); + + describe('Basic Authorization', () => { + it.each([ + ['basic '], + ['basic garbage'], + ['Basic Z29vZCB1c2VybmFtZTpnb29kIHBhc3N3b3Jk'] + ])('sets authType to BASIC with authorization header "%s"', async (authorization) => { + config.has.mockReturnValueOnce(true); // basicAuth.enabled + req.get.mockReturnValueOnce(authorization); + + mw.currentUser(req, res, next); + + expect(req.currentUser).toBeTruthy(); + expect(req.currentUser).toHaveProperty('authType', AuthType.BASIC); + expect(req.get).toHaveBeenCalledTimes(1); + expect(req.get).toHaveBeenCalledWith('Authorization'); + expect(config.has).toHaveBeenCalledTimes(1); + expect(config.has).toHaveBeenNthCalledWith(1, 'basicAuth.enabled'); + expect(checkBasicAuthSpy).toHaveBeenCalledTimes(1); + expect(checkBasicAuthSpy).toHaveBeenCalledWith(req, res, next); + expect(next).toHaveBeenCalledTimes(0); + }); + }); + + describe('OIDC Authorization', () => { + const authorization = 'bearer '; + const serverUrl = 'serverUrl'; + const realm = 'realm'; + const spki = 'SOMESPKI'; + const publicKey = `-----BEGIN PUBLIC KEY-----\n${spki}\n-----END PUBLIC KEY-----`; + + it.each([ + ['SPKI', spki], + ['PEM', publicKey] + ])('sets authType to BEARER with keycloak.publicKey %s and valid auth token', async (_desc, pkey) => { + jwtVerifySpy.mockReturnValue({ sub: 'sub' }); // return truthy value + loginSpy.mockImplementation(() => { }); + config.has + .mockReturnValueOnce(false) // basicAuth.enabled + .mockReturnValueOnce(true) // keycloak.enabled + .mockReturnValueOnce(true); // keycloak.publicKey + config.get + .mockReturnValueOnce(pkey) // keycloak.publicKey + .mockReturnValueOnce(serverUrl) // keycloak.serverUrl + .mockReturnValueOnce(realm); // keycloak.realm + req.get.mockReturnValueOnce(authorization); + + await mw.currentUser(req, res, next); + + expect(req.currentUser).toBeTruthy(); + expect(req.currentUser).toHaveProperty('authType', AuthType.BEARER); + expect(req.currentUser).toHaveProperty('tokenPayload', { sub: 'sub' }); + expect(req.get).toHaveBeenCalledTimes(1); + expect(req.get).toHaveBeenCalledWith('Authorization'); + expect(config.has).toHaveBeenCalledTimes(3); + expect(config.has).toHaveBeenNthCalledWith(1, 'basicAuth.enabled'); + expect(config.has).toHaveBeenNthCalledWith(2, 'keycloak.enabled'); + expect(config.has).toHaveBeenNthCalledWith(3, 'keycloak.publicKey'); + expect(config.get).toHaveBeenCalledTimes(3); + expect(config.get).toHaveBeenNthCalledWith(1, 'keycloak.publicKey'); + expect(config.get).toHaveBeenNthCalledWith(2, 'keycloak.serverUrl'); + expect(config.get).toHaveBeenNthCalledWith(3, 'keycloak.realm'); + expect(checkBasicAuthSpy).toHaveBeenCalledTimes(0); + expect(jwtVerifySpy).toHaveBeenCalledTimes(1); + expect(jwtVerifySpy).toHaveBeenCalledWith(expect.any(String), publicKey, expect.objectContaining({ + issuer: `${serverUrl}/realms/${realm}` + })); + expect(loginSpy).toHaveBeenCalledTimes(1); + expect(loginSpy).toHaveBeenCalledWith(expect.objectContaining({ sub: 'sub' })); + expect(next).toHaveBeenCalledTimes(1); + expect(next).toHaveBeenCalledWith(); + }); + + it('short circuits with invalid auth token', async () => { + const authorization = 'bearer '; + + config.has + .mockReturnValueOnce(false) // basicAuth.enabled + .mockReturnValueOnce(true) // keycloak.enabled + .mockReturnValueOnce(true); // keycloak.publicKey + config.get + .mockReturnValueOnce(spki) // keycloak.publicKey + .mockReturnValueOnce(serverUrl) // keycloak.serverUrl + .mockReturnValueOnce(realm); // keycloak.realm + req.get.mockReturnValueOnce(authorization); + + await mw.currentUser(req, res, next); + + expect(req.currentUser).toBeFalsy(); + expect(req.get).toHaveBeenCalledTimes(1); + expect(req.get).toHaveBeenCalledWith('Authorization'); + expect(config.has).toHaveBeenCalledTimes(3); + expect(config.has).toHaveBeenNthCalledWith(1, 'basicAuth.enabled'); + expect(config.has).toHaveBeenNthCalledWith(2, 'keycloak.enabled'); + expect(config.has).toHaveBeenNthCalledWith(3, 'keycloak.publicKey'); + expect(checkBasicAuthSpy).toHaveBeenCalledTimes(0); + expect(jwtVerifySpy).toHaveBeenCalledTimes(1); + expect(jwtVerifySpy).toHaveBeenCalledWith(expect.any(String), publicKey, expect.objectContaining({ + issuer: `${serverUrl}/realms/${realm}` + })); + expect(loginSpy).toHaveBeenCalledTimes(0); + expect(next).toHaveBeenCalledTimes(1); + expect(next).toHaveBeenCalledWith(expect.any(Object)); + }); + + it('short circuits without keycloak.publicKey', async () => { + jwtVerifySpy.mockReturnValue({ sub: 'sub' }); + loginSpy.mockImplementation(() => { }); + config.has + .mockReturnValueOnce(false) // basicAuth.enabled + .mockReturnValueOnce(true) // keycloak.enabled + .mockReturnValueOnce(false); // keycloak.publicKey + req.get.mockReturnValueOnce(authorization); + + await mw.currentUser(req, res, next); + + expect(req.currentUser).toBeFalsy(); + expect(req.get).toHaveBeenCalledTimes(1); + expect(req.get).toHaveBeenCalledWith('Authorization'); + expect(config.has).toHaveBeenCalledTimes(3); + expect(config.has).toHaveBeenNthCalledWith(1, 'basicAuth.enabled'); + expect(config.has).toHaveBeenNthCalledWith(2, 'keycloak.enabled'); + expect(config.has).toHaveBeenNthCalledWith(3, 'keycloak.publicKey'); + expect(checkBasicAuthSpy).toHaveBeenCalledTimes(0); + expect(jwtVerifySpy).toHaveBeenCalledTimes(0); + expect(loginSpy).toHaveBeenCalledTimes(0); + expect(next).toHaveBeenCalledTimes(1); + expect(next).toHaveBeenCalledWith(expect.any(Object)); + }); + }); +}); diff --git a/comsapi/app/tests/unit/middleware/authorization.spec.js b/comsapi/app/tests/unit/middleware/authorization.spec.js new file mode 100644 index 00000000..5ce5aafc --- /dev/null +++ b/comsapi/app/tests/unit/middleware/authorization.spec.js @@ -0,0 +1,411 @@ +const { NIL: SYSTEM_USER } = require('uuid'); + +const mw = require('../../../src/middleware/authorization'); +const { bucketPermissionService, objectService, objectPermissionService, userService } = require('../../../src/services'); +const { AuthMode, AuthType, Permissions } = require('../../../src/components/constants'); +const utils = require('../../../src/components/utils'); + +// Mock config library - @see {@link https://stackoverflow.com/a/64819698} +jest.mock('config'); +// Mock out utils library and use a spy to observe behavior +jest.mock('../../../src/components/utils'); + +const checkPermissionSpy = jest.spyOn(mw, '_checkPermission'); + +beforeEach(() => { + jest.resetAllMocks(); +}); + +afterAll(() => { + jest.restoreAllMocks(); +}); + +describe('_checkPermission', () => { + const getCurrentIdentitySpy = jest.spyOn(utils, 'getCurrentIdentity'); + const getCurrentUserIdSpy = jest.spyOn(userService, 'getCurrentUserId'); + const bucketSearchPermissionsSpy = jest.spyOn(bucketPermissionService, 'searchPermissions'); + const objSearchPermissionsSpy = jest.spyOn(objectPermissionService, 'searchPermissions'); + + beforeAll(() => { + checkPermissionSpy.mockRestore(); // Run actual function here + }); + + it('should return false when nothing is provided', () => { + getCurrentIdentitySpy.mockReturnValue(SYSTEM_USER); + getCurrentUserIdSpy.mockResolvedValue(SYSTEM_USER); + + const result = mw._checkPermission({}, undefined); + + expect(result).resolves.toBe(false); + expect(getCurrentIdentitySpy).toHaveBeenCalledTimes(1); + expect(getCurrentIdentitySpy).toHaveBeenCalledWith(undefined, SYSTEM_USER); + expect(getCurrentUserIdSpy).toHaveBeenCalledTimes(1); + expect(getCurrentUserIdSpy).toHaveBeenCalledWith(SYSTEM_USER); + expect(bucketSearchPermissionsSpy).toHaveBeenCalledTimes(0); + expect(objSearchPermissionsSpy).toHaveBeenCalledTimes(0); + }); + + it.each([ + [false, Permissions.READ, {}, [], []], + [false, Permissions.READ, { objectId: SYSTEM_USER }, [], []], + [false, Permissions.READ, { objectId: SYSTEM_USER }, [{ permCode: Permissions.UPDATE }], []], + [true, Permissions.READ, { objectId: SYSTEM_USER }, [{ permCode: Permissions.READ }], []], + [true, Permissions.READ, { objectId: SYSTEM_USER }, [{ permCode: Permissions.READ }, { permCode: Permissions.UPDATE }], []], + [false, Permissions.READ, { bucketId: SYSTEM_USER }, [], []], + [false, Permissions.READ, { bucketId: SYSTEM_USER }, [], [{ permCode: Permissions.UPDATE }]], + [true, Permissions.READ, { bucketId: SYSTEM_USER }, [], [{ permCode: Permissions.READ }]], + [true, Permissions.READ, { bucketId: SYSTEM_USER }, [], [{ permCode: Permissions.READ }, { permCode: Permissions.UPDATE }]], + [false, Permissions.READ, { bucketId: SYSTEM_USER, objectId: SYSTEM_USER }, [], []], + [false, Permissions.READ, { bucketId: SYSTEM_USER, objectId: SYSTEM_USER }, [{ permCode: Permissions.UPDATE }], []], + [true, Permissions.READ, { bucketId: SYSTEM_USER, objectId: SYSTEM_USER }, [{ permCode: Permissions.READ }], []], + [true, Permissions.READ, { bucketId: SYSTEM_USER, objectId: SYSTEM_USER }, [{ permCode: Permissions.READ }, { permCode: Permissions.UPDATE }], []], + [false, Permissions.READ, { bucketId: SYSTEM_USER, objectId: SYSTEM_USER }, [{ permCode: Permissions.UPDATE }], [{ permCode: Permissions.UPDATE }]], + [true, Permissions.READ, { bucketId: SYSTEM_USER, objectId: SYSTEM_USER }, [{ permCode: Permissions.UPDATE }], [{ permCode: Permissions.READ }]], + [true, Permissions.READ, { bucketId: SYSTEM_USER, objectId: SYSTEM_USER }, [{ permCode: Permissions.READ }], [{ permCode: Permissions.UPDATE }]], + [true, Permissions.READ, { bucketId: SYSTEM_USER, objectId: SYSTEM_USER }, [{ permCode: Permissions.READ }, { permCode: Permissions.UPDATE }], [{ permCode: Permissions.UPDATE }]], + [true, Permissions.READ, { bucketId: SYSTEM_USER, objectId: SYSTEM_USER }, [{ permCode: Permissions.UPDATE }], [{ permCode: Permissions.READ }, { permCode: Permissions.UPDATE }]], + ])('should return %s given a bucketless object, permission %s, params %j, objPerms %j and bucketPerms %j', async (expected, permission, params, objPerms, bucketPerms) => { + const req = { + currentObject: {}, + currentUser: { authType: AuthType.BEARER }, + params: params + }; + getCurrentIdentitySpy.mockReturnValue(SYSTEM_USER); + getCurrentUserIdSpy.mockResolvedValue(SYSTEM_USER); + bucketSearchPermissionsSpy.mockResolvedValue(bucketPerms); + objSearchPermissionsSpy.mockResolvedValue(objPerms); + + const result = await mw._checkPermission(req, permission); + + expect(result).toBe(expected); + expect(getCurrentIdentitySpy).toHaveBeenCalledTimes(1); + expect(getCurrentIdentitySpy).toHaveBeenCalledWith(req.currentUser, SYSTEM_USER); + expect(getCurrentUserIdSpy).toHaveBeenCalledTimes(1); + expect(getCurrentUserIdSpy).toHaveBeenCalledWith(SYSTEM_USER); + expect(bucketSearchPermissionsSpy).toHaveBeenCalledTimes(params.bucketId ? 1 : 0); + expect(objSearchPermissionsSpy).toHaveBeenCalledTimes(params.objectId ? 1 : 0); + }); + + it.each([ + [false, Permissions.READ, {}, [], []], + [false, Permissions.READ, { objectId: SYSTEM_USER }, [], []], + [false, Permissions.READ, { objectId: SYSTEM_USER }, [{ permCode: Permissions.UPDATE }], []], + [true, Permissions.READ, { objectId: SYSTEM_USER }, [{ permCode: Permissions.READ }], []], + [true, Permissions.READ, { objectId: SYSTEM_USER }, [{ permCode: Permissions.READ }, { permCode: Permissions.UPDATE }], []], + [false, Permissions.READ, { bucketId: SYSTEM_USER }, [], []], + [false, Permissions.READ, { bucketId: SYSTEM_USER }, [], [{ permCode: Permissions.UPDATE }]], + [true, Permissions.READ, { bucketId: SYSTEM_USER }, [], [{ permCode: Permissions.READ }]], + [true, Permissions.READ, { bucketId: SYSTEM_USER }, [], [{ permCode: Permissions.READ }, { permCode: Permissions.UPDATE }]], + [false, Permissions.READ, { bucketId: SYSTEM_USER, objectId: SYSTEM_USER }, [], []], + [false, Permissions.READ, { bucketId: SYSTEM_USER, objectId: SYSTEM_USER }, [{ permCode: Permissions.UPDATE }], []], + [true, Permissions.READ, { bucketId: SYSTEM_USER, objectId: SYSTEM_USER }, [{ permCode: Permissions.READ }], []], + [true, Permissions.READ, { bucketId: SYSTEM_USER, objectId: SYSTEM_USER }, [{ permCode: Permissions.READ }, { permCode: Permissions.UPDATE }], []], + [false, Permissions.READ, { bucketId: SYSTEM_USER, objectId: SYSTEM_USER }, [{ permCode: Permissions.UPDATE }], [{ permCode: Permissions.UPDATE }]], + [true, Permissions.READ, { bucketId: SYSTEM_USER, objectId: SYSTEM_USER }, [{ permCode: Permissions.UPDATE }], [{ permCode: Permissions.READ }]], + [true, Permissions.READ, { bucketId: SYSTEM_USER, objectId: SYSTEM_USER }, [{ permCode: Permissions.READ }], [{ permCode: Permissions.UPDATE }]], + [true, Permissions.READ, { bucketId: SYSTEM_USER, objectId: SYSTEM_USER }, [{ permCode: Permissions.READ }, { permCode: Permissions.UPDATE }], [{ permCode: Permissions.UPDATE }]], + [true, Permissions.READ, { bucketId: SYSTEM_USER, objectId: SYSTEM_USER }, [{ permCode: Permissions.UPDATE }], [{ permCode: Permissions.READ }, { permCode: Permissions.UPDATE }]], + ])('should return %s given a bucketed object, permission %s, params %j, objPerms %j and bucketPerms %j', async (expected, permission, params, objPerms, bucketPerms) => { + const req = { + currentObject: { bucketId: SYSTEM_USER }, + currentUser: { authType: AuthType.BEARER }, + params: params + }; + getCurrentIdentitySpy.mockReturnValue(SYSTEM_USER); + getCurrentUserIdSpy.mockResolvedValue(SYSTEM_USER); + bucketSearchPermissionsSpy.mockResolvedValue(bucketPerms); + objSearchPermissionsSpy.mockResolvedValue(objPerms); + + const result = await mw._checkPermission(req, permission); + + expect(result).toBe(expected); + expect(getCurrentIdentitySpy).toHaveBeenCalledTimes(1); + expect(getCurrentIdentitySpy).toHaveBeenCalledWith(req.currentUser, SYSTEM_USER); + expect(getCurrentUserIdSpy).toHaveBeenCalledTimes(1); + expect(getCurrentUserIdSpy).toHaveBeenCalledWith(SYSTEM_USER); + expect(bucketSearchPermissionsSpy).toHaveBeenCalledTimes(1); + expect(objSearchPermissionsSpy).toHaveBeenCalledTimes(params.objectId ? 1 : 0); + }); +}); + +describe('checkAppMode', () => { + const getAppAuthModeSpy = jest.spyOn(utils, 'getAppAuthMode'); + + let req, res, next; + + beforeEach(() => { + req = {}; + res = {}; + next = jest.fn(); + }); + + it.each([ + [1, AuthMode.NOAUTH, undefined], + [1, AuthMode.NOAUTH, AuthType.NONE], + [1, AuthMode.NOAUTH, AuthType.BASIC], + [1, AuthMode.NOAUTH, AuthType.BEARER], + [1, AuthMode.BASICAUTH, undefined], + [1, AuthMode.BASICAUTH, AuthType.NONE], + [1, AuthMode.BASICAUTH, AuthType.BASIC], + [0, AuthMode.BASICAUTH, AuthType.BEARER], + [1, AuthMode.OIDCAUTH, undefined], + [1, AuthMode.OIDCAUTH, AuthType.NONE], + [0, AuthMode.OIDCAUTH, AuthType.BASIC], + [1, AuthMode.OIDCAUTH, AuthType.BEARER], + [1, AuthMode.FULLAUTH, undefined], + [1, AuthMode.FULLAUTH, AuthType.NONE], + [1, AuthMode.FULLAUTH, AuthType.BASIC], + [1, AuthMode.FULLAUTH, AuthType.BEARER] + ])('should call next %i times given authMode %s and authType %s', (nextCount, mode, type) => { + const sendCount = 1 - nextCount; + getAppAuthModeSpy.mockReturnValue(mode); + if (type) req.currentUser = { authType: type }; + + const result = () => mw.checkAppMode(req, res, next); + if (sendCount) expect(result).toThrow(); + else expect(result).not.toThrow(); + + expect(getAppAuthModeSpy).toHaveBeenCalledTimes(1); + expect(getAppAuthModeSpy).toHaveBeenCalledWith(); + expect(next).toHaveBeenCalledTimes(nextCount); + }); +}); + +describe('currentObject', () => { + const objectReadSpy = jest.spyOn(objectService, 'read'); + + let req, res, next; + + beforeEach(() => { + req = {}; + res = {}; + next = jest.fn(); + }); + + it.each([ + [undefined], + [''] + ])('does not inject any current object to request with objectId %o', (objectId) => { + req.params = { objectId: objectId }; + + mw.currentObject(req, res, next); + + expect(req.currentObject).toBeUndefined(); + expect(objectReadSpy).toHaveBeenCalledTimes(0); + expect(next).toHaveBeenCalledTimes(1); + expect(next).toHaveBeenCalledWith(); + }); + + it('does not inject any current object if an exception happens', () => { + const objectId = '1234'; + req.params = { objectId: objectId }; + objectReadSpy.mockImplementation(() => { throw new Error('test'); }); + + mw.currentObject(req, res, next); + + expect(req.currentObject).toBeUndefined(); + expect(objectReadSpy).toHaveBeenCalledTimes(1); + expect(objectReadSpy).toHaveBeenCalledWith(objectId); + expect(next).toHaveBeenCalledTimes(1); + expect(next).toHaveBeenCalledWith(); + }); + + it('injects the current object based on the service results', async () => { + const objectId = '1234'; + const testRecord = { a: 1 }; + req.params = { objectId: objectId }; + objectReadSpy.mockResolvedValue(testRecord); + + await mw.currentObject(req, res, next); + + expect(req.currentObject).toBeTruthy(); + expect(req.currentObject).toEqual(expect.objectContaining(testRecord)); + expect(objectReadSpy).toHaveBeenCalledTimes(1); + expect(objectReadSpy).toHaveBeenCalledWith(objectId); + expect(next).toHaveBeenCalledTimes(1); + expect(next).toHaveBeenCalledWith(); + }); +}); + +describe('hasPermission', () => { + const getAppAuthModeSpy = jest.spyOn(utils, 'getAppAuthMode'); + const getCurrentIdentitySpy = jest.spyOn(utils, 'getCurrentIdentity'); + const getCurrentUserIdSpy = jest.spyOn(userService, 'getCurrentUserId'); + const objSearchPermissionsSpy = jest.spyOn(objectPermissionService, 'searchPermissions'); + + let req, res, next; + + beforeEach(() => { + req = {}; + res = {}; + next = jest.fn(); + }); + + describe('given no currentObject nor currentUser', () => { + it.each([ + [1, AuthMode.NOAUTH], + [1, AuthMode.BASICAUTH], + [0, AuthMode.OIDCAUTH], + [0, AuthMode.FULLAUTH] + ])('should call next %i times given authMode %s', async (nextCount, mode) => { + getAppAuthModeSpy.mockReturnValue(mode); + + const result = mw.hasPermission(Permissions.READ); + expect(result).toBeInstanceOf(Function); + await result(req, res, next); + + expect(next).toHaveBeenCalledTimes(1); + if (nextCount) expect(next).toHaveBeenCalledWith(); + else expect(next).toHaveBeenCalledWith(expect.any(Object)); + expect(objSearchPermissionsSpy).toHaveBeenCalledTimes(0); + }); + }); + + describe('given a currentObject but no currentUser', () => { + beforeEach(() => { + req.currentObject = {}; + req.params = {}; + }); + + it.each([ + [1, AuthMode.NOAUTH], + [1, AuthMode.BASICAUTH], + [0, AuthMode.OIDCAUTH], + [0, AuthMode.FULLAUTH] + ])('should call next %i times given authMode %s', async (nextCount, mode) => { + getAppAuthModeSpy.mockReturnValue(mode); + getCurrentUserIdSpy.mockResolvedValue(SYSTEM_USER); + getCurrentIdentitySpy.mockReturnValue(SYSTEM_USER); + + const result = mw.hasPermission(Permissions.READ); + expect(result).toBeInstanceOf(Function); + await result(req, res, next); + + expect(next).toHaveBeenCalledTimes(1); + if (nextCount) expect(next).toHaveBeenCalledWith(); + else expect(next).toHaveBeenCalledWith(expect.any(Object)); + expect(objSearchPermissionsSpy).toHaveBeenCalledTimes(0); + }); + }); + + describe('given a currentUser but no currentObject', () => { + beforeEach(() => { + req.currentUser = {}; + }); + + it.each([ + [{}], + [{ bucketId: SYSTEM_USER }], + [{ objectId: SYSTEM_USER }], + [{ bucketId: SYSTEM_USER, objectId: SYSTEM_USER }] + ])('should call next 0 times with params %j', async (params) => { + req.params = params; + getAppAuthModeSpy.mockReturnValue(AuthMode.FULLAUTH); + getCurrentUserIdSpy.mockResolvedValue(SYSTEM_USER); + getCurrentIdentitySpy.mockReturnValue(SYSTEM_USER); + + const result = mw.hasPermission(Permissions.READ); + expect(result).toBeInstanceOf(Function); + await result(req, res, next); + + expect(next).toHaveBeenCalledTimes(1); + expect(next).toHaveBeenCalledWith(expect.any(Object)); + }); + }); + + describe('given a currentObject and currentUser', () => { + beforeEach(() => { + req.currentObject = {}; + req.currentUser = {}; + req.params = { objectId: SYSTEM_USER }; + }); + + it.each([ + [1, AuthMode.NOAUTH], + [1, AuthMode.BASICAUTH], + [0, AuthMode.OIDCAUTH], + [1, AuthMode.FULLAUTH] + ])('should call next %i times when authType BASIC and authMode %s', async (nextCount, mode) => { + getAppAuthModeSpy.mockReturnValue(mode); + checkPermissionSpy.mockResolvedValue(false); + req.currentUser.authType = AuthType.BASIC; + + const result = mw.hasPermission(Permissions.READ); + expect(result).toBeInstanceOf(Function); + await result(req, res, next); + + expect(next).toHaveBeenCalledTimes(1); + if (nextCount) expect(next).toHaveBeenCalledWith(); + else expect(next).toHaveBeenCalledWith(expect.any(Object)); + expect(checkPermissionSpy).toHaveBeenCalledTimes(0); + }); + + it.each([ + [0, false, Permissions.CREATE], + [0, false, Permissions.READ], + [0, false, Permissions.UPDATE], + [0, false, Permissions.DELETE], + [0, false, Permissions.MANAGE], + [0, true, Permissions.CREATE], + [1, true, Permissions.READ], + [0, true, Permissions.UPDATE], + [0, true, Permissions.DELETE], + [0, true, Permissions.MANAGE] + ])('should call next %i times when public %s and permission %s', async (nextCount, isPublic, perm) => { + getAppAuthModeSpy.mockReturnValue(AuthMode.OIDCAUTH); + getCurrentUserIdSpy.mockResolvedValue(SYSTEM_USER); + req.currentUser.authType = AuthType.OIDC; + req.currentObject.public = isPublic; + + const result = mw.hasPermission(perm); + expect(result).toBeInstanceOf(Function); + await result(req, res, next); + + expect(next).toHaveBeenCalledTimes(1); + if (nextCount) expect(next).toHaveBeenCalledWith(); + else expect(next).toHaveBeenCalledWith(expect.any(Object)); + expect(objSearchPermissionsSpy).toHaveBeenCalledTimes(0); + }); + }); + + describe('given currentObject with public false and currentUser', () => { + beforeEach(() => { + req.currentObject = {}; + req.currentUser = {}; + req.params = {}; + }); + + it.each([ + [0, AuthType.NONE, undefined, []], + [0, AuthType.NONE, SYSTEM_USER, []], + [0, AuthType.NONE, SYSTEM_USER, [{ permCode: Permissions.UPDATE }]], + [0, AuthType.NONE, SYSTEM_USER, [{ permCode: Permissions.READ }, { permCode: Permissions.UPDATE }]], + [0, AuthType.BEARER, undefined, []], + [0, AuthType.BEARER, SYSTEM_USER, []], + [0, AuthType.BEARER, SYSTEM_USER, [{ permCode: Permissions.UPDATE }]], + [1, AuthType.BEARER, SYSTEM_USER, [{ permCode: Permissions.READ }, { permCode: Permissions.UPDATE }]] + ])('should call next %i times when authType %s, userId %o and have permissions %j', async (nextCount, type, userId, perms) => { + const searchPermCount = +(type === AuthType.BEARER && !!userId); + getAppAuthModeSpy.mockReturnValue(AuthMode.OIDCAUTH); + getCurrentUserIdSpy.mockResolvedValue(userId); + objSearchPermissionsSpy.mockResolvedValue(perms); + req.currentObject.public = false; + req.currentUser.authType = type; + req.params.objectId = SYSTEM_USER; + + const result = mw.hasPermission(Permissions.READ); + expect(result).toBeInstanceOf(Function); + await result(req, res, next); + + expect(next).toHaveBeenCalledTimes(1); + if (nextCount) expect(next).toHaveBeenCalledWith(); + else expect(next).toHaveBeenCalledWith(expect.any(Object)); + + expect(objSearchPermissionsSpy).toHaveBeenCalledTimes(searchPermCount); + if (searchPermCount) { + expect(objSearchPermissionsSpy).toHaveBeenCalledWith(expect.objectContaining({ objId: SYSTEM_USER })); + expect(objSearchPermissionsSpy).toHaveBeenCalledWith(expect.objectContaining({ userId: userId })); + } + }); + }); +}); diff --git a/comsapi/app/tests/unit/middleware/featureToggle.spec.js b/comsapi/app/tests/unit/middleware/featureToggle.spec.js new file mode 100644 index 00000000..6eb30ac9 --- /dev/null +++ b/comsapi/app/tests/unit/middleware/featureToggle.spec.js @@ -0,0 +1,98 @@ +const { requireBasicAuth, requireSomeAuth } = require('../../../src/middleware/featureToggle'); +const { AuthMode, AuthType } = require('../../../src/components/constants'); +const utils = require('../../../src/components/utils'); + +// Mock config library - @see {@link https://stackoverflow.com/a/64819698} +jest.mock('config'); +// Mock out utils library and use a spy to observe behavior +jest.mock('../../../src/components/utils'); + +beforeEach(() => { + jest.resetAllMocks(); +}); + +afterAll(() => { + jest.restoreAllMocks(); +}); + +describe('requireBasicAuth', () => { + const getAppAuthModeSpy = jest.spyOn(utils, 'getAppAuthMode'); + + let req, res, next; + + beforeEach(() => { + req = {}; + res = {}; + next = jest.fn(); + }); + + it.each([ + [1, AuthMode.NOAUTH, undefined], + [1, AuthMode.NOAUTH, AuthType.NONE], + [1, AuthMode.NOAUTH, AuthType.BASIC], + [1, AuthMode.NOAUTH, AuthType.BEARER], + [0, AuthMode.BASICAUTH, undefined], + [0, AuthMode.BASICAUTH, AuthType.NONE], + [1, AuthMode.BASICAUTH, AuthType.BASIC], + [0, AuthMode.BASICAUTH, AuthType.BEARER], + [0, AuthMode.OIDCAUTH, undefined], + [0, AuthMode.OIDCAUTH, AuthType.NONE], + [0, AuthMode.OIDCAUTH, AuthType.BASIC], + [0, AuthMode.OIDCAUTH, AuthType.BEARER], + [0, AuthMode.FULLAUTH, undefined], + [0, AuthMode.FULLAUTH, AuthType.NONE], + [1, AuthMode.FULLAUTH, AuthType.BASIC], + [0, AuthMode.FULLAUTH, AuthType.BEARER] + ])('should call next %i times given authMode %s and authType %s', (nextCount, mode, type) => { + const sendCount = 1 - nextCount; + getAppAuthModeSpy.mockReturnValue(mode); + if (type) req.currentUser = { authType: type }; + + if (sendCount) expect(() => requireBasicAuth(req, res, next)).toThrow(); + else expect(() => requireBasicAuth(req, res, next)).not.toThrow(); + + expect(next).toHaveBeenCalledTimes(nextCount); + if (nextCount) expect(next).toHaveBeenCalledWith(); + }); +}); + +describe('requireSomeAuth', () => { + const getAppAuthModeSpy = jest.spyOn(utils, 'getAppAuthMode'); + + let req, res, next; + + beforeEach(() => { + req = {}; + res = {}; + next = jest.fn(); + }); + + it.each([ + [1, AuthMode.NOAUTH, undefined], + [1, AuthMode.NOAUTH, AuthType.NONE], + [1, AuthMode.NOAUTH, AuthType.BASIC], + [1, AuthMode.NOAUTH, AuthType.BEARER], + [0, AuthMode.BASICAUTH, undefined], + [0, AuthMode.BASICAUTH, AuthType.NONE], + [1, AuthMode.BASICAUTH, AuthType.BASIC], + [1, AuthMode.BASICAUTH, AuthType.BEARER], + [0, AuthMode.OIDCAUTH, undefined], + [0, AuthMode.OIDCAUTH, AuthType.NONE], + [1, AuthMode.OIDCAUTH, AuthType.BASIC], + [1, AuthMode.OIDCAUTH, AuthType.BEARER], + [0, AuthMode.FULLAUTH, undefined], + [0, AuthMode.FULLAUTH, AuthType.NONE], + [1, AuthMode.FULLAUTH, AuthType.BASIC], + [1, AuthMode.FULLAUTH, AuthType.BEARER] + ])('should call next %i times given authMode %s and authType %s', (nextCount, mode, type) => { + const sendCount = 1 - nextCount; + getAppAuthModeSpy.mockReturnValue(mode); + if (type) req.currentUser = { authType: type }; + + if (sendCount) expect(() => requireSomeAuth(req, res, next)).toThrow(); + else expect(() => requireSomeAuth(req, res, next)).not.toThrow(); + + expect(next).toHaveBeenCalledTimes(nextCount); + if (nextCount) expect(next).toHaveBeenCalledWith(); + }); +}); diff --git a/comsapi/app/tests/unit/middleware/upload.spec.js b/comsapi/app/tests/unit/middleware/upload.spec.js new file mode 100644 index 00000000..7419b98f --- /dev/null +++ b/comsapi/app/tests/unit/middleware/upload.spec.js @@ -0,0 +1,64 @@ +const { currentUpload } = require('../../../src/middleware/upload'); + +beforeEach(() => { + jest.resetAllMocks(); +}); + +afterAll(() => { + jest.restoreAllMocks(); +}); + +describe('currentUpload', () => { + let req, res, next; + + beforeEach(() => { + req = { + get: jest.fn(), + socket: { server: {} } + }; + res = {}; + next = jest.fn(); + }); + + it.each([ + [0, undefined, false, undefined, undefined, undefined], + [0, undefined, false, 0, undefined, undefined], + [1, { contentLength: 539, filename: undefined, mimeType: 'application/octet-stream' }, false, 539, undefined, undefined], + [1, { contentLength: 539, filename: undefined, mimeType: 'application/octet-stream' }, false, 539, 'inline', undefined], + [1, { contentLength: 539, filename: undefined, mimeType: 'application/octet-stream' }, false, 539, 'xattachment', undefined], + [1, { contentLength: 539, filename: undefined, mimeType: 'application/octet-stream' }, false, 539, 'attachment; xfilename="foo.txt"', undefined], + [1, { contentLength: 539, filename: 'foo.txt', mimeType: 'application/octet-stream' }, false, 539, 'attachment; filename="foo.txt"', undefined], + [1, { contentLength: 539, filename: 'foo.txt', mimeType: 'text/plain' }, false, 539, 'attachment; filename="foo.txt"', 'text/plain'], + [1, { contentLength: 539, filename: 'föo.txt', mimeType: 'text/plain' }, false, 539, 'attachment; filename=foo.txt; filename*=UTF-8\'\'f%C3%B6o.txt', 'text/plain'], + [1, { contentLength: 539, filename: 'föo.txt', mimeType: 'text/plain' }, false, 539, 'attachment; filename*=UTF-8\'\'f%C3%B6o.txt; filename=foo.txt', 'text/plain'], + [0, undefined, true, undefined, undefined, undefined], + [0, undefined, true, 0, undefined, undefined], + [0, undefined, true, 539, undefined, undefined], + [0, undefined, true, 539, 'inline', undefined], + [0, undefined, true, 539, 'xattachment', undefined], + [0, undefined, true, 539, 'attachment; xfilename="foo.txt"', undefined], + [1, { contentLength: 539, filename: 'foo.txt', mimeType: 'application/octet-stream' }, true, 539, 'attachment; filename="foo.txt"', undefined], + [1, { contentLength: 539, filename: 'foo.txt', mimeType: 'text/plain' }, true, 539, 'attachment; filename="foo.txt"', 'text/plain'], + [1, { contentLength: 539, filename: 'föo.txt', mimeType: 'text/plain' }, true, 539, 'attachment; filename=foo.txt; filename*=UTF-8\'\'f%C3%B6o.txt', 'text/plain'], + [1, { contentLength: 539, filename: 'föo.txt', mimeType: 'text/plain' }, true, 539, 'attachment; filename*=UTF-8\'\'f%C3%B6o.txt; filename=foo.txt', 'text/plain'] + ])('should call next %i times with currentUpload %j given strict %j, length %j, disposition %j and type %j', (nextCount, current, strict, length, disposition, type) => { + const sendCount = 1 - nextCount; + + req.get.mockReturnValueOnce(length); // contentlength + req.get.mockReturnValueOnce(disposition); // contentdisposition + req.get.mockReturnValueOnce(type); // contenttype + + const result = currentUpload(strict); + expect(result).toBeInstanceOf(Function); + if (sendCount) expect(() => result(req, res, next)).toThrow(); + else expect(() => result(req, res, next)).not.toThrow(); + + expect(req.currentUpload).toEqual(current); + expect(next).toHaveBeenCalledTimes(nextCount); + if (nextCount) { + expect(req.socket.server.requestTimeout).toEqual(0); + expect(next).toHaveBeenCalledWith(); + } + }); +}); + diff --git a/comsapi/app/tests/unit/middleware/validation.spec.js b/comsapi/app/tests/unit/middleware/validation.spec.js new file mode 100644 index 00000000..df1fc994 --- /dev/null +++ b/comsapi/app/tests/unit/middleware/validation.spec.js @@ -0,0 +1,64 @@ +const { validate } = require('../../../src/middleware/validation'); + +beforeEach(() => { + jest.resetAllMocks(); +}); + +afterAll(() => { + jest.restoreAllMocks(); +}); + +describe('validate', () => { + let req, res, next; + + beforeEach(() => { + req = { + originalUrl: 'originalUrl', + params: { id: 'id' }, + query: { foo: 'bar', bool: false } + }; + res = {}; + next = jest.fn(); + }); + + it('should call next when no validation errors', () => { + const errors = undefined; + const schema = { query: { validate: jest.fn().mockReturnValue(errors) } }; + + const result = validate(schema); + expect(result).toBeInstanceOf(Function); + expect(() => result(req, res, next)).not.toThrow(); + + expect(schema.query.validate).toHaveBeenCalledTimes(1); + expect(next).toHaveBeenCalledTimes(1); + expect(next).toHaveBeenCalledWith(); + }); + + it('should respond with 422 with one validation error', () => { + const errors = { error: { details: [{ message: 'message' }] } }; + const schema = { query: { validate: jest.fn().mockReturnValue(errors) } }; + + const result = validate(schema); + expect(result).toBeInstanceOf(Function); + expect(() => result(req, res, next)).toThrow(); + + expect(schema.query.validate).toHaveBeenCalledTimes(1); + expect(next).toHaveBeenCalledTimes(0); + }); + + it('should respond with 422 with multiple validation errors', () => { + const errors = { error: { details: [{ message: 'message' }] } }; + const schema = { + params: { validate: jest.fn().mockReturnValue(errors) }, + query: { validate: jest.fn().mockReturnValue(errors) } + }; + + const result = validate(schema); + expect(result).toBeInstanceOf(Function); + expect(() => result(req, res, next)).toThrow(); + + expect(schema.query.validate).toHaveBeenCalledTimes(1); + expect(schema.query.validate).toHaveBeenCalledTimes(1); + expect(next).toHaveBeenCalledTimes(0); + }); +}); diff --git a/comsapi/app/tests/unit/routes/v1.spec.js b/comsapi/app/tests/unit/routes/v1.spec.js new file mode 100644 index 00000000..69145064 --- /dev/null +++ b/comsapi/app/tests/unit/routes/v1.spec.js @@ -0,0 +1,34 @@ +const request = require('supertest'); + +const { expressHelper } = require('../../common/helper'); +const router = require('../../../src/routes/v1'); + +// Simple Express Server +const basePath = '/api/v1'; +const app = expressHelper(basePath, router); + +// Mock config library - @see {@link https://stackoverflow.com/a/64819698} +jest.mock('config'); + +describe(`GET ${basePath}`, () => { + it('should return all available endpoints', async () => { + const response = await request(app).get(`${basePath}`); + + expect(response.statusCode).toBe(200); + expect(response.body).toBeTruthy(); + expect(Array.isArray(response.body.endpoints)).toBeTruthy(); + expect(response.body.endpoints).toHaveLength(9); + expect(response.body.endpoints).toEqual(expect.arrayContaining([ + '/bucket', + '/docs', + '/metadata', + '/object', + '/permission', + '/sync', + '/tagging', + '/user', + '/version' + ])); + }); +}); + diff --git a/comsapi/app/tests/unit/routes/v1/docs.spec.js b/comsapi/app/tests/unit/routes/v1/docs.spec.js new file mode 100644 index 00000000..797e09e9 --- /dev/null +++ b/comsapi/app/tests/unit/routes/v1/docs.spec.js @@ -0,0 +1,46 @@ +const request = require('supertest'); + +const { expressHelper } = require('../../../common/helper'); +const router = require('../../../../src/routes/v1/docs'); + +// Simple Express Server +const basePath = '/api/v1/docs'; +const app = expressHelper(basePath, router); + +// Mock config library - @see {@link https://stackoverflow.com/a/64819698} +jest.mock('config'); + +describe(`GET ${basePath}`, () => { + it('should return a redoc html page', async () => { + const response = await request(app).get(`${basePath}`); + + expect(response.statusCode).toBe(200); + expect(response.text).toContain('Common Object Management Service API - Documentation'); + }); +}); + +describe(`GET ${basePath}/api-spec.yaml`, () => { + it('should return the OpenAPI spec in yaml', async () => { + const response = await request(app).get(`${basePath}/api-spec.yaml`); + + expect(response.statusCode).toBe(200); + expect(response.body).toBeTruthy(); + expect(response.headers['content-type']).toBeTruthy(); + expect(response.headers['content-type']).toMatch('application/yaml; charset=utf-8'); + expect(response.text).toContain('openapi: 3.0.2'); + expect(response.text).toContain('title: Common Object Management Service (COMS)'); + }); +}); + +describe(`GET ${basePath}/api-spec.json`, () => { + it('should return the OpenAPI spec in json', async () => { + const response = await request(app).get(`${basePath}/api-spec.json`); + + expect(response.statusCode).toBe(200); + expect(response.headers['content-type']).toBeTruthy(); + expect(response.headers['content-type']).toMatch('application/json; charset=utf-8'); + expect(response.body).toBeTruthy(); + expect(response.body.openapi).toMatch('3.0.2'); + expect(response.body.info.title).toMatch('Common Object Management Service (COMS)'); + }); +}); diff --git a/comsapi/app/tests/unit/routes/v1/object.spec.js b/comsapi/app/tests/unit/routes/v1/object.spec.js new file mode 100644 index 00000000..c8ead71c --- /dev/null +++ b/comsapi/app/tests/unit/routes/v1/object.spec.js @@ -0,0 +1,29 @@ +const request = require('supertest'); + +const { expressHelper } = require('../../../common/helper'); +const router = require('../../../../src/routes/v1/object'); + +const { objectController } = require('../../../../src/controllers'); + +// Express Server +const basePath = '/api/v1/object'; +const app = expressHelper(basePath, router); + +// Mock config library - @see {@link https://stackoverflow.com/a/64819698} +jest.mock('config'); + +describe(`GET ${basePath}`, () => { + const spy = jest.spyOn(objectController, 'searchObjects'); + + beforeEach(() => { + spy.mockReset(); + }); + + it('Should call controller', async () => { + // eslint-disable-next-line no-unused-vars + spy.mockImplementation((_req, res, _next) => res.status(200).end()); + await request(app).get(`${basePath}`); + + expect(spy).toHaveBeenCalledTimes(1); + }); +}); diff --git a/comsapi/app/tests/unit/routes/v1/user.spec.js b/comsapi/app/tests/unit/routes/v1/user.spec.js new file mode 100644 index 00000000..961705c5 --- /dev/null +++ b/comsapi/app/tests/unit/routes/v1/user.spec.js @@ -0,0 +1,91 @@ +/* eslint-disable no-unused-vars */ +const request = require('supertest'); + +const { expressHelper } = require('../../../common/helper'); +const validator = require('../../../../src/validators/user'); + +// Mock config library - @see {@link https://stackoverflow.com/a/64819698} +jest.mock('config'); + +// +// Mock middleware, we are not testing this here +// +jest.mock('../../../../src/middleware/authorization', () => ({ + checkAppMode: jest.fn((_req, _res, next) => next()), +})); + +jest.mock('../../../../src/middleware/featureToggle', () => ({ + requireSomeAuth: jest.fn((_req, _res, next) => next()), +})); + +jest.mock('../../../../src/validators/user'); + +// +// Mocks are in place, create the router +// +const router = require('../../../../src/routes/v1/user'); + +const { userController } = require('../../../../src/controllers'); + +// Express Server +const basePath = '/api/v1/user'; +const app = expressHelper(basePath, router); + +describe(`GET ${basePath}`, () => { + // TODO: Ensure other middleware funcs are called once + const validatorSpy = jest.spyOn(validator, 'searchUsers'); + const controllerSpy = jest.spyOn(userController, 'searchUsers'); + + beforeEach(() => { + validatorSpy.mockReset(); + controllerSpy.mockReset(); + }); + + it('should call controller with known query params', async () => { + validatorSpy.mockImplementation((req, res, next) => next()); + controllerSpy.mockImplementation((req, res, next) => res.status(200).end()); + + const response = await request(app).get(`${basePath}`).query({ + userId: ['11bf5b37-e0b8-42e0-8dcf-dc8c4aefc000'], + idp: ['IDIR'], + active: 'true' + }); + + expect(validatorSpy).toHaveBeenCalledTimes(1); + expect(controllerSpy).toHaveBeenCalledTimes(1); + expect(response.statusCode).toBe(200); + }); +}); + +describe(`GET ${basePath}/idpList`, () => { + // TODO: Ensure other middleware funcs are called once + const validatorSpy = jest.spyOn(validator, 'listIdps'); + const controllerSpy = jest.spyOn(userController, 'listIdps'); + + beforeEach(() => { + validatorSpy.mockReset(); + controllerSpy.mockReset(); + }); + + it('should call controller with known query params', async () => { + validatorSpy.mockImplementation((req, res, next) => next()); + controllerSpy.mockImplementation((req, res, next) => res.status(200).end()); + + const response = await request(app).get(`${basePath}/idpList`).query({ active: 'true' }); + + expect(validatorSpy).toHaveBeenCalledTimes(1); + expect(controllerSpy).toHaveBeenCalledTimes(1); + expect(response.statusCode).toBe(200); + }); + + it('should call controller with no query params', async () => { + validatorSpy.mockImplementation((req, res, next) => next()); + controllerSpy.mockImplementation((req, res, next) => res.status(200).end()); + + const response = await request(app).get(`${basePath}/idpList`); + + expect(response.statusCode).toBe(200); + expect(validatorSpy).toHaveBeenCalledTimes(1); + expect(controllerSpy).toHaveBeenCalledTimes(1); + }); +}); diff --git a/comsapi/app/tests/unit/services/bucket.spec.js b/comsapi/app/tests/unit/services/bucket.spec.js new file mode 100644 index 00000000..3df05c96 --- /dev/null +++ b/comsapi/app/tests/unit/services/bucket.spec.js @@ -0,0 +1,169 @@ +const { NIL: BUCKET_ID, NIL: SYSTEM_USER } = require('uuid'); + +const { resetModel, trxBuilder } = require('../../common/helper'); +const Bucket = require('../../../src/db/models/tables/bucket'); + +const bucketTrx = trxBuilder(); +jest.mock('../../../src/db/models/tables/bucket', () => ({ + startTransaction: jest.fn(), + then: jest.fn(), + + allowGraph: jest.fn(), + deleteById: jest.fn(), + findById: jest.fn(), + first: jest.fn(), + insert: jest.fn(), + modify: jest.fn(), + patchAndFetchById: jest.fn(), + query: jest.fn(), + returning: jest.fn(), + throwIfNotFound: jest.fn(), + where: jest.fn() +})); + +const service = require('../../../src/services/bucket'); +const bucketPermissionService = require('../../../src/services/bucketPermission'); + +const data = { + bucketId: BUCKET_ID, + bucketName: 'bucketName', + accessKeyId: 'accesskeyid', + bucket: 'bucket', + endpoint: 'endpoint', + key: 'key', + secretAccessKey: 'secretaccesskey', + region: 'region', + active: 'true', + createdBy: SYSTEM_USER, + userId: SYSTEM_USER +}; + +beforeEach(() => { + jest.clearAllMocks(); + resetModel(Bucket, bucketTrx); +}); + +describe('checkGrantPermissions', () => { + const readUniqueSpy = jest.spyOn(service, 'readUnique'); + + beforeEach(() => { + readUniqueSpy.mockReset(); + }); + + afterAll(() => { + readUniqueSpy.mockRestore(); + }); + + it('Grants a user full permissions to the bucket if the data precisely matches', async () => { + readUniqueSpy.mockResolvedValue({ accessKeyId: data.accessKeyId, secretAccessKey: data.secretAccessKey }); + + await service.checkGrantPermissions(data); + + expect(Bucket.startTransaction).toHaveBeenCalledTimes(1); + expect(bucketTrx.commit).toHaveBeenCalledTimes(1); + }); +}); + +describe('create', () => { + const addPermissionsSpy = jest.spyOn(bucketPermissionService, 'addPermissions'); + + beforeEach(() => { + addPermissionsSpy.mockReset(); + }); + + afterAll(() => { + addPermissionsSpy.mockRestore(); + }); + + it('Create a bucket record and give the uploader (if authed) permissions', async () => { + addPermissionsSpy.mockResolvedValue({ ...data }); + + await service.create(data); + + expect(Bucket.startTransaction).toHaveBeenCalledTimes(1); + expect(Bucket.query).toHaveBeenCalledTimes(1); + expect(Bucket.insert).toHaveBeenCalledTimes(1); + expect(Bucket.insert).toBeCalledWith(expect.anything()); + expect(Bucket.returning).toHaveBeenCalledTimes(1); + expect(Bucket.returning).toBeCalledWith('*'); + expect(bucketTrx.commit).toHaveBeenCalledTimes(1); + }); +}); + +describe('delete', () => { + it('Delete a bucket record, this will also delete all objects and permissions', async () => { + await service.delete(BUCKET_ID); + + expect(Bucket.startTransaction).toHaveBeenCalledTimes(1); + expect(Bucket.query).toHaveBeenCalledTimes(1); + expect(Bucket.deleteById).toHaveBeenCalledTimes(1); + expect(Bucket.deleteById).toBeCalledWith(BUCKET_ID); + expect(Bucket.throwIfNotFound).toHaveBeenCalledTimes(1); + expect(Bucket.throwIfNotFound).toBeCalledWith(); + expect(Bucket.returning).toHaveBeenCalledTimes(1); + expect(Bucket.returning).toBeCalledWith('*'); + expect(bucketTrx.commit).toHaveBeenCalledTimes(1); + }); +}); + +describe('searchBuckets', () => { + it('search and filter for specific bucket records', async () => { + Bucket.then.mockImplementation(() => { }); + + await service.searchBuckets([]); + + expect(Bucket.query).toHaveBeenCalledTimes(1); + expect(Bucket.allowGraph).toHaveBeenCalledTimes(1); + expect(Bucket.modify).toHaveBeenCalledTimes(5); + expect(Bucket.then).toHaveBeenCalledTimes(1); + }); +}); + +describe('read', () => { + it('Get a bucket db record based on bucketId', async () => { + await service.read(BUCKET_ID); + + expect(Bucket.query).toHaveBeenCalledTimes(1); + expect(Bucket.findById).toHaveBeenCalledTimes(1); + expect(Bucket.findById).toBeCalledWith(BUCKET_ID); + expect(Bucket.throwIfNotFound).toHaveBeenCalledTimes(1); + expect(Bucket.throwIfNotFound).toBeCalledWith(); + }); +}); + +describe('readUnique', () => { + it('Get a bucket db record based on unique parameters', async () => { + await service.readUnique(data); + + expect(Bucket.query).toHaveBeenCalledTimes(1); + expect(Bucket.where).toHaveBeenCalledTimes(3); + expect(Bucket.where).toBeCalledWith('bucket', expect.any(String)); + expect(Bucket.where).toBeCalledWith('endpoint', expect.any(String)); + expect(Bucket.where).toBeCalledWith('key', expect.any(String)); + expect(Bucket.first).toHaveBeenCalledTimes(1); + expect(Bucket.first).toBeCalledWith(); + expect(Bucket.throwIfNotFound).toHaveBeenCalledTimes(1); + expect(Bucket.throwIfNotFound).toBeCalledWith(); + }); +}); + +describe('update', () => { + it('Update a bucket DB record', async () => { + await service.update(data); + + expect(Bucket.startTransaction).toHaveBeenCalledTimes(1); + expect(Bucket.query).toHaveBeenCalledTimes(1); + expect(Bucket.patchAndFetchById).toHaveBeenCalledTimes(1); + expect(Bucket.patchAndFetchById).toBeCalledWith(data.bucketId, { + bucketName: data.bucketName, + accessKeyId: data.accessKeyId, + bucket: data.bucket, + endpoint: data.endpoint, + secretAccessKey: data.secretAccessKey, + region: data.region, + active: data.active, + updatedBy: data.userId + }); + expect(bucketTrx.commit).toHaveBeenCalledTimes(1); + }); +}); diff --git a/comsapi/app/tests/unit/services/bucketPermission.spec.js b/comsapi/app/tests/unit/services/bucketPermission.spec.js new file mode 100644 index 00000000..32e87d80 --- /dev/null +++ b/comsapi/app/tests/unit/services/bucketPermission.spec.js @@ -0,0 +1,116 @@ +const { NIL: BUCKET_ID, NIL: OBJECT_ID, NIL: SYSTEM_USER } = require('uuid'); + +const { resetModel, trxBuilder } = require('../../common/helper'); +const BucketPermission = require('../../../src/db/models/tables/bucketPermission'); +const ObjectPermission = require('../../../src/db/models/tables/objectPermission'); + +const bucketPermissionTrx = trxBuilder(); +jest.mock('../../../src/db/models/tables/bucketPermission', () => ({ + startTransaction: jest.fn(), + then: jest.fn(), + + delete: jest.fn(), + insertAndFetch: jest.fn(), + modify: jest.fn(), + query: jest.fn(), + returning: jest.fn() +})); + +const objectPermissionTrx = trxBuilder(); +jest.mock('../../../src/db/models/tables/objectPermission', () => ({ + startTransaction: jest.fn(), + then: jest.fn(), + + distinct: jest.fn(), + joinRelated: jest.fn(), + modify: jest.fn(), + query: jest.fn(), + select: jest.fn(), + whereNotNull: jest.fn() +})); + +const service = require('../../../src/services/bucketPermission'); + +const data = [{ + id: OBJECT_ID, + bucketId: BUCKET_ID, + path: 'path', + public: 'true', + active: 'true', + createdBy: SYSTEM_USER, + permCode: 'READ' +}]; + +beforeEach(() => { + jest.clearAllMocks(); + resetModel(BucketPermission, bucketPermissionTrx); + resetModel(ObjectPermission, objectPermissionTrx); +}); + +describe('addPermissions', () => { + const searchPermissionsSpy = jest.spyOn(service, 'searchPermissions'); + + beforeEach(() => { + searchPermissionsSpy.mockReset(); + }); + + afterAll(() => { + searchPermissionsSpy.mockRestore(); + }); + + it('Grants bucket permissions to users', async () => { + searchPermissionsSpy.mockResolvedValue([{ userId: SYSTEM_USER, permCode: 'READ' }]); + + await service.addPermissions(BUCKET_ID, data); + + expect(BucketPermission.startTransaction).toHaveBeenCalledTimes(1); + expect(BucketPermission.insertAndFetch).toHaveBeenCalledTimes(1); + expect(BucketPermission.insertAndFetch).toBeCalledWith(expect.anything()); + expect(bucketPermissionTrx.commit).toHaveBeenCalledTimes(1); + }); +}); + +describe('removePermissions', () => { + it('Deletes bucket permissions for a user', async () => { + await service.removePermissions(BUCKET_ID, [SYSTEM_USER]); + + expect(BucketPermission.startTransaction).toHaveBeenCalledTimes(1); + expect(BucketPermission.delete).toHaveBeenCalledTimes(1); + expect(BucketPermission.delete).toBeCalledWith(); + expect(BucketPermission.modify).toHaveBeenCalledTimes(3); + expect(BucketPermission.modify).toBeCalledWith('filterUserId', [SYSTEM_USER]); + expect(BucketPermission.modify).toBeCalledWith('filterBucketId', BUCKET_ID); + expect(BucketPermission.returning).toHaveBeenCalledTimes(1); + expect(BucketPermission.returning).toBeCalledWith('*'); + expect(bucketPermissionTrx.commit).toHaveBeenCalledTimes(1); + }); +}); + +describe('listInheritedBucketIds', () => { + it('Searches for specific (bucket) object permissions', async () => { + ObjectPermission.then.mockImplementation(); + + await service.listInheritedBucketIds(); + + expect(ObjectPermission.query).toHaveBeenCalledTimes(1); + expect(ObjectPermission.select).toHaveBeenCalledTimes(1); + expect(ObjectPermission.distinct).toHaveBeenCalledTimes(1); + expect(ObjectPermission.joinRelated).toHaveBeenCalledTimes(1); + expect(ObjectPermission.modify).toHaveBeenCalledTimes(1); + expect(ObjectPermission.whereNotNull).toHaveBeenCalledTimes(1); + expect(ObjectPermission.then).toHaveBeenCalledTimes(1); + }); +}); + +describe('searchPermissions', () => { + it('Search and filter for specific bucket permissions', () => { + service.searchPermissions({ userId: SYSTEM_USER, bucketId: BUCKET_ID, permCode: 'READ' }); + + expect(BucketPermission.query).toHaveBeenCalledTimes(1); + expect(BucketPermission.modify).toHaveBeenCalledTimes(3); + expect(BucketPermission.modify).toBeCalledWith('filterUserId', SYSTEM_USER); + expect(BucketPermission.modify).toBeCalledWith('filterBucketId', BUCKET_ID); + expect(BucketPermission.modify).toBeCalledWith('filterPermissionCode', 'READ'); + expect(BucketPermission.modify).toHaveBeenCalledTimes(3); + }); +}); diff --git a/comsapi/app/tests/unit/services/metadata.spec.js b/comsapi/app/tests/unit/services/metadata.spec.js new file mode 100644 index 00000000..111ede23 --- /dev/null +++ b/comsapi/app/tests/unit/services/metadata.spec.js @@ -0,0 +1,232 @@ +const { NIL: SYSTEM_USER, NIL: VERSION_ID } = require('uuid'); + +const { resetModel, trxBuilder } = require('../../common/helper'); +const Metadata = require('../../../src/db/models/tables/metadata'); +const ObjectModel = require('../../../src/db/models/tables/objectModel'); +const Version = require('../../../src/db/models/tables/version'); +const VersionMetadata = require('../../../src/db/models/tables/versionMetadata'); + +const metadataTrx = trxBuilder(); +jest.mock('../../../src/db/models/tables/metadata', () => ({ + startTransaction: jest.fn(), + then: jest.fn(), + + allowGraph: jest.fn(), + delete: jest.fn(), + find: jest.fn(), + insert: jest.fn(), + map: jest.fn(), + modify: jest.fn(), + query: jest.fn(), + returning: jest.fn(), + select: jest.fn(), + whereIn: jest.fn(), + whereNull: jest.fn(), + withGraphJoined: jest.fn() +})); + +const objectModelTrx = trxBuilder(); +jest.mock('../../../src/db/models/tables/objectModel', () => ({ + startTransaction: jest.fn(), + then: jest.fn(), + + allowGraph: jest.fn(), + modify: jest.fn(), + modifyGraph: jest.fn(), + query: jest.fn(), + select: jest.fn(), + withGraphJoined: jest.fn() +})); + +const versionTrx = trxBuilder(); +jest.mock('../../../src/db/models/tables/version', () => ({ + startTransaction: jest.fn(), + then: jest.fn(), + + allowGraph: jest.fn(), + modify: jest.fn(), + modifyGraph: jest.fn(), + orderBy: jest.fn(), + query: jest.fn(), + select: jest.fn(), + withGraphJoined: jest.fn() +})); + +const versionMetadataTrx = trxBuilder(); +jest.mock('../../../src/db/models/tables/versionMetadata', () => ({ + startTransaction: jest.fn(), + then: jest.fn(), + + modify: jest.fn(), + query: jest.fn() +})); + +const service = require('../../../src/services/metadata'); +const utils = require('../../../src/components/utils'); + +const metadata = [{ key: 'a', value: '1' }, { key: 'B', value: '2' }]; +const params = { objId: 1, metadata: metadata, userId: SYSTEM_USER, privacyMask: 'privacyMask' }; + +beforeEach(() => { + jest.clearAllMocks(); + resetModel(Metadata, metadataTrx); + resetModel(ObjectModel, objectModelTrx); + resetModel(Version, versionTrx); + resetModel(VersionMetadata, versionMetadataTrx); +}); + +describe('associateMetadata', () => { + const createMetadataSpy = jest.spyOn(service, 'createMetadata'); + const pruneOrphanedMetadataSpy = jest.spyOn(service, 'pruneOrphanedMetadata'); + + beforeEach(() => { + createMetadataSpy.mockReset(); + pruneOrphanedMetadataSpy.mockReset(); + }); + + afterAll(() => { + createMetadataSpy.mockRestore(); + pruneOrphanedMetadataSpy.mockRestore(); + }); + + it('Makes the incoming list of metadata the definitive set associated with versionId', async () => { + createMetadataSpy.mockResolvedValue({ ...metadata }); + pruneOrphanedMetadataSpy.mockImplementation(() => { }); + + await service.associateMetadata(VERSION_ID, metadata); + + expect(Metadata.startTransaction).toHaveBeenCalledTimes(1); + expect(VersionMetadata.query).toHaveBeenCalledTimes(1); + expect(VersionMetadata.query).toHaveBeenCalledWith(expect.anything()); + expect(VersionMetadata.modify).toHaveBeenCalledTimes(1); + expect(VersionMetadata.modify).toHaveBeenCalledWith('filterVersionId', VERSION_ID); + expect(metadataTrx.commit).toHaveBeenCalledTimes(1); + expect(createMetadataSpy).toHaveBeenCalledTimes(1); + expect(createMetadataSpy).toHaveBeenCalledWith(metadata, expect.anything()); + expect(pruneOrphanedMetadataSpy).toHaveBeenCalledTimes(1); + expect(pruneOrphanedMetadataSpy).toHaveBeenCalledWith(expect.anything()); + }); +}); + +describe('pruneOrphanedMetadata', () => { + it('Deletes metadata records if they are no longer related to any versions', async () => { + Metadata.whereNull.mockResolvedValue([ + { + ...metadata, + map: jest.fn() + } + ]); + + await service.pruneOrphanedMetadata(); + + expect(Metadata.query).toHaveBeenCalledTimes(2); + expect(Metadata.allowGraph).toHaveBeenCalledTimes(1); + expect(Metadata.allowGraph).toHaveBeenCalledWith('versionMetadata'); + expect(Metadata.withGraphJoined).toHaveBeenCalledTimes(1); + expect(Metadata.withGraphJoined).toHaveBeenCalledWith('versionMetadata'); + expect(Metadata.select).toHaveBeenCalledTimes(1); + expect(Metadata.select).toHaveBeenCalledWith('metadata.id'); + expect(Metadata.whereNull).toHaveBeenCalledTimes(1); + expect(Metadata.whereNull).toHaveBeenCalledWith('versionMetadata.metadataId'); + expect(Metadata.delete).toHaveBeenCalledTimes(1); + expect(Metadata.whereIn).toHaveBeenCalledTimes(1); + }); +}); + +describe('createMetadata', () => { + const getObjectsByKeyValueSpy = jest.spyOn(utils, 'getObjectsByKeyValue'); + + beforeEach(() => { + getObjectsByKeyValueSpy.mockReset(); + }); + + afterAll(() => { + getObjectsByKeyValueSpy.mockRestore(); + }); + + it('Inserts any metadata records if they dont already exist in db', async () => { + Metadata.select.mockResolvedValue([ + { + ...metadata, + find: jest.fn() + } + ]); + + getObjectsByKeyValueSpy.mockResolvedValue({ key: 'a', value: '1' }); + + await service.createMetadata(metadata); + + expect(Metadata.startTransaction).toHaveBeenCalledTimes(1); + expect(Metadata.query).toHaveBeenCalledTimes(2); + expect(Metadata.query).toHaveBeenCalledWith(expect.anything()); + expect(Metadata.select).toHaveBeenCalledTimes(1); + expect(metadataTrx.commit).toHaveBeenCalledTimes(1); + }); +}); + +describe('fetchMetadataForObject', () => { + it('Fetch metadata for specific objects', () => { + ObjectModel.then.mockResolvedValue([ + { + ...metadata, + map: jest.fn() + } + ]); + + service.fetchMetadataForObject(params); + + expect(ObjectModel.query).toHaveBeenCalledTimes(1); + expect(ObjectModel.select).toHaveBeenCalledTimes(1); + expect(ObjectModel.select).toHaveBeenCalledWith('object.id AS objectId', 'object.bucketId as bucketId'); + expect(ObjectModel.allowGraph).toHaveBeenCalledTimes(1); + expect(ObjectModel.allowGraph).toHaveBeenCalledWith('version.metadata'); + expect(ObjectModel.withGraphJoined).toHaveBeenCalledTimes(1); + expect(ObjectModel.withGraphJoined).toHaveBeenCalledWith('version.metadata'); + expect(ObjectModel.modifyGraph).toHaveBeenCalledTimes(2); + expect(ObjectModel.modifyGraph).toHaveBeenCalledWith('version', expect.anything()); + expect(ObjectModel.modifyGraph).toHaveBeenCalledWith('version.metadata', expect.anything()); + expect(ObjectModel.modify).toHaveBeenCalledTimes(3); + expect(ObjectModel.modify).toHaveBeenCalledWith('filterIds', params.objId); + expect(ObjectModel.modify).toHaveBeenCalledWith('hasPermission', params.userId, 'READ'); + expect(ObjectModel.then).toHaveBeenCalledTimes(1); + }); +}); + +describe('fetchMetadataForVersion', () => { + it('Fetch metadata for specific versions', async () => { + Version.then.mockResolvedValue([ + { + ...metadata, + map: jest.fn() + } + ]); + + await service.fetchMetadataForVersion(params); + + expect(Metadata.startTransaction).toHaveBeenCalledTimes(1); + expect(Version.query).toHaveBeenCalledTimes(1); + expect(Version.select).toHaveBeenCalledTimes(1); + expect(Version.select).toHaveBeenCalledWith('version.id as versionId', 'version.s3VersionId'); + expect(Version.allowGraph).toHaveBeenCalledTimes(1); + expect(Version.allowGraph).toHaveBeenCalledWith('metadata'); + expect(Version.withGraphJoined).toHaveBeenCalledTimes(1); + expect(Version.withGraphJoined).toHaveBeenCalledWith('metadata'); + expect(Version.modifyGraph).toHaveBeenCalledTimes(1); + expect(Version.modifyGraph).toHaveBeenCalledWith('metadata', expect.anything()); + expect(Version.modify).toHaveBeenCalledTimes(3); + expect(Version.modify).toHaveBeenCalledWith('filterId', params.versionId); + expect(Version.orderBy).toHaveBeenCalledTimes(1); + expect(Version.orderBy).toHaveBeenCalledWith('version.createdAt', 'desc'); + expect(Version.then).toHaveBeenCalledTimes(1); + expect(metadataTrx.commit).toHaveBeenCalledTimes(1); + }); +}); + +describe('searchMetadata', () => { + it('Search and filter for specific metadata keys', () => { + service.searchMetadata(params); + + expect(Metadata.query).toHaveBeenCalledTimes(1); + expect(Metadata.modify).toHaveBeenCalledTimes(1); + }); +}); diff --git a/comsapi/app/tests/unit/services/object.spec.js b/comsapi/app/tests/unit/services/object.spec.js new file mode 100644 index 00000000..efdedeb3 --- /dev/null +++ b/comsapi/app/tests/unit/services/object.spec.js @@ -0,0 +1,172 @@ +const { NIL: BUCKET_ID, NIL: OBJECT_ID, NIL: SYSTEM_USER } = require('uuid'); + +const { resetModel, trxBuilder } = require('../../common/helper'); +const ObjectModel = require('../../../src/db/models/tables/objectModel'); + +const objectModelTrx = trxBuilder(); +jest.mock('../../../src/db/models/tables/objectModel', () => ({ + startTransaction: jest.fn(), + then: jest.fn(), + + allowGraph: jest.fn(), + deleteById: jest.fn(), + findById: jest.fn(), + first: jest.fn(), + insert: jest.fn(), + joinRelated: jest.fn(), + modify: jest.fn(), + patchAndFetchById: jest.fn(), + query: jest.fn(), + returning: jest.fn(), + select: jest.fn(), + throwIfNotFound: jest.fn() +})); + +const service = require('../../../src/services/object'); +const objectPermissionService = require('../../../src/services/objectPermission'); + +const data = { + id: OBJECT_ID, + bucketId: BUCKET_ID, + path: 'path', + public: 'true', + active: 'true', + createdBy: SYSTEM_USER, + userId: SYSTEM_USER +}; + +beforeEach(() => { + jest.clearAllMocks(); + resetModel(ObjectModel, objectModelTrx); +}); + +describe('create', () => { + const addPermissionsSpy = jest.spyOn(objectPermissionService, 'addPermissions'); + + beforeEach(() => { + addPermissionsSpy.mockReset(); + }); + + afterAll(() => { + addPermissionsSpy.mockRestore(); + }); + + it('Create an object db record and give the uploader (if authed) permissions', async () => { + addPermissionsSpy.mockResolvedValue({}); + + await service.create({ ...data, userId: SYSTEM_USER }); + + expect(ObjectModel.startTransaction).toHaveBeenCalledTimes(1); + expect(ObjectModel.query).toHaveBeenCalledTimes(1); + expect(ObjectModel.query).toHaveBeenCalledWith(expect.anything()); + expect(ObjectModel.insert).toHaveBeenCalledTimes(1); + expect(ObjectModel.insert).toBeCalledWith(expect.anything()); + expect(ObjectModel.returning).toHaveBeenCalledTimes(1); + expect(ObjectModel.returning).toBeCalledWith('*'); + expect(objectModelTrx.commit).toHaveBeenCalledTimes(1); + }); +}); + +describe('delete', () => { + it('Delete an object record', async () => { + await service.delete(OBJECT_ID); + + expect(ObjectModel.startTransaction).toHaveBeenCalledTimes(1); + expect(ObjectModel.query).toHaveBeenCalledTimes(1); + expect(ObjectModel.query).toHaveBeenCalledWith(expect.anything()); + expect(ObjectModel.deleteById).toHaveBeenCalledTimes(1); + expect(ObjectModel.deleteById).toBeCalledWith(OBJECT_ID); + expect(ObjectModel.throwIfNotFound).toHaveBeenCalledTimes(1); + expect(ObjectModel.throwIfNotFound).toBeCalledWith(); + expect(ObjectModel.returning).toHaveBeenCalledTimes(1); + expect(ObjectModel.returning).toBeCalledWith('*'); + expect(objectModelTrx.commit).toHaveBeenCalledTimes(1); + }); +}); + +describe('getBucketKey', () => { + it('Gets the associated key path for a specific object record', () => { + service.getBucketKey(OBJECT_ID); + + expect(ObjectModel.query).toHaveBeenCalledTimes(1); + expect(ObjectModel.findById).toHaveBeenCalledTimes(1); + expect(ObjectModel.findById).toBeCalledWith(OBJECT_ID); + expect(ObjectModel.select).toHaveBeenCalledTimes(1); + expect(ObjectModel.select).toBeCalledWith('bucket.key'); + expect(ObjectModel.joinRelated).toHaveBeenCalledTimes(1); + expect(ObjectModel.joinRelated).toBeCalledWith('bucket'); + expect(ObjectModel.first).toHaveBeenCalledTimes(1); + expect(ObjectModel.first).toBeCalledWith(); + expect(ObjectModel.throwIfNotFound).toHaveBeenCalledTimes(1); + expect(ObjectModel.throwIfNotFound).toBeCalledWith(); + }); +}); + +describe('searchObjects', () => { + it('Search and filter for specific object records', async () => { + ObjectModel.then.mockImplementation(() => { }); + const params = { + bucketId: BUCKET_ID, + bucketName: 'bucketName', + active: 'true', + key: 'key', + userId: SYSTEM_USER + }; + + await service.searchObjects(params); + + expect(ObjectModel.startTransaction).toHaveBeenCalledTimes(1); + expect(ObjectModel.query).toHaveBeenCalledTimes(1); + expect(ObjectModel.query).toHaveBeenCalledWith(expect.anything()); + expect(ObjectModel.allowGraph).toHaveBeenCalledTimes(1); + expect(ObjectModel.modify).toHaveBeenCalledTimes(11); + expect(ObjectModel.modify).toHaveBeenNthCalledWith(1, 'filterIds', params.id); + expect(ObjectModel.modify).toHaveBeenNthCalledWith(2, 'filterBucketIds', params.bucketId); + expect(ObjectModel.modify).toHaveBeenNthCalledWith(3, 'filterName', params.name); + expect(ObjectModel.modify).toHaveBeenNthCalledWith(4, 'filterPath', params.path); + expect(ObjectModel.modify).toHaveBeenNthCalledWith(5, 'filterPublic', params.public); + expect(ObjectModel.modify).toHaveBeenNthCalledWith(6, 'filterActive', params.active); + expect(ObjectModel.modify).toHaveBeenNthCalledWith(7, 'filterMimeType', params.mimeType); + expect(ObjectModel.modify).toHaveBeenNthCalledWith(8, 'filterDeleteMarker', params.deleteMarker); + expect(ObjectModel.modify).toHaveBeenNthCalledWith(9, 'filterLatest', params.latest); + expect(ObjectModel.modify).toHaveBeenNthCalledWith(10, 'filterMetadataTag', { + metadata: params.metadata, + tag: params.tag + }); + expect(ObjectModel.modify).toHaveBeenNthCalledWith(11, 'hasPermission', params.userId, 'READ'); + expect(ObjectModel.then).toHaveBeenCalledTimes(1); + expect(objectModelTrx.commit).toHaveBeenCalledTimes(1); + }); +}); + +describe('read', () => { + it('Get an object db record', async () => { + await service.read(SYSTEM_USER); + + expect(ObjectModel.startTransaction).toHaveBeenCalledTimes(1); + expect(ObjectModel.query).toHaveBeenCalledTimes(1); + expect(ObjectModel.query).toHaveBeenCalledWith(expect.anything()); + expect(ObjectModel.findById).toHaveBeenCalledTimes(1); + expect(ObjectModel.findById).toBeCalledWith(OBJECT_ID); + expect(ObjectModel.throwIfNotFound).toHaveBeenCalledTimes(1); + expect(ObjectModel.throwIfNotFound).toBeCalledWith(); + expect(objectModelTrx.commit).toHaveBeenCalledTimes(1); + }); +}); + +describe('update', () => { + it('Update an object DB record', async () => { + await service.update({ ...data }); + + expect(ObjectModel.startTransaction).toHaveBeenCalledTimes(1); + expect(ObjectModel.query).toHaveBeenCalledTimes(1); + expect(ObjectModel.patchAndFetchById).toHaveBeenCalledTimes(1); + expect(ObjectModel.patchAndFetchById).toBeCalledWith(data.id, { + path: data.path, + public: data.public, + active: data.active, + updatedBy: data.userId + }); + expect(objectModelTrx.commit).toHaveBeenCalledTimes(1); + }); +}); diff --git a/comsapi/app/tests/unit/services/objectPermission.spec.js b/comsapi/app/tests/unit/services/objectPermission.spec.js new file mode 100644 index 00000000..2d713c37 --- /dev/null +++ b/comsapi/app/tests/unit/services/objectPermission.spec.js @@ -0,0 +1,111 @@ +const { NIL: BUCKET_ID, NIL: OBJECT_ID, NIL: SYSTEM_USER } = require('uuid'); + +const { resetModel, trxBuilder } = require('../../common/helper'); +const BucketPermission = require('../../../src/db/models/tables/bucketPermission'); +const ObjectPermission = require('../../../src/db/models/tables/objectPermission'); + +const bucketPermissionTrx = trxBuilder(); +jest.mock('../../../src/db/models/tables/bucketPermission', () => ({ + startTransaction: jest.fn(), + then: jest.fn(), + + distinct: jest.fn(), + rightJoin: jest.fn(), + modify: jest.fn(), + query: jest.fn(), +})); + +const objectPermissionTrx = trxBuilder(); +jest.mock('../../../src/db/models/tables/objectPermission', () => ({ + startTransaction: jest.fn(), + then: jest.fn(), + + delete: jest.fn(), + insertAndFetch: jest.fn(), + modify: jest.fn(), + query: jest.fn(), + returning: jest.fn() +})); + +const service = require('../../../src/services/objectPermission'); + +beforeEach(() => { + jest.clearAllMocks(); + resetModel(BucketPermission, bucketPermissionTrx); + resetModel(ObjectPermission, objectPermissionTrx); +}); + +describe('addPermissions', () => { + const searchPermissionsSpy = jest.spyOn(service, 'searchPermissions'); + + beforeEach(() => { + searchPermissionsSpy.mockReset(); + }); + + afterAll(() => { + searchPermissionsSpy.mockRestore(); + }); + + it('Grants object permissions to users', async () => { + searchPermissionsSpy.mockResolvedValue([{ userId: SYSTEM_USER, permCode: 'READ' }]); + + await service.addPermissions(OBJECT_ID, [{ + id: OBJECT_ID, + bucketId: BUCKET_ID, + path: 'path', + public: 'true', + active: 'true', + createdBy: SYSTEM_USER, + permCode: 'READ' + }]); + + expect(ObjectPermission.startTransaction).toHaveBeenCalledTimes(1); + expect(ObjectPermission.query).toHaveBeenCalledTimes(1); + expect(ObjectPermission.insertAndFetch).toHaveBeenCalledTimes(1); + expect(ObjectPermission.insertAndFetch).toBeCalledWith(expect.any(Object)); + expect(objectPermissionTrx.commit).toHaveBeenCalledTimes(1); + }); +}); + +describe('listInheritedObjectIds', () => { + it('searches for specific (object) bucket permissions', async () => { + BucketPermission.then.mockImplementation(); + + await service.listInheritedObjectIds(); + + expect(BucketPermission.distinct).toHaveBeenCalledTimes(1); + expect(BucketPermission.rightJoin).toHaveBeenCalledTimes(1); + expect(BucketPermission.modify).toHaveBeenCalledTimes(2); + expect(BucketPermission.then).toHaveBeenCalledTimes(1); + }); +}); + +describe('removePermissions', () => { + it('Deletes object permissions for a user', async () => { + await service.removePermissions(OBJECT_ID, [SYSTEM_USER]); + + expect(ObjectPermission.startTransaction).toHaveBeenCalledTimes(1); + expect(ObjectPermission.query).toHaveBeenCalledTimes(1); + expect(ObjectPermission.delete).toHaveBeenCalledTimes(1); + expect(ObjectPermission.delete).toBeCalledWith(); + expect(ObjectPermission.modify).toHaveBeenCalledTimes(3); + expect(ObjectPermission.modify).toBeCalledWith('filterUserId', [SYSTEM_USER]); + expect(ObjectPermission.modify).toBeCalledWith('filterObjectId', OBJECT_ID); + expect(ObjectPermission.returning).toHaveBeenCalledTimes(1); + expect(ObjectPermission.returning).toBeCalledWith('*'); + expect(objectPermissionTrx.commit).toHaveBeenCalledTimes(1); + }); +}); + +describe('SearchPermissions', () => { + it('search and filter for specific object permissions', async () => { + await service.searchPermissions({ bucketId: BUCKET_ID, userId: SYSTEM_USER, objId: OBJECT_ID, permCode: 'READ' }); + + expect(ObjectPermission.query).toHaveBeenCalledTimes(1); + expect(ObjectPermission.modify).toBeCalledWith('filterBucketId', BUCKET_ID); + expect(ObjectPermission.modify).toBeCalledWith('filterUserId', SYSTEM_USER); + expect(ObjectPermission.modify).toBeCalledWith('filterObjectId', OBJECT_ID); + expect(ObjectPermission.modify).toBeCalledWith('filterPermissionCode', 'READ'); + expect(ObjectPermission.modify).toHaveBeenCalledTimes(4); + }); +}); diff --git a/comsapi/app/tests/unit/services/objectQueue.spec.js b/comsapi/app/tests/unit/services/objectQueue.spec.js new file mode 100644 index 00000000..9d29abfe --- /dev/null +++ b/comsapi/app/tests/unit/services/objectQueue.spec.js @@ -0,0 +1,88 @@ +const { NIL: BUCKET_ID, NIL: SYSTEM_USER } = require('uuid'); + +const { resetModel, trxBuilder } = require('../../common/helper'); +const ObjectQueue = require('../../../src/db/models/tables/objectQueue'); + +const objectQueueTrx = trxBuilder(); +jest.mock('../../../src/db/models/tables/objectQueue', () => ({ + startTransaction: jest.fn(), + then: jest.fn(), + + count: jest.fn(), + delete: jest.fn(), + first: jest.fn(), + ignore: jest.fn(), + insert: jest.fn(), + modify: jest.fn(), + onConflict: jest.fn(), + query: jest.fn(), + returning: jest.fn() +})); + +const service = require('../../../src/services/objectQueue'); + +beforeEach(() => { + jest.clearAllMocks(); + resetModel(ObjectQueue, objectQueueTrx); +}); + +describe('dequeue', () => { + it('Pops a job from the object queue if available via findNextJob', async () => { + await service.dequeue(); + + expect(ObjectQueue.startTransaction).toHaveBeenCalledTimes(1); + expect(ObjectQueue.query).toHaveBeenCalledTimes(1); + expect(ObjectQueue.query).toBeCalledWith(expect.anything()); + expect(ObjectQueue.modify).toHaveBeenCalledTimes(1); + expect(ObjectQueue.modify).toBeCalledWith('findNextJob'); + expect(ObjectQueue.delete).toHaveBeenCalledTimes(1); + expect(ObjectQueue.returning).toHaveBeenCalledTimes(1); + expect(ObjectQueue.returning).toBeCalledWith('*'); + expect(objectQueueTrx.commit).toHaveBeenCalledTimes(1); + }); +}); + +describe('enqueue', () => { + it('Inserts a job into the object queue only if it is not already present', async () => { + ObjectQueue.ignore.mockReturnValue([]); + const data = { + jobs: [{ + path: 'path', + bucketId: BUCKET_ID + }], + full: true, + retries: 0, + createdBy: SYSTEM_USER + }; + + await service.enqueue(data); + + expect(ObjectQueue.startTransaction).toHaveBeenCalledTimes(1); + expect(ObjectQueue.query).toHaveBeenCalledTimes(1); + expect(ObjectQueue.query).toBeCalledWith(expect.anything()); + expect(ObjectQueue.insert).toHaveBeenCalledTimes(1); + expect(ObjectQueue.insert).toBeCalledWith(expect.arrayContaining([ + expect.objectContaining({ + bucketId: data.jobs[0].bucketId, + createdBy: data.createdBy, + full: data.full, + path: data.jobs[0].path, + retries: data.retries + }) + ])); + expect(ObjectQueue.onConflict).toHaveBeenCalledTimes(1); + expect(ObjectQueue.ignore).toHaveBeenCalledTimes(1); + expect(objectQueueTrx.commit).toHaveBeenCalledTimes(1); + }); +}); + +describe('queueSize', () => { + it('Returns the number of jobs in the queue', async () => { + await service.queueSize(); + + expect(ObjectQueue.query).toHaveBeenCalledTimes(1); + expect(ObjectQueue.count).toHaveBeenCalledTimes(1); + expect(ObjectQueue.first).toHaveBeenCalledTimes(1); + expect(ObjectQueue.then).toHaveBeenCalledTimes(1); + }); +}); diff --git a/comsapi/app/tests/unit/services/storage.spec.js b/comsapi/app/tests/unit/services/storage.spec.js new file mode 100644 index 00000000..9c8ed137 --- /dev/null +++ b/comsapi/app/tests/unit/services/storage.spec.js @@ -0,0 +1,1055 @@ +const { + CopyObjectCommand, + DeleteObjectCommand, + DeleteObjectTaggingCommand, + GetBucketEncryptionCommand, + GetBucketVersioningCommand, + GetObjectCommand, + GetObjectTaggingCommand, + HeadBucketCommand, + HeadObjectCommand, + ListObjectsV2Command, + ListObjectVersionsCommand, + PutBucketEncryptionCommand, + PutObjectCommand, + PutObjectTaggingCommand, + S3Client +} = require('@aws-sdk/client-s3'); +const { getSignedUrl } = require('@aws-sdk/s3-request-presigner'); +const { mockClient } = require('aws-sdk-client-mock'); +require('aws-sdk-client-mock-jest'); // Must be globally imported +const config = require('config'); +const { Readable } = require('stream'); + +const service = require('../../../src/services/storage'); +const utils = require('../../../src/components/utils'); +const { MetadataDirective, TaggingDirective } = require('../../../src/components/constants'); + +const DEFAULTREGION = 'us-east-1'; // Need to specify valid AWS region or it'll explode ('us-east-1' is default, 'ca-central-1' for Canada) +const bucket = 'bucket'; +const key = 'filePath'; +const defaultTempExpiresIn = parseInt(config.get('server.defaultTempExpiresIn'), 10); + +const s3ClientMock = mockClient(S3Client); + +jest.mock('@aws-sdk/s3-request-presigner', () => ({ + getSignedUrl: jest.fn() +})); +// Mock config library - @see {@link https://stackoverflow.com/a/64819698} +jest.mock('config'); + +beforeEach(() => { + s3ClientMock.reset(); + config.get + .mockReturnValueOnce('accessKeyId') // objectStorage.accessKeyId + .mockReturnValueOnce(bucket) // objectStorage.bucket + .mockReturnValueOnce('https://endpoint.com') // objectStorage.endpoint + .mockReturnValueOnce(key) // objectStorage.key + .mockReturnValueOnce('secretAccessKey'); // objectStorage.secretAccessKey + utils.getBucket = jest.fn(() => ({ + accessKeyId: 'accessKeyId', + bucket: bucket, + endpoint: 'https://endpoint.com', + key: key, + region: DEFAULTREGION, + secretAccessKey: config.get('secretAccessKey') + })); + utils.isAtPath = jest.fn(() => true); +}); + +describe('_getS3Client', () => { + it('should be a function', () => { + expect(service._getS3Client).toBeTruthy(); + expect(typeof service._getS3Client).toBe('function'); + }); +}); + +describe('copyObject', () => { + beforeEach(() => { + s3ClientMock.on(CopyObjectCommand).resolves({}); + }); + + it('should send a copy object command copying the metadata and tags', async () => { + const copySource = 'filePath'; + const filePath = 'filePath'; + const result = await service.copyObject({ copySource, filePath }); + + expect(result).toBeTruthy(); + expect(utils.getBucket).toHaveBeenCalledTimes(1); + expect(s3ClientMock).toHaveReceivedCommandTimes(CopyObjectCommand, 1); + expect(s3ClientMock).toHaveReceivedCommandWith(CopyObjectCommand, { + Bucket: bucket, + CopySource: `${bucket}/${copySource}`, + Key: filePath, + Metadata: undefined, + MetadataDirective: MetadataDirective.COPY, + TaggingDirective: TaggingDirective.COPY, + VersionId: undefined + }); + }); + + it('should send a copy object command copying the metadata and tags for a specific version', async () => { + const copySource = 'filePath'; + const filePath = 'filePath'; + const s3VersionId = '1234'; + const result = await service.copyObject({ copySource, filePath, s3VersionId }); + + expect(result).toBeTruthy(); + expect(utils.getBucket).toHaveBeenCalledTimes(1); + expect(s3ClientMock).toHaveReceivedCommandTimes(CopyObjectCommand, 1); + expect(s3ClientMock).toHaveReceivedCommandWith(CopyObjectCommand, { + Bucket: bucket, + CopySource: `${bucket}/${copySource}`, + Key: filePath, + Metadata: undefined, + MetadataDirective: MetadataDirective.COPY, + TaggingDirective: TaggingDirective.COPY, + VersionId: s3VersionId + }); + }); + + it('should send a copy object command replacing the metadata', async () => { + const copySource = 'filePath'; + const filePath = 'filePath'; + const metadata = { 'x-amz-meta-test': 123 }; + const metadataDirective = MetadataDirective.REPLACE; + const result = await service.copyObject({ copySource, filePath, metadata, metadataDirective }); + + expect(result).toBeTruthy(); + expect(utils.getBucket).toHaveBeenCalledTimes(1); + expect(s3ClientMock).toHaveReceivedCommandTimes(CopyObjectCommand, 1); + expect(s3ClientMock).toHaveReceivedCommandWith(CopyObjectCommand, { + Bucket: bucket, + CopySource: `${bucket}/${copySource}`, + Key: filePath, + Metadata: metadata, + MetadataDirective: metadataDirective, + TaggingDirective: TaggingDirective.COPY, + VersionId: undefined + }); + }); + + it('should send a copy object command replacing the metadata for a specific version', async () => { + const copySource = 'filePath'; + const filePath = 'filePath'; + const metadata = { 'x-amz-meta-test': 123 }; + const metadataDirective = MetadataDirective.REPLACE; + const s3VersionId = '1234'; + const result = await service.copyObject({ copySource, filePath, metadata, metadataDirective, s3VersionId }); + + expect(result).toBeTruthy(); + expect(utils.getBucket).toHaveBeenCalledTimes(1); + expect(s3ClientMock).toHaveReceivedCommandTimes(CopyObjectCommand, 1); + expect(s3ClientMock).toHaveReceivedCommandWith(CopyObjectCommand, { + Bucket: bucket, + CopySource: `${bucket}/${copySource}`, + Key: filePath, + Metadata: metadata, + MetadataDirective: metadataDirective, + TaggingDirective: TaggingDirective.COPY, + VersionId: s3VersionId + }); + }); + + it('should send a copy object command replacing the tags', async () => { + const copySource = 'filePath'; + const filePath = 'filePath'; + const tags = { 'test': 123 }; + const taggingDirective = TaggingDirective.REPLACE; + const result = await service.copyObject({ copySource, filePath, tags, taggingDirective }); + + expect(result).toBeTruthy(); + expect(utils.getBucket).toHaveBeenCalledTimes(1); + expect(s3ClientMock).toHaveReceivedCommandTimes(CopyObjectCommand, 1); + expect(s3ClientMock).toHaveReceivedCommandWith(CopyObjectCommand, { + Bucket: bucket, + CopySource: `${bucket}/${copySource}`, + Key: filePath, + Metadata: undefined, + MetadataDirective: MetadataDirective.COPY, + Tagging: 'test=123', + TaggingDirective: taggingDirective, + VersionId: undefined + }); + }); + + it('should send a copy object command replacing the tags for a specific version', async () => { + const copySource = 'filePath'; + const filePath = 'filePath'; + const tags = { 'test': 123 }; + const taggingDirective = TaggingDirective.REPLACE; + const s3VersionId = '1234'; + const result = await service.copyObject({ copySource, filePath, tags, taggingDirective, s3VersionId }); + + expect(result).toBeTruthy(); + expect(utils.getBucket).toHaveBeenCalledTimes(1); + expect(s3ClientMock).toHaveReceivedCommandTimes(CopyObjectCommand, 1); + expect(s3ClientMock).toHaveReceivedCommandWith(CopyObjectCommand, { + Bucket: bucket, + CopySource: `${bucket}/${copySource}`, + Key: filePath, + Metadata: undefined, + MetadataDirective: MetadataDirective.COPY, + Tagging: 'test=123', + TaggingDirective: taggingDirective, + VersionId: s3VersionId + }); + }); +}); + +describe('deleteObject', () => { + beforeEach(() => { + s3ClientMock.on(DeleteObjectCommand).resolves({}); + }); + + it('should send a delete object command for the entire object', async () => { + const filePath = 'filePath'; + const result = await service.deleteObject({ filePath }); + + expect(result).toBeTruthy(); + expect(utils.getBucket).toHaveBeenCalledTimes(1); + expect(s3ClientMock).toHaveReceivedCommandTimes(DeleteObjectCommand, 1); + expect(s3ClientMock).toHaveReceivedCommandWith(DeleteObjectCommand, { + Bucket: bucket, + Key: filePath, + VersionId: undefined + }); + }); + + it('should send a delete object command for a specific version', async () => { + const filePath = 'filePath'; + const s3VersionId = '1234'; + const result = await service.deleteObject({ filePath, s3VersionId }); + + expect(result).toBeTruthy(); + expect(utils.getBucket).toHaveBeenCalledTimes(1); + expect(s3ClientMock).toHaveReceivedCommandTimes(DeleteObjectCommand, 1); + expect(s3ClientMock).toHaveReceivedCommandWith(DeleteObjectCommand, { + Bucket: bucket, + Key: filePath, + VersionId: s3VersionId + }); + }); +}); + +describe('deleteObjectTagging', () => { + beforeEach(() => { + s3ClientMock.on(DeleteObjectTaggingCommand).resolves({}); + }); + + it('should send a delete object tagging command', async () => { + const filePath = 'filePath'; + const result = await service.deleteObjectTagging({ filePath }); + + expect(result).toBeTruthy(); + expect(utils.getBucket).toHaveBeenCalledTimes(1); + expect(s3ClientMock).toHaveReceivedCommandTimes(DeleteObjectTaggingCommand, 1); + expect(s3ClientMock).toHaveReceivedCommandWith(DeleteObjectTaggingCommand, { + Bucket: bucket, + Key: filePath, + VersionId: undefined + }); + }); + + it('should send a delete object tagging command for a specific version', async () => { + const filePath = 'filePath'; + const s3VersionId = '1234'; + const result = await service.deleteObjectTagging({ filePath, s3VersionId }); + + expect(result).toBeTruthy(); + expect(utils.getBucket).toHaveBeenCalledTimes(1); + expect(s3ClientMock).toHaveReceivedCommandTimes(DeleteObjectTaggingCommand, 1); + expect(s3ClientMock).toHaveReceivedCommandWith(DeleteObjectTaggingCommand, { + Bucket: bucket, + Key: filePath, + VersionId: s3VersionId + }); + }); +}); + +describe('headBucket', () => { + beforeEach(() => { + s3ClientMock.on(HeadBucketCommand).resolves({}); + }); + + it('should send a head bucket command', async () => { + const result = await service.headBucket({ bucketId: 'abc-123' }); + + expect(result).toBeTruthy(); + expect(utils.getBucket).toHaveBeenCalledTimes(1); + expect(s3ClientMock).toHaveReceivedCommandTimes(HeadBucketCommand, 1); + expect(s3ClientMock).toHaveReceivedCommandWith(HeadBucketCommand, { + Bucket: bucket + }); + }); + + it('should not get the existing bucket if no id provided', async () => { + const result = await service.headBucket({ region: 'test', bucket: 'specify' }); + + expect(result).toBeTruthy(); + expect(utils.getBucket).toHaveBeenCalledTimes(0); + expect(s3ClientMock).toHaveReceivedCommandTimes(HeadBucketCommand, 1); + expect(s3ClientMock).toHaveReceivedCommandWith(HeadBucketCommand, { + Bucket: 'specify' + }); + }); + + it('should not get the existing bucket if default params', async () => { + const result = await service.headBucket(); + + expect(result).toBeTruthy(); + expect(utils.getBucket).toHaveBeenCalledTimes(0); + expect(s3ClientMock).toHaveReceivedCommandTimes(HeadBucketCommand, 1); + expect(s3ClientMock).toHaveReceivedCommandWith(HeadBucketCommand, { + Bucket: undefined + }); + }); +}); + +describe('getBucketEncryption', () => { + beforeEach(() => { + s3ClientMock.on(GetBucketEncryptionCommand).resolves({}); + }); + + it('should send a get bucket encryption command', async () => { + const result = await service.getBucketEncryption(); + + expect(result).toBeTruthy(); + expect(utils.getBucket).toHaveBeenCalledTimes(1); + expect(s3ClientMock).toHaveReceivedCommandTimes(GetBucketEncryptionCommand, 1); + expect(s3ClientMock).toHaveReceivedCommandWith(GetBucketEncryptionCommand, { + Bucket: bucket + }); + }); +}); + +describe('getBucketVersioning', () => { + beforeEach(() => { + s3ClientMock.on(GetBucketVersioningCommand).resolves({}); + }); + + it('should send a get bucket versioning command', async () => { + const result = await service.getBucketVersioning(); + + expect(result).toBeFalsy(); + expect(utils.getBucket).toHaveBeenCalledTimes(1); + expect(s3ClientMock).toHaveReceivedCommandTimes(GetBucketVersioningCommand, 1); + expect(s3ClientMock).toHaveReceivedCommandWith(GetBucketVersioningCommand, { + Bucket: bucket + }); + }); +}); + +describe('getObjectTagging', () => { + beforeEach(() => { + s3ClientMock.on(GetObjectTaggingCommand).resolves({}); + }); + + it('should send a get object tagging command', async () => { + const filePath = 'filePath'; + const result = await service.getObjectTagging({ filePath }); + + expect(result).toBeTruthy(); + expect(utils.getBucket).toHaveBeenCalledTimes(1); + expect(s3ClientMock).toHaveReceivedCommandTimes(GetObjectTaggingCommand, 1); + expect(s3ClientMock).toHaveReceivedCommandWith(GetObjectTaggingCommand, { + Bucket: bucket, + Key: filePath, + VersionId: undefined + }); + }); + + it('should send a put object tagging command for a specific version', async () => { + const filePath = 'filePath'; + const s3VersionId = '1234'; + const result = await service.getObjectTagging({ filePath, s3VersionId }); + + expect(result).toBeTruthy(); + expect(utils.getBucket).toHaveBeenCalledTimes(1); + expect(s3ClientMock).toHaveReceivedCommandTimes(GetObjectTaggingCommand, 1); + expect(s3ClientMock).toHaveReceivedCommandWith(GetObjectTaggingCommand, { + Bucket: bucket, + Key: filePath, + VersionId: s3VersionId + }); + }); +}); + +describe('headObject', () => { + beforeEach(() => { + s3ClientMock.on(HeadObjectCommand).resolves({}); + }); + + it('should send a head object command', async () => { + const filePath = 'filePath'; + const s3VersionId = '123'; + const result = await service.headObject({ filePath, s3VersionId }); + + expect(result).toBeTruthy(); + expect(utils.getBucket).toHaveBeenCalledTimes(1); + expect(s3ClientMock).toHaveReceivedCommandTimes(HeadObjectCommand, 1); + expect(s3ClientMock).toHaveReceivedCommandWith(HeadObjectCommand, { + Bucket: bucket, + Key: filePath, + VersionId: s3VersionId + }); + }); + + it('should not require a version ID parameter', async () => { + const filePath = 'filePath'; + const s3VersionId = undefined; + const result = await service.headObject({ filePath, s3VersionId }); + + expect(result).toBeTruthy(); + expect(utils.getBucket).toHaveBeenCalledTimes(1); + expect(s3ClientMock).toHaveReceivedCommandTimes(HeadObjectCommand, 1); + expect(s3ClientMock).toHaveReceivedCommandWith(HeadObjectCommand, { + Bucket: bucket, + Key: filePath, + VersionId: s3VersionId + }); + }); +}); + +describe('listAllObjects', () => { + const listObjectsV2Mock = jest.spyOn(service, 'listObjectsV2'); + + beforeEach(() => { + listObjectsV2Mock.mockReset(); + }); + + afterAll(() => { + listObjectsV2Mock.mockRestore(); + }); + + it('should call listObjectsV2 at least once and return an empty array', async () => { + listObjectsV2Mock.mockResolvedValue({ IsTruncated: false }); + + const result = await service.listAllObjects(); + + expect(result).toBeTruthy(); + expect(Array.isArray(result)).toBeTruthy(); + expect(result).toHaveLength(0); + expect(utils.getBucket).toHaveBeenCalledTimes(1); + expect(utils.isAtPath).toHaveBeenCalledTimes(0); + expect(listObjectsV2Mock).toHaveBeenCalledTimes(1); + expect(listObjectsV2Mock).toHaveBeenCalledWith(expect.objectContaining({ + filePath: key + })); + }); + + it('should call listObjectsV2 at least once and return an empty array of objects', async () => { + listObjectsV2Mock.mockResolvedValue({ Contents: [], IsTruncated: false }); + + const result = await service.listAllObjects(); + + expect(result).toBeTruthy(); + expect(Array.isArray(result)).toBeTruthy(); + expect(result).toHaveLength(0); + expect(utils.getBucket).toHaveBeenCalledTimes(1); + expect(utils.isAtPath).toHaveBeenCalledTimes(0); + expect(listObjectsV2Mock).toHaveBeenCalledTimes(1); + expect(listObjectsV2Mock).toHaveBeenCalledWith(expect.objectContaining({ + filePath: key + })); + }); + + it('should call listObjectsV2 multiple times and return an array of precise path objects', async () => { + const continueToken = 'token'; + listObjectsV2Mock.mockResolvedValueOnce({ Contents: [{ Key: 'filePath/foo' }], IsTruncated: true, NextContinuationToken: continueToken }); + listObjectsV2Mock.mockResolvedValueOnce({ Contents: [{ Key: 'filePath/bar' }], IsTruncated: false }); + + const result = await service.listAllObjects(); + + expect(result).toBeTruthy(); + expect(Array.isArray(result)).toBeTruthy(); + expect(result).toHaveLength(2); + expect(result).toEqual(expect.arrayContaining([ + { Key: 'filePath/foo' }, + { Key: 'filePath/bar' } + ])); + expect(utils.getBucket).toHaveBeenCalledTimes(1); + expect(utils.isAtPath).toHaveBeenCalledTimes(2); + expect(listObjectsV2Mock).toHaveBeenCalledTimes(2); + expect(listObjectsV2Mock).toHaveBeenNthCalledWith(1, expect.objectContaining({ + filePath: key + })); + expect(listObjectsV2Mock).toHaveBeenNthCalledWith(2, expect.objectContaining({ + filePath: key, + continuationToken: continueToken + })); + }); + + it('should call listObjectsV2 multiple times and return an array of all path objects', async () => { + const continueToken = 'token'; + listObjectsV2Mock.mockResolvedValueOnce({ Contents: [{ Key: 'filePath/test/foo' }], IsTruncated: true, NextContinuationToken: continueToken }); + listObjectsV2Mock.mockResolvedValueOnce({ Contents: [{ Key: 'filePath/test/bar' }], IsTruncated: false }); + + const result = await service.listAllObjects({ precisePath: false }); + + expect(result).toBeTruthy(); + expect(Array.isArray(result)).toBeTruthy(); + expect(result).toHaveLength(2); + expect(result).toEqual(expect.arrayContaining([ + { Key: 'filePath/test/foo' }, + { Key: 'filePath/test/bar' } + ])); + expect(utils.getBucket).toHaveBeenCalledTimes(1); + expect(utils.isAtPath).toHaveBeenCalledTimes(0); + expect(listObjectsV2Mock).toHaveBeenCalledTimes(2); + expect(listObjectsV2Mock).toHaveBeenNthCalledWith(1, expect.objectContaining({ + filePath: key + })); + expect(listObjectsV2Mock).toHaveBeenNthCalledWith(2, expect.objectContaining({ + filePath: key, + continuationToken: continueToken + })); + }); + + it('should call listObjectsV2 multiple times with the right bucketId and filePath, returning an array of objects', async () => { + const continueToken = 'token'; + const customPath = 'filePath/test'; + listObjectsV2Mock.mockResolvedValueOnce({ Contents: [{ Key: 'filePath/test/foo' }], IsTruncated: true, NextContinuationToken: continueToken }); + listObjectsV2Mock.mockResolvedValueOnce({ Contents: [{ Key: 'filePath/test/bar' }], IsTruncated: false }); + + const result = await service.listAllObjects({ filePath: customPath, bucketId: bucket }); + + expect(result).toBeTruthy(); + expect(Array.isArray(result)).toBeTruthy(); + expect(result).toHaveLength(2); + expect(result).toEqual(expect.arrayContaining([ + { Key: 'filePath/test/foo' }, + { Key: 'filePath/test/bar' } + ])); + expect(utils.getBucket).toHaveBeenCalledTimes(0); + expect(utils.isAtPath).toHaveBeenCalledTimes(2); + expect(listObjectsV2Mock).toHaveBeenCalledTimes(2); + expect(listObjectsV2Mock).toHaveBeenNthCalledWith(1, expect.objectContaining({ + filePath: customPath, + bucketId: bucket + })); + expect(listObjectsV2Mock).toHaveBeenNthCalledWith(2, expect.objectContaining({ + filePath: customPath, + continuationToken: continueToken, + bucketId: bucket + })); + }); +}); + +describe('listAllObjectVersions', () => { + const listObjectVersionMock = jest.spyOn(service, 'listObjectVersion'); + const bucketId = 'abc'; + + beforeEach(() => { + listObjectVersionMock.mockReset(); + }); + + afterAll(() => { + listObjectVersionMock.mockRestore(); + }); + + it('should call listObjectVersion at least once and yield empty arrays', async () => { + listObjectVersionMock.mockResolvedValue({ IsTruncated: false }); + + const result = await service.listAllObjectVersions({ filePath: '/' }); + + expect(result).toBeTruthy(); + expect(Array.isArray(result.DeleteMarkers)).toBeTruthy(); + expect(result.DeleteMarkers).toHaveLength(0); + expect(Array.isArray(result.Versions)).toBeTruthy(); + expect(result.Versions).toHaveLength(0); + expect(utils.getBucket).toHaveBeenCalledTimes(0); + expect(utils.isAtPath).toHaveBeenCalledTimes(0); + expect(listObjectVersionMock).toHaveBeenCalledTimes(1); + expect(listObjectVersionMock).toHaveBeenCalledWith(expect.objectContaining({ + filePath: '' + })); + }); + + it('should call listObjectVersion at least once with bucket lookup and yield empty arrays', async () => { + utils.getBucket.mockResolvedValue({ key: key }); + listObjectVersionMock.mockResolvedValue({ IsTruncated: false }); + + const result = await service.listAllObjectVersions({ bucketId: bucketId }); + + expect(result).toBeTruthy(); + expect(Array.isArray(result.DeleteMarkers)).toBeTruthy(); + expect(result.DeleteMarkers).toHaveLength(0); + expect(Array.isArray(result.Versions)).toBeTruthy(); + expect(result.Versions).toHaveLength(0); + expect(utils.getBucket).toHaveBeenCalledTimes(1); + expect(utils.getBucket).toHaveBeenCalledWith(bucketId); + expect(utils.isAtPath).toHaveBeenCalledTimes(0); + expect(listObjectVersionMock).toHaveBeenCalledTimes(1); + expect(listObjectVersionMock).toHaveBeenCalledWith(expect.objectContaining({ + filePath: key, + bucketId: bucketId + })); + }); + + it('should call listObjectVersion multiple times and return precise path objects', async () => { + const nextKeyMarker = 'token'; + listObjectVersionMock.mockResolvedValueOnce({ DeleteMarkers: [{ Key: 'filePath/foo' }], IsTruncated: true, NextKeyMarker: nextKeyMarker }); + listObjectVersionMock.mockResolvedValueOnce({ Versions: [{ Key: 'filePath/bar' }], IsTruncated: false }); + + const result = await service.listAllObjectVersions({ filePath: 'filePath' }); + + expect(result).toBeTruthy(); + expect(Array.isArray(result.DeleteMarkers)).toBeTruthy(); + expect(result.DeleteMarkers).toHaveLength(1); + expect(result.DeleteMarkers).toEqual(expect.arrayContaining([ + { Key: 'filePath/foo' } + ])); + expect(Array.isArray(result.Versions)).toBeTruthy(); + expect(result.Versions).toHaveLength(1); + expect(result.Versions).toEqual(expect.arrayContaining([ + { Key: 'filePath/bar' } + ])); + expect(utils.getBucket).toHaveBeenCalledTimes(0); + expect(utils.isAtPath).toHaveBeenCalledTimes(2); + expect(listObjectVersionMock).toHaveBeenCalledTimes(2); + expect(listObjectVersionMock).toHaveBeenNthCalledWith(1, expect.objectContaining({ + filePath: key + })); + expect(listObjectVersionMock).toHaveBeenNthCalledWith(2, expect.objectContaining({ + filePath: key, + keyMarker: nextKeyMarker + })); + }); + + it('should call listObjectVersion multiple times and return all path objects', async () => { + const nextKeyMarker = 'token'; + listObjectVersionMock.mockResolvedValueOnce({ DeleteMarkers: [{ Key: 'filePath/test/foo' }], IsTruncated: true, NextKeyMarker: nextKeyMarker }); + listObjectVersionMock.mockResolvedValueOnce({ Versions: [{ Key: 'filePath/test/bar' }], IsTruncated: false }); + + const result = await service.listAllObjectVersions({ filePath: 'filePath', precisePath: false }); + + expect(result).toBeTruthy(); + expect(Array.isArray(result.DeleteMarkers)).toBeTruthy(); + expect(result.DeleteMarkers).toHaveLength(1); + expect(result.DeleteMarkers).toEqual(expect.arrayContaining([ + { Key: 'filePath/test/foo' } + ])); + expect(Array.isArray(result.Versions)).toBeTruthy(); + expect(result.Versions).toHaveLength(1); + expect(result.Versions).toEqual(expect.arrayContaining([ + { Key: 'filePath/test/bar' } + ])); + expect(utils.getBucket).toHaveBeenCalledTimes(0); + expect(utils.isAtPath).toHaveBeenCalledTimes(0); + expect(listObjectVersionMock).toHaveBeenCalledTimes(2); + expect(listObjectVersionMock).toHaveBeenNthCalledWith(1, expect.objectContaining({ + filePath: key + })); + expect(listObjectVersionMock).toHaveBeenNthCalledWith(2, expect.objectContaining({ + filePath: key, + keyMarker: nextKeyMarker + })); + }); + + it('should call listObjectVersion multiple times and return all latest path objects', async () => { + const nextKeyMarker = 'token'; + listObjectVersionMock.mockResolvedValueOnce({ DeleteMarkers: [{ Key: 'filePath/test/foo', IsLatest: true }], IsTruncated: true, NextKeyMarker: nextKeyMarker }); + listObjectVersionMock.mockResolvedValueOnce({ Versions: [{ Key: 'filePath/test/bar', IsLatest: false }], IsTruncated: false }); + + const result = await service.listAllObjectVersions({ filePath: 'filePath', precisePath: false, filterLatest: true }); + + expect(result).toBeTruthy(); + expect(Array.isArray(result.DeleteMarkers)).toBeTruthy(); + expect(result.DeleteMarkers).toHaveLength(1); + expect(result.DeleteMarkers).toEqual(expect.arrayContaining([ + { Key: 'filePath/test/foo', IsLatest: true } + ])); + expect(Array.isArray(result.Versions)).toBeTruthy(); + expect(result.Versions).toHaveLength(0); + expect(utils.getBucket).toHaveBeenCalledTimes(0); + expect(utils.isAtPath).toHaveBeenCalledTimes(0); + expect(listObjectVersionMock).toHaveBeenCalledTimes(2); + expect(listObjectVersionMock).toHaveBeenNthCalledWith(1, expect.objectContaining({ + filePath: key + })); + expect(listObjectVersionMock).toHaveBeenNthCalledWith(2, expect.objectContaining({ + filePath: key, + keyMarker: nextKeyMarker + })); + }); +}); + +describe('listObjectsV2', () => { + beforeEach(() => { + s3ClientMock.on(ListObjectsV2Command).resolves({}); + }); + + it('should send a list objects command with default undefined maxKeys', async () => { + const filePath = 'filePath'; + const result = await service.listObjectsV2({ filePath }); + + expect(result).toBeTruthy(); + expect(utils.getBucket).toHaveBeenCalledTimes(1); + expect(s3ClientMock).toHaveReceivedCommandTimes(ListObjectsV2Command, 1); + expect(s3ClientMock).toHaveReceivedCommandWith(ListObjectsV2Command, { + Bucket: bucket, + Prefix: filePath, + MaxKeys: undefined + }); + }); + + it('should send a list objects command with 200 maxKeys', async () => { + const filePath = 'filePath'; + const maxKeys = 200; + const result = await service.listObjectsV2({ filePath, maxKeys }); + + expect(result).toBeTruthy(); + expect(utils.getBucket).toHaveBeenCalledTimes(1); + expect(s3ClientMock).toHaveReceivedCommandTimes(ListObjectsV2Command, 1); + expect(s3ClientMock).toHaveReceivedCommandWith(ListObjectsV2Command, { + Bucket: bucket, + Prefix: filePath, + MaxKeys: maxKeys + }); + }); +}); + +describe('listObjectVersion', () => { + beforeEach(() => { + s3ClientMock.on(ListObjectVersionsCommand).resolves({}); + }); + + it('should send a list object versions command', async () => { + const filePath = 'filePath'; + const result = await service.listObjectVersion({ filePath }); + + expect(result).toBeTruthy(); + expect(utils.getBucket).toHaveBeenCalledTimes(1); + expect(s3ClientMock).toHaveReceivedCommandTimes(ListObjectVersionsCommand, 1); + expect(s3ClientMock).toHaveReceivedCommandWith(ListObjectVersionsCommand, { + Bucket: bucket, + Prefix: filePath + }); + }); +}); + +describe('presignUrl', () => { + beforeEach(() => { + getSignedUrl.mockReset(); + }); + + afterAll(() => { + getSignedUrl.mockRestore(); + }); + + it('should call getSignedUrl with default expiry', async () => { + getSignedUrl.mockResolvedValue('foo'); + const command = {}; + const result = await service.presignUrl(command); + + expect(result).toBeTruthy(); + expect(utils.getBucket).toHaveBeenCalledTimes(1); + expect(getSignedUrl).toHaveBeenCalledTimes(1); + expect(getSignedUrl).toHaveBeenCalledWith(expect.any(Object), command, { expiresIn: defaultTempExpiresIn }); + }); + + it('should call getSignedUrl with custom expiry', async () => { + getSignedUrl.mockResolvedValue('foo'); + const command = {}; + const expiresIn = 1234; + const result = await service.presignUrl(command, expiresIn); + + expect(result).toBeTruthy(); + expect(utils.getBucket).toHaveBeenCalledTimes(1); + expect(getSignedUrl).toHaveBeenCalledTimes(1); + expect(getSignedUrl).toHaveBeenCalledWith(expect.any(Object), command, { expiresIn: expiresIn }); + }); +}); + +describe('putBucketEncryption', () => { + beforeEach(() => { + s3ClientMock.on(PutBucketEncryptionCommand).resolves({}); + }); + + it('should send a get bucket encryption command', async () => { + const result = await service.putBucketEncryption(); + + expect(result).toBeTruthy(); + expect(utils.getBucket).toHaveBeenCalledTimes(1); + expect(s3ClientMock).toHaveReceivedCommandTimes(PutBucketEncryptionCommand, 1); + expect(s3ClientMock).toHaveReceivedCommandWith(PutBucketEncryptionCommand, { + Bucket: bucket, + ServerSideEncryptionConfiguration: { + Rules: [{ + ApplyServerSideEncryptionByDefault: { + SSEAlgorithm: 'AES256' + } + }] + } + }); + }); +}); + +describe('putObject', () => { + const name = 'foo.txt'; + const keyPath = utils.joinPath(key, name); + const length = 123; + + beforeEach(() => { + s3ClientMock.on(PutObjectCommand).resolves({}); + }); + + it('should send a put object command', async () => { + const stream = new Readable(); + const mimeType = 'mimeType'; + const result = await service.putObject({ stream, name, length, mimeType }); + + expect(result).toBeTruthy(); + expect(utils.getBucket).toHaveBeenCalledTimes(1); + expect(s3ClientMock).toHaveReceivedCommandTimes(PutObjectCommand, 1); + expect(s3ClientMock).toHaveReceivedCommandWith(PutObjectCommand, { + Bucket: bucket, + ContentLength: length, + ContentType: mimeType, + Key: keyPath, + Body: stream + }); + }); + + it('should send a put object command with custom metadata', async () => { + const stream = new Readable(); + const mimeType = 'mimeType'; + const metadata = { foo: 'foo', bar: 'bar' }; + const result = await service.putObject({ stream, name, length, mimeType, metadata }); + + expect(result).toBeTruthy(); + expect(utils.getBucket).toHaveBeenCalledTimes(1); + expect(s3ClientMock).toHaveReceivedCommandTimes(PutObjectCommand, 1); + expect(s3ClientMock).toHaveReceivedCommandWith(PutObjectCommand, { + Bucket: bucket, + ContentLength: length, + ContentType: mimeType, + Key: keyPath, + Body: stream, + Metadata: metadata + }); + }); + + it('should send a put object command with custom tags', async () => { + const stream = new Readable(); + const mimeType = 'mimeType'; + const tags = { foo: 'foo', bar: 'bar' }; + const result = await service.putObject({ stream, name, length, mimeType, tags }); + + expect(result).toBeTruthy(); + expect(utils.getBucket).toHaveBeenCalledTimes(1); + expect(s3ClientMock).toHaveReceivedCommandTimes(PutObjectCommand, 1); + expect(s3ClientMock).toHaveReceivedCommandWith(PutObjectCommand, { + Bucket: bucket, + ContentLength: length, + ContentType: mimeType, + Key: keyPath, + Body: stream, + Tagging: 'foo=foo&bar=bar' + }); + }); +}); + +describe('putObjectTagging', () => { + beforeEach(() => { + s3ClientMock.on(PutObjectTaggingCommand).resolves({}); + }); + + it('should send a put object tagging command', async () => { + const filePath = 'filePath'; + const tags = [{ Key: 'abc', Value: '123' }]; + const result = await service.putObjectTagging({ filePath, tags }); + + expect(result).toBeTruthy(); + expect(utils.getBucket).toHaveBeenCalledTimes(1); + expect(s3ClientMock).toHaveReceivedCommandTimes(PutObjectTaggingCommand, 1); + expect(s3ClientMock).toHaveReceivedCommandWith(PutObjectTaggingCommand, { + Bucket: bucket, + Key: filePath, + Tagging: { + TagSet: [{ Key: 'abc', Value: '123' }] + }, + VersionId: undefined + }); + }); + + it('should send a put object tagging command for a specific version', async () => { + const filePath = 'filePath'; + const s3VersionId = '1234'; + const tags = [{ Key: 'abc', Value: '123' }]; + const result = await service.putObjectTagging({ filePath, tags, s3VersionId }); + + expect(result).toBeTruthy(); + expect(utils.getBucket).toHaveBeenCalledTimes(1); + expect(s3ClientMock).toHaveReceivedCommandTimes(PutObjectTaggingCommand, 1); + expect(s3ClientMock).toHaveReceivedCommandWith(PutObjectTaggingCommand, { + Bucket: bucket, + Key: filePath, + Tagging: { + TagSet: [{ Key: 'abc', Value: '123' }] + }, + VersionId: s3VersionId + }); + }); +}); + +describe('readObject', () => { + beforeEach(() => { + s3ClientMock.on(GetObjectCommand).resolves({}); + }); + + it('should send a get object command for the latest object', async () => { + const filePath = 'filePath'; + const result = await service.readObject({ filePath }); + + expect(result).toBeTruthy(); + expect(utils.getBucket).toHaveBeenCalledTimes(1); + expect(s3ClientMock).toHaveReceivedCommandTimes(GetObjectCommand, 1); + expect(s3ClientMock).toHaveReceivedCommandWith(GetObjectCommand, { + Bucket: bucket, + Key: filePath, + VersionId: undefined + }); + }); + + it('should send a get object command for a specific version', async () => { + const filePath = 'filePath'; + const s3VersionId = '1234'; + const result = await service.readObject({ filePath, s3VersionId }); + + expect(result).toBeTruthy(); + expect(utils.getBucket).toHaveBeenCalledTimes(1); + expect(s3ClientMock).toHaveReceivedCommandTimes(GetObjectCommand, 1); + expect(s3ClientMock).toHaveReceivedCommandWith(GetObjectCommand, { + Bucket: bucket, + Key: filePath, + VersionId: s3VersionId + }); + }); +}); + +// TODO: Figure out a way to intercept GetObjectCommand constructor parameters for higher level validation +describe('readSignedUrl', () => { + const presignUrlMock = jest.spyOn(service, 'presignUrl'); + + beforeEach(() => { + presignUrlMock.mockResolvedValue('url'); + }); + + afterAll(() => { + presignUrlMock.mockRestore(); + }); + + it('should call presignUrl with a get object command for the latest object and default expiration and bucketId', async () => { + const filePath = 'filePath'; + const bucketId = 'abc'; + const result = await service.readSignedUrl({ filePath, bucketId: bucketId }); + + expect(result).toBeTruthy(); + expect(utils.getBucket).toHaveBeenCalledTimes(1); + expect(presignUrlMock).toHaveBeenCalledTimes(1); + expect(presignUrlMock).toHaveBeenCalledWith(expect.objectContaining({ + input: { + Bucket: bucket, + Key: filePath + } + }), defaultTempExpiresIn, bucketId); + }); + + it('should call presignUrl with a get object command for a specific version and default expiration', async () => { + const filePath = 'filePath'; + const s3VersionId = '1234'; + const bucketId = 'abc'; + const result = await service.readSignedUrl({ filePath, s3VersionId, bucketId }); + + expect(result).toBeTruthy(); + expect(utils.getBucket).toHaveBeenCalledTimes(1); + expect(presignUrlMock).toHaveBeenCalledTimes(1); + expect(presignUrlMock).toHaveBeenCalledWith(expect.objectContaining({ + input: { + Bucket: bucket, + Key: filePath, + VersionId: s3VersionId + } + }), defaultTempExpiresIn, bucketId); + }); + + it('should call presignUrl with a get object command for a specific version and custom expiration', async () => { + const filePath = 'filePath'; + const s3VersionId = '1234'; + const expires = '2345'; + const bucketId = 'abc'; + const result = await service.readSignedUrl({ filePath, s3VersionId, expiresIn: expires, bucketId }); + + expect(result).toBeTruthy(); + expect(utils.getBucket).toHaveBeenCalledTimes(1); + expect(presignUrlMock).toHaveBeenCalledTimes(1); + expect(presignUrlMock).toHaveBeenCalledWith(expect.objectContaining({ + input: { + Bucket: bucket, + Key: filePath, + VersionId: s3VersionId + } + }), expires, bucketId); + }); +}); + +describe('upload', () => { + const id = 'id'; + + beforeEach(() => { + s3ClientMock.on(PutObjectCommand).resolves({}); + s3ClientMock.on(PutObjectTaggingCommand).resolves({}); + }); + + afterEach(() => { + jest.resetAllMocks(); // TODO: Figure out why we can't do this at top level? + }); + + it('should send a put object command', async () => { + const stream = new Readable({ + read() { + this.push(null); // End the stream + } + }); + const mimeType = 'mimeType'; + const metadata = { 'coms-name': 'originalName', 'coms-id': id }; + const result = await service.upload({ stream, id, mimeType, metadata }); + + expect(result).toBeTruthy(); + expect(utils.getBucket).toHaveBeenCalledTimes(1); + expect(s3ClientMock).toHaveReceivedCommandTimes(PutObjectCommand, 1); + expect(s3ClientMock).toHaveReceivedCommandWith(PutObjectCommand, { + Bucket: bucket, + ContentType: mimeType, + Key: expect.any(String), // TODO: Fix after getPath is refactored + Body: expect.any(Object), + Metadata: metadata, + }); + }); + + it('should send a put object and put object tagging command', async () => { + const stream = new Readable({ + read() { + this.push(null); // End the stream + } + }); + const mimeType = 'mimeType'; + const metadata = { 'coms-name': 'originalName', 'coms-id': id }; + const tags = { foo: 'bar' }; + const result = await service.upload({ stream, id, mimeType, metadata, tags }); + + expect(result).toBeTruthy(); + expect(utils.getBucket).toHaveBeenCalledTimes(1); + expect(s3ClientMock).toHaveReceivedCommandTimes(PutObjectCommand, 1); + expect(s3ClientMock).toHaveReceivedNthCommandWith(1, PutObjectCommand, { + Bucket: bucket, + ContentType: mimeType, + Key: expect.any(String), // TODO: Fix after getPath is refactored + Body: expect.any(Buffer), + Metadata: metadata, + Tagging: 'foo=bar' + }); + }); +}); diff --git a/comsapi/app/tests/unit/services/sync.spec.js b/comsapi/app/tests/unit/services/sync.spec.js new file mode 100644 index 00000000..736f9851 --- /dev/null +++ b/comsapi/app/tests/unit/services/sync.spec.js @@ -0,0 +1,1310 @@ +const { resetModel, trxBuilder } = require('../../common/helper'); +const utils = require('../../../src/db/models/utils'); +const ObjectModel = require('../../../src/db/models/tables/objectModel'); +const Version = require('../../../src/db/models/tables/version'); + +const objectModelTrx = trxBuilder(); +jest.mock('../../../src/db/models/tables/objectModel', () => ({ + startTransaction: jest.fn(), + then: jest.fn() +})); + +const versionTrx = trxBuilder(); +jest.mock('../../../src/db/models/tables/version', () => ({ + delete: jest.fn(), + query: jest.fn(), + startTransaction: jest.fn(), + then: jest.fn(), + where: jest.fn(), + whereIn: jest.fn(), + whereNotNull: jest.fn(), +})); + +const { + objectService, + metadataService, + storageService, + tagService, + versionService +} = require('../../../src/services'); +const service = require('../../../src/services/sync'); + +const bucketId = 'bucketId'; +const path = 'path'; +const uuidv4Regex = /^[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i; +const validUuidv4 = '3f4da093-6399-4711-8765-36ec5f8017c2'; + +// Shared Spy Scopes +const getSpy = jest.spyOn(versionService, 'get'); +const getObjectTaggingSpy = jest.spyOn(storageService, 'getObjectTagging'); +const headObjectSpy = jest.spyOn(storageService, 'headObject'); +const listAllObjectVersionsSpy = jest.spyOn(storageService, 'listAllObjectVersions'); +const putObjectTaggingSpy = jest.spyOn(storageService, 'putObjectTagging'); + +beforeEach(() => { + jest.clearAllMocks(); + resetModel(ObjectModel, objectModelTrx); + resetModel(Version, versionTrx); + + getSpy.mockReset(); + getObjectTaggingSpy.mockReset(); + headObjectSpy.mockReset(); + listAllObjectVersionsSpy.mockReset(); + putObjectTaggingSpy.mockReset(); +}); + +afterAll(() => { // Mockrestores must only happen after suite is completed + getSpy.mockRestore(); + getObjectTaggingSpy.mockRestore(); + headObjectSpy.mockRestore(); + listAllObjectVersionsSpy.mockRestore(); + putObjectTaggingSpy.mockRestore(); +}); + +describe('_deriveObjectId', () => { + describe('Regular S3 Object', () => { + it('Returns an existing coms-id if valid', async () => { + getObjectTaggingSpy.mockResolvedValue({ + TagSet: [{ Key: 'coms-id', Value: validUuidv4 }] + }); + + const result = await service._deriveObjectId({}, path, bucketId); + + expect(result).toBeTruthy(); + expect(typeof result).toBe('string'); + expect(result).toMatch(validUuidv4); + expect(getObjectTaggingSpy).toHaveBeenCalledTimes(1); + expect(getObjectTaggingSpy).toHaveBeenCalledWith(expect.objectContaining({ + filePath: path, + bucketId: bucketId + })); + expect(listAllObjectVersionsSpy).toHaveBeenCalledTimes(0); + expect(putObjectTaggingSpy).toHaveBeenCalledTimes(0); + }); + + it('Returns a new uuid if invalid and pushes tags when less than 10 present', async () => { + getObjectTaggingSpy.mockResolvedValue({ + TagSet: [{ Key: 'coms-id', Value: 'garbage' }] + }); + + const result = await service._deriveObjectId({}, path, bucketId); + + expect(result).toBeTruthy(); + expect(typeof result).toBe('string'); + expect(result).toMatch(uuidv4Regex); + expect(getObjectTaggingSpy).toHaveBeenCalledTimes(1); + expect(getObjectTaggingSpy).toHaveBeenCalledWith(expect.objectContaining({ + filePath: path, + bucketId: bucketId + })); + expect(listAllObjectVersionsSpy).toHaveBeenCalledTimes(0); + expect(putObjectTaggingSpy).toHaveBeenCalledTimes(1); + expect(putObjectTaggingSpy).toHaveBeenCalledWith(expect.objectContaining({ + filePath: path, + bucketId: bucketId, + tags: expect.any(Array) + })); + }); + + it('Returns a new uuid if unavailable and pushes tags when less than 10 present', async () => { + getObjectTaggingSpy.mockResolvedValue({ TagSet: [] }); + + const result = await service._deriveObjectId({}, path, bucketId); + + expect(result).toBeTruthy(); + expect(typeof result).toBe('string'); + expect(result).toMatch(uuidv4Regex); + expect(getObjectTaggingSpy).toHaveBeenCalledTimes(1); + expect(getObjectTaggingSpy).toHaveBeenCalledWith(expect.objectContaining({ + filePath: path, + bucketId: bucketId + })); + expect(listAllObjectVersionsSpy).toHaveBeenCalledTimes(0); + expect(putObjectTaggingSpy).toHaveBeenCalledTimes(1); + expect(putObjectTaggingSpy).toHaveBeenCalledWith(expect.objectContaining({ + filePath: path, + bucketId: bucketId, + tags: expect.any(Array) + })); + }); + + it('Returns an existing coms-id if found', async () => { + getObjectTaggingSpy.mockResolvedValue({ TagSet: [{}, {}, {}, {}, {}, {}, {}, {}, {}, {}] }); + + const result = await service._deriveObjectId({}, path, bucketId); + + expect(result).toBeTruthy(); + expect(typeof result).toBe('string'); + expect(result).toMatch(uuidv4Regex); + expect(getObjectTaggingSpy).toHaveBeenCalledTimes(1); + expect(getObjectTaggingSpy).toHaveBeenCalledWith(expect.objectContaining({ + filePath: path, + bucketId: bucketId + })); + expect(listAllObjectVersionsSpy).toHaveBeenCalledTimes(0); + expect(putObjectTaggingSpy).toHaveBeenCalledTimes(0); + }); + }); + + describe('Soft-Deleted S3 Object', () => { + it('Returns a new uuid if valid found', async () => { + listAllObjectVersionsSpy.mockResolvedValue({ Versions: [{ VersionId: '2' }, { VersionId: '1' }] }); + getObjectTaggingSpy.mockResolvedValueOnce({ TagSet: [] }); + getObjectTaggingSpy.mockResolvedValueOnce({ + TagSet: [{ Key: 'coms-id', Value: validUuidv4 }] + }); + + const result = await service._deriveObjectId(true, path, bucketId); + + expect(result).toBeTruthy(); + expect(typeof result).toBe('string'); + expect(result).toMatch(validUuidv4); + expect(getObjectTaggingSpy).toHaveBeenCalledTimes(2); + expect(listAllObjectVersionsSpy).toHaveBeenCalledTimes(1); + expect(listAllObjectVersionsSpy).toHaveBeenCalledWith(expect.objectContaining({ + filePath: path, + bucketId: bucketId + })); + expect(putObjectTaggingSpy).toHaveBeenCalledTimes(0); + }); + + it('Returns a new uuid if valid not found', async () => { + listAllObjectVersionsSpy.mockResolvedValue({ Versions: [{ VersionId: '1' }] }); + getObjectTaggingSpy.mockResolvedValueOnce({ TagSet: [] }); + + const result = await service._deriveObjectId(true, path, bucketId); + + expect(result).toBeTruthy(); + expect(typeof result).toBe('string'); + expect(result).toMatch(uuidv4Regex); + expect(getObjectTaggingSpy).toHaveBeenCalledTimes(1); + expect(listAllObjectVersionsSpy).toHaveBeenCalledTimes(1); + expect(listAllObjectVersionsSpy).toHaveBeenCalledWith(expect.objectContaining({ + filePath: path, + bucketId: bucketId + })); + expect(putObjectTaggingSpy).toHaveBeenCalledTimes(0); + }); + }); + + describe('Unexpected S3 Object definition', () => { + it('Returns a new uuid for all other cases', async () => { + const result = await service._deriveObjectId(undefined, path, bucketId); + + expect(result).toBeTruthy(); + expect(typeof result).toBe('string'); + expect(result).toMatch(uuidv4Regex); + expect(getObjectTaggingSpy).toHaveBeenCalledTimes(0); + expect(listAllObjectVersionsSpy).toHaveBeenCalledTimes(0); + expect(putObjectTaggingSpy).toHaveBeenCalledTimes(0); + }); + }); +}); + +describe('syncJob', () => { + const trxWrapperSpy = jest.spyOn(utils, 'trxWrapper'); + const syncObjectSpy = jest.spyOn(service, 'syncObject'); + const syncVersionsSpy = jest.spyOn(service, 'syncVersions'); + const syncTagsSpy = jest.spyOn(service, 'syncTags'); + const syncMetadataSpy = jest.spyOn(service, 'syncMetadata'); + + beforeEach(() => { + syncObjectSpy.mockReset(); + syncVersionsSpy.mockReset(); + syncTagsSpy.mockReset(); + syncMetadataSpy.mockReset(); + trxWrapperSpy.mockReset(); + + trxWrapperSpy.mockImplementation(callback => callback({})); + }); + + afterAll(() => { + syncObjectSpy.mockRestore(); + syncVersionsSpy.mockRestore(); + syncTagsSpy.mockRestore(); + syncMetadataSpy.mockRestore(); + trxWrapperSpy.mockRestore(); + }); + + it('Throws when path is not defined', () => { + const result = (() => service.syncJob(undefined, bucketId))(); + + expect(result).rejects.toThrow(); + expect(syncObjectSpy).toHaveBeenCalledTimes(0); + expect(syncVersionsSpy).toHaveBeenCalledTimes(0); + expect(syncTagsSpy).toHaveBeenCalledTimes(0); + expect(syncMetadataSpy).toHaveBeenCalledTimes(0); + }); + + it('Only calls syncObject when object is deleted from S3', async () => { + syncObjectSpy.mockResolvedValue({ modified: false, object: undefined }); + + const result = await service.syncJob(path, bucketId); + + expect(result).toBeUndefined(); + expect(syncObjectSpy).toHaveBeenCalledTimes(1); + expect(syncObjectSpy).toHaveBeenCalledWith(path, bucketId, expect.any(String), expect.any(Object)); + expect(syncVersionsSpy).toHaveBeenCalledTimes(0); + expect(syncTagsSpy).toHaveBeenCalledTimes(0); + expect(syncMetadataSpy).toHaveBeenCalledTimes(0); + }); + + it('Always calls at syncObject, syncVersions and syncTags', async () => { + syncObjectSpy.mockResolvedValue({ modified: true, object: { id: validUuidv4 } }); + syncVersionsSpy.mockResolvedValue([{ modified: false, version: {} }]); + syncTagsSpy.mockResolvedValue([]); + syncMetadataSpy.mockResolvedValue([]); + + const result = await service.syncJob(path, bucketId); + + expect(result).toMatch(validUuidv4); + expect(syncObjectSpy).toHaveBeenCalledTimes(1); + expect(syncObjectSpy).toHaveBeenCalledWith(path, bucketId, expect.any(String), expect.any(Object)); + expect(syncVersionsSpy).toHaveBeenCalledTimes(1); + expect(syncVersionsSpy).toHaveBeenCalledWith(expect.any(Object), expect.any(String), expect.any(Object)); + expect(syncTagsSpy).toHaveBeenCalledTimes(1); + expect(syncTagsSpy).toHaveBeenCalledWith(expect.any(Object), path, bucketId, expect.any(String), expect.any(Object)); + expect(syncMetadataSpy).toHaveBeenCalledTimes(0); + }); + + it('Calls syncTags and syncMetadata when version modified', async () => { + syncObjectSpy.mockResolvedValue({ modified: true, object: { id: validUuidv4 } }); + syncVersionsSpy.mockResolvedValue([{ modified: true, version: {} }]); + syncTagsSpy.mockResolvedValue([]); + syncMetadataSpy.mockResolvedValue([]); + + const result = await service.syncJob(path, bucketId); + + expect(result).toMatch(validUuidv4); + expect(syncObjectSpy).toHaveBeenCalledTimes(1); + expect(syncObjectSpy).toHaveBeenCalledWith(path, bucketId, expect.any(String), expect.any(Object)); + expect(syncVersionsSpy).toHaveBeenCalledTimes(1); + expect(syncVersionsSpy).toHaveBeenCalledWith(expect.any(Object), expect.any(String), expect.any(Object)); + expect(syncTagsSpy).toHaveBeenCalledTimes(1); + expect(syncTagsSpy).toHaveBeenCalledWith(expect.any(Object), path, bucketId, expect.any(String), expect.any(Object)); + expect(syncMetadataSpy).toHaveBeenCalledTimes(1); + expect(syncMetadataSpy).toHaveBeenCalledWith(expect.any(Object), path, bucketId, expect.any(String), expect.any(Object)); + }); + + it('Calls everything when full mode', async () => { + syncObjectSpy.mockResolvedValue({ modified: false, object: { id: validUuidv4 } }); + syncVersionsSpy.mockResolvedValue([{ modified: false, version: {} }]); + syncTagsSpy.mockResolvedValue([]); + syncMetadataSpy.mockResolvedValue([]); + + const result = await service.syncJob(path, bucketId, true); + + expect(result).toMatch(validUuidv4); + expect(syncObjectSpy).toHaveBeenCalledTimes(1); + expect(syncObjectSpy).toHaveBeenCalledWith(path, bucketId, expect.any(String), expect.any(Object)); + expect(syncVersionsSpy).toHaveBeenCalledTimes(1); + expect(syncVersionsSpy).toHaveBeenCalledWith(expect.any(Object), expect.any(String), expect.any(Object)); + expect(syncTagsSpy).toHaveBeenCalledTimes(1); + expect(syncTagsSpy).toHaveBeenCalledWith(expect.any(Object), path, bucketId, expect.any(String), expect.any(Object)); + expect(syncMetadataSpy).toHaveBeenCalledTimes(1); + expect(syncMetadataSpy).toHaveBeenCalledWith(expect.any(Object), path, bucketId, expect.any(String), expect.any(Object)); + }); +}); + +describe('syncObject', () => { + const _deriveObjectIdSpy = jest.spyOn(service, '_deriveObjectId'); + const createSpy = jest.spyOn(objectService, 'create'); + const deleteSpy = jest.spyOn(objectService, 'delete'); + const pruneOrphanedMetadataSpy = jest.spyOn(metadataService, 'pruneOrphanedMetadata'); + const pruneOrphanedTagsSpy = jest.spyOn(tagService, 'pruneOrphanedTags'); + const searchObjectsSpy = jest.spyOn(objectService, 'searchObjects'); + + beforeEach(() => { + _deriveObjectIdSpy.mockReset(); + createSpy.mockReset(); + deleteSpy.mockReset(); + pruneOrphanedMetadataSpy.mockReset(); + pruneOrphanedTagsSpy.mockReset(); + searchObjectsSpy.mockReset(); + }); + + afterAll(() => { + _deriveObjectIdSpy.mockRestore(); + createSpy.mockRestore(); + deleteSpy.mockRestore(); + pruneOrphanedMetadataSpy.mockRestore(); + pruneOrphanedTagsSpy.mockRestore(); + searchObjectsSpy.mockRestore(); + }); + + it('should return object when already synced', async () => { + const comsObject = { id: validUuidv4 }; + headObjectSpy.mockResolvedValue({}); + searchObjectsSpy.mockResolvedValue([comsObject]); + + const result = await service.syncObject(path, bucketId); + + expect(result).toBeTruthy(); + expect(result.modified).toBeFalsy(); + expect(result.object).toEqual(comsObject); + + expect(ObjectModel.startTransaction).toHaveBeenCalledTimes(1); + expect(_deriveObjectIdSpy).toHaveBeenCalledTimes(0); + expect(createSpy).toHaveBeenCalledTimes(0); + expect(deleteSpy).toHaveBeenCalledTimes(0); + expect(headObjectSpy).toHaveBeenCalledTimes(1); + expect(headObjectSpy).toHaveBeenCalledWith(expect.objectContaining({ + filePath: path, bucketId: bucketId + })); + expect(pruneOrphanedMetadataSpy).toHaveBeenCalledTimes(0); + expect(pruneOrphanedTagsSpy).toHaveBeenCalledTimes(0); + expect(searchObjectsSpy).toHaveBeenCalledTimes(1); + expect(searchObjectsSpy).toHaveBeenCalledWith(expect.objectContaining({ + path: path, bucketId: bucketId + }), expect.any(Object)); + expect(objectModelTrx.commit).toHaveBeenCalledTimes(1); + }); + + it('should insert new object when not in COMS', async () => { + const comsObject = {}; + _deriveObjectIdSpy.mockResolvedValue(validUuidv4); + createSpy.mockResolvedValue(comsObject); + headObjectSpy.mockResolvedValue({}); + searchObjectsSpy.mockResolvedValue(undefined); + + const result = await service.syncObject(path, bucketId); + + expect(result).toBeTruthy(); + expect(result.modified).toBeTruthy(); + expect(result.object).toEqual(comsObject); + + expect(ObjectModel.startTransaction).toHaveBeenCalledTimes(1); + expect(_deriveObjectIdSpy).toHaveBeenCalledTimes(1); + expect(_deriveObjectIdSpy).toHaveBeenCalledWith(expect.any(Object), path, bucketId); + expect(createSpy).toHaveBeenCalledTimes(1); + expect(createSpy).toHaveBeenCalledWith(expect.objectContaining({ + id: validUuidv4, + name: path.match(/(?!.*\/)(.*)$/)[0], + path: path, + bucketId: bucketId, + userId: expect.any(String) + }), expect.any(Object)); + expect(deleteSpy).toHaveBeenCalledTimes(0); + expect(headObjectSpy).toHaveBeenCalledTimes(1); + expect(headObjectSpy).toHaveBeenCalledWith(expect.objectContaining({ + filePath: path, bucketId: bucketId + })); + expect(pruneOrphanedMetadataSpy).toHaveBeenCalledTimes(0); + expect(pruneOrphanedTagsSpy).toHaveBeenCalledTimes(0); + expect(searchObjectsSpy).toHaveBeenCalledTimes(1); + expect(searchObjectsSpy).toHaveBeenCalledWith(expect.objectContaining({ + path: path, bucketId: bucketId + }), expect.any(Object)); + expect(objectModelTrx.commit).toHaveBeenCalledTimes(1); + }); + + it('should drop COMS object when not in S3', async () => { + const comsObject = { id: validUuidv4 }; + deleteSpy.mockResolvedValue(comsObject); + headObjectSpy.mockRejectedValue({}); + pruneOrphanedMetadataSpy.mockResolvedValue(0); + pruneOrphanedTagsSpy.mockResolvedValue(0); + searchObjectsSpy.mockResolvedValue([comsObject]); + + const result = await service.syncObject(path, bucketId); + + expect(result).toBeTruthy(); + expect(result.modified).toBeFalsy(); + expect(result.object).toBeUndefined(); + + expect(ObjectModel.startTransaction).toHaveBeenCalledTimes(1); + expect(_deriveObjectIdSpy).toHaveBeenCalledTimes(0); + expect(createSpy).toHaveBeenCalledTimes(0); + expect(deleteSpy).toHaveBeenCalledTimes(1); + expect(deleteSpy).toHaveBeenCalledWith(validUuidv4, expect.any(Object)); + expect(headObjectSpy).toHaveBeenCalledTimes(1); + expect(headObjectSpy).toHaveBeenCalledWith(expect.objectContaining({ + filePath: path, bucketId: bucketId + })); + expect(pruneOrphanedMetadataSpy).toHaveBeenCalledTimes(1); + expect(pruneOrphanedMetadataSpy).toHaveBeenCalledWith(expect.any(Object)); + expect(pruneOrphanedTagsSpy).toHaveBeenCalledTimes(1); + expect(pruneOrphanedTagsSpy).toHaveBeenCalledWith(expect.any(Object)); + expect(searchObjectsSpy).toHaveBeenCalledTimes(1); + expect(searchObjectsSpy).toHaveBeenCalledWith(expect.objectContaining({ + path: path, bucketId: bucketId + }), expect.any(Object)); + expect(objectModelTrx.commit).toHaveBeenCalledTimes(1); + }); +}); + +describe('syncVersions', () => { + const createSpy = jest.spyOn(versionService, 'create'); + const listSpy = jest.spyOn(versionService, 'list'); + const listAllObjectVersionsSpy = jest.spyOn(storageService, 'listAllObjectVersions'); + const readSpy = jest.spyOn(objectService, 'read'); + const updateSpy = jest.spyOn(versionService, 'update'); + const updateIsLatestSpy = jest.spyOn(versionService, 'updateIsLatest'); + + const comsObject = { + id: validUuidv4, + path: path, + bucketId: validUuidv4 + }; + + beforeEach(() => { + createSpy.mockReset(); + headObjectSpy.mockReset(); + listSpy.mockReset(); + listAllObjectVersionsSpy.mockReset(); + readSpy.mockReset(); + updateSpy.mockReset(); + updateIsLatestSpy.mockReset(); + }); + + afterAll(() => { + createSpy.mockRestore(); + listSpy.mockRestore(); + listAllObjectVersionsSpy.mockRestore(); + readSpy.mockRestore(); + updateSpy.mockRestore(); + updateIsLatestSpy.mockRestore(); + }); + + describe('Common', () => { + it('should look up COMS object when given an objectId', async () => { + createSpy.mockResolvedValue({}); + headObjectSpy.mockResolvedValue({}); + listSpy.mockResolvedValue([]); + listAllObjectVersionsSpy.mockResolvedValue({ DeleteMarkers: [{}], Versions: [{}] }); + readSpy.mockResolvedValue(comsObject); + + const result = await service.syncVersions(validUuidv4); + + expect(result).toBeTruthy(); + expect(Array.isArray(result)).toBeTruthy(); + expect(result).toHaveLength(2); + expect(result).toEqual(expect.arrayContaining([expect.objectContaining({ + modified: true, + version: expect.any(Object) + })])); + + expect(Version.startTransaction).toHaveBeenCalledTimes(1); + expect(createSpy).toHaveBeenCalledTimes(2); + expect(Version.delete).toHaveBeenCalledTimes(0); + expect(headObjectSpy).toHaveBeenCalledTimes(1); + expect(headObjectSpy).toHaveBeenCalledWith(expect.objectContaining({ + filePath: comsObject.path, + bucketId: comsObject.bucketId + })); + expect(listSpy).toHaveBeenCalledTimes(1); + expect(listSpy).toHaveBeenCalledWith(validUuidv4, expect.any(Object)); + expect(listAllObjectVersionsSpy).toHaveBeenCalledTimes(1); + expect(listAllObjectVersionsSpy).toHaveBeenCalledWith(expect.objectContaining({ + filePath: comsObject.path, + bucketId: comsObject.bucketId + })); + expect(readSpy).toHaveBeenCalledTimes(1); + expect(readSpy).toHaveBeenCalledWith(validUuidv4, expect.any(Object)); + expect(updateSpy).toHaveBeenCalledTimes(0); + expect(updateIsLatestSpy).toHaveBeenCalledTimes(0); + expect(versionTrx.commit).toHaveBeenCalledTimes(1); + }); + + it('should use COMS object when given an object', async () => { + createSpy.mockResolvedValue({}); + headObjectSpy.mockResolvedValue({}); + listSpy.mockResolvedValue([]); + listAllObjectVersionsSpy.mockResolvedValue({ DeleteMarkers: [{}], Versions: [{}] }); + + const result = await service.syncVersions(comsObject); + + expect(result).toBeTruthy(); + expect(Array.isArray(result)).toBeTruthy(); + expect(result).toHaveLength(2); + expect(result).toEqual(expect.arrayContaining([expect.objectContaining({ + modified: true, + version: expect.any(Object) + })])); + + expect(Version.startTransaction).toHaveBeenCalledTimes(1); + expect(createSpy).toHaveBeenCalledTimes(2); + expect(Version.delete).toHaveBeenCalledTimes(0); + expect(headObjectSpy).toHaveBeenCalledTimes(1); + expect(headObjectSpy).toHaveBeenCalledWith(expect.objectContaining({ + filePath: comsObject.path, + bucketId: comsObject.bucketId + })); + expect(listSpy).toHaveBeenCalledTimes(1); + expect(listSpy).toHaveBeenCalledWith(validUuidv4, expect.any(Object)); + expect(listAllObjectVersionsSpy).toHaveBeenCalledTimes(1); + expect(listAllObjectVersionsSpy).toHaveBeenCalledWith(expect.objectContaining({ + filePath: comsObject.path, + bucketId: comsObject.bucketId + })); + expect(readSpy).toHaveBeenCalledTimes(0); + expect(updateSpy).toHaveBeenCalledTimes(0); + expect(updateIsLatestSpy).toHaveBeenCalledTimes(0); + expect(versionTrx.commit).toHaveBeenCalledTimes(1); + }); + }); + + describe('Unversioned Bucket', () => { + it('should create a new version if not already present', async () => { + createSpy.mockResolvedValue({}); + headObjectSpy.mockResolvedValue({ ContentType: 'application/octet-stream' }); + listSpy.mockResolvedValue([]); + listAllObjectVersionsSpy.mockResolvedValue({ + DeleteMarkers: [], + Versions: [{ IsLatest: true, VersionId: 'null' }] + }); + + const result = await service.syncVersions(comsObject); + + expect(result).toBeTruthy(); + expect(Array.isArray(result)).toBeTruthy(); + expect(result).toHaveLength(1); + expect(result).toEqual(expect.arrayContaining([expect.objectContaining({ + modified: true, + version: expect.any(Object) + })])); + + expect(Version.startTransaction).toHaveBeenCalledTimes(1); + expect(createSpy).toHaveBeenCalledTimes(1); + expect(Version.delete).toHaveBeenCalledTimes(0); + expect(headObjectSpy).toHaveBeenCalledTimes(1); + expect(headObjectSpy).toHaveBeenCalledWith(expect.objectContaining({ + filePath: comsObject.path, + bucketId: comsObject.bucketId + })); + expect(listSpy).toHaveBeenCalledTimes(1); + expect(listSpy).toHaveBeenCalledWith(validUuidv4, expect.any(Object)); + expect(listAllObjectVersionsSpy).toHaveBeenCalledTimes(1); + expect(listAllObjectVersionsSpy).toHaveBeenCalledWith(expect.objectContaining({ + filePath: comsObject.path, + bucketId: comsObject.bucketId + })); + expect(readSpy).toHaveBeenCalledTimes(0); + expect(updateSpy).toHaveBeenCalledTimes(0); + expect(updateIsLatestSpy).toHaveBeenCalledTimes(0); + expect(versionTrx.commit).toHaveBeenCalledTimes(1); + }); + + it('should update existing version if mimeType has changed', async () => { + headObjectSpy.mockResolvedValue({ ContentType: 'application/octet-stream' }); + listSpy.mockResolvedValue([{ etag: 'etag', mimeType: 'text/plain', s3VersionId: null }]); + listAllObjectVersionsSpy.mockResolvedValue({ + DeleteMarkers: [], + Versions: [{ ETag: 'etag', IsLatest: true, VersionId: 'null' }] + }); + updateSpy.mockResolvedValue({}); + + const result = await service.syncVersions(comsObject); + + expect(result).toBeTruthy(); + expect(Array.isArray(result)).toBeTruthy(); + expect(result).toHaveLength(1); + expect(result).toEqual(expect.arrayContaining([expect.objectContaining({ + modified: true, + version: expect.any(Object) + })])); + + expect(Version.startTransaction).toHaveBeenCalledTimes(1); + expect(createSpy).toHaveBeenCalledTimes(0); + expect(Version.delete).toHaveBeenCalledTimes(0); + expect(headObjectSpy).toHaveBeenCalledTimes(1); + expect(headObjectSpy).toHaveBeenCalledWith(expect.objectContaining({ + filePath: comsObject.path, + bucketId: comsObject.bucketId + })); + expect(listSpy).toHaveBeenCalledTimes(1); + expect(listSpy).toHaveBeenCalledWith(validUuidv4, expect.any(Object)); + expect(listAllObjectVersionsSpy).toHaveBeenCalledTimes(1); + expect(listAllObjectVersionsSpy).toHaveBeenCalledWith(expect.objectContaining({ + filePath: comsObject.path, + bucketId: comsObject.bucketId + })); + expect(readSpy).toHaveBeenCalledTimes(0); + expect(updateSpy).toHaveBeenCalledTimes(1); + expect(updateIsLatestSpy).toHaveBeenCalledTimes(0); + expect(versionTrx.commit).toHaveBeenCalledTimes(1); + }); + + it('should update existing version if etag has changed', async () => { + headObjectSpy.mockResolvedValue({ ContentType: 'application/octet-stream' }); + listSpy.mockResolvedValue([{ etag: 'old', mimeType: 'application/octet-stream', s3VersionId: null }]); + listAllObjectVersionsSpy.mockResolvedValue({ + DeleteMarkers: [], + Versions: [{ ETag: 'new', IsLatest: true, VersionId: 'null' }] + }); + updateSpy.mockResolvedValue({}); + + const result = await service.syncVersions(comsObject); + + expect(result).toBeTruthy(); + expect(Array.isArray(result)).toBeTruthy(); + expect(result).toHaveLength(1); + expect(result).toEqual(expect.arrayContaining([expect.objectContaining({ + modified: true, + version: expect.any(Object) + })])); + + expect(Version.startTransaction).toHaveBeenCalledTimes(1); + expect(createSpy).toHaveBeenCalledTimes(0); + expect(Version.delete).toHaveBeenCalledTimes(0); + expect(headObjectSpy).toHaveBeenCalledTimes(1); + expect(headObjectSpy).toHaveBeenCalledWith(expect.objectContaining({ + filePath: comsObject.path, + bucketId: comsObject.bucketId + })); + expect(listSpy).toHaveBeenCalledTimes(1); + expect(listSpy).toHaveBeenCalledWith(validUuidv4, expect.any(Object)); + expect(listAllObjectVersionsSpy).toHaveBeenCalledTimes(1); + expect(listAllObjectVersionsSpy).toHaveBeenCalledWith(expect.objectContaining({ + filePath: comsObject.path, + bucketId: comsObject.bucketId + })); + expect(readSpy).toHaveBeenCalledTimes(0); + expect(updateSpy).toHaveBeenCalledTimes(1); + expect(updateIsLatestSpy).toHaveBeenCalledTimes(0); + expect(versionTrx.commit).toHaveBeenCalledTimes(1); + }); + + it('should update nothing when version record not modified', async () => { + headObjectSpy.mockResolvedValue({ ContentType: 'application/octet-stream' }); + listSpy.mockResolvedValue([{ etag: 'etag', mimeType: 'application/octet-stream', s3VersionId: null }]); + listAllObjectVersionsSpy.mockResolvedValue({ + DeleteMarkers: [], + Versions: [{ ETag: 'etag', IsLatest: true, VersionId: 'null' }] + }); + updateSpy.mockResolvedValue({}); + + const result = await service.syncVersions(comsObject); + + expect(result).toBeTruthy(); + expect(Array.isArray(result)).toBeTruthy(); + expect(result).toHaveLength(1); + expect(result).toEqual(expect.arrayContaining([expect.objectContaining({ + version: expect.any(Object) + })])); + + expect(Version.startTransaction).toHaveBeenCalledTimes(1); + expect(createSpy).toHaveBeenCalledTimes(0); + expect(Version.delete).toHaveBeenCalledTimes(0); + expect(headObjectSpy).toHaveBeenCalledTimes(1); + expect(headObjectSpy).toHaveBeenCalledWith(expect.objectContaining({ + filePath: comsObject.path, + bucketId: comsObject.bucketId + })); + expect(listSpy).toHaveBeenCalledTimes(1); + expect(listSpy).toHaveBeenCalledWith(validUuidv4, expect.any(Object)); + expect(listAllObjectVersionsSpy).toHaveBeenCalledTimes(1); + expect(listAllObjectVersionsSpy).toHaveBeenCalledWith(expect.objectContaining({ + filePath: comsObject.path, + bucketId: comsObject.bucketId + })); + expect(readSpy).toHaveBeenCalledTimes(0); + expect(updateSpy).toHaveBeenCalledTimes(0); + expect(updateIsLatestSpy).toHaveBeenCalledTimes(0); + expect(versionTrx.commit).toHaveBeenCalledTimes(1); + }); + }); + + describe('Versioned Bucket', () => { + it('should drop COMS versions that are not in S3', async () => { + createSpy.mockResolvedValue({}); + headObjectSpy.mockResolvedValue({}); + // extra versions in coms to delete + listSpy.mockResolvedValue([ + { s3VersionId: validUuidv4 }, + { s3VersionId: validUuidv4 }, + { s3VersionId: validUuidv4 } + ]); + listAllObjectVersionsSpy.mockResolvedValue({ DeleteMarkers: [{}], Versions: [{}] }); + + const result = await service.syncVersions(comsObject); + + expect(result).toBeTruthy(); + expect(Array.isArray(result)).toBeTruthy(); + expect(result).toHaveLength(2); + expect(result).toEqual(expect.arrayContaining([expect.objectContaining({ + modified: true, + version: expect.any(Object) + })])); + + expect(Version.startTransaction).toHaveBeenCalledTimes(1); + expect(createSpy).toHaveBeenCalledTimes(2); + expect(Version.delete).toHaveBeenCalledTimes(1); + + expect(headObjectSpy).toHaveBeenCalledTimes(1); + expect(headObjectSpy).toHaveBeenCalledWith(expect.objectContaining({ + filePath: comsObject.path, + bucketId: comsObject.bucketId + })); + expect(listSpy).toHaveBeenCalledTimes(1); + expect(listSpy).toHaveBeenCalledWith(validUuidv4, expect.any(Object)); + expect(listAllObjectVersionsSpy).toHaveBeenCalledTimes(1); + expect(listAllObjectVersionsSpy).toHaveBeenCalledWith(expect.objectContaining({ + filePath: comsObject.path, + bucketId: comsObject.bucketId + })); + expect(readSpy).toHaveBeenCalledTimes(0); + expect(updateSpy).toHaveBeenCalledTimes(0); + expect(updateIsLatestSpy).toHaveBeenCalledTimes(0); + expect(versionTrx.commit).toHaveBeenCalledTimes(1); + }); + + it('should update isLatest values when evaluated S3 version IsLatest', async () => { + createSpy.mockResolvedValue({}); + headObjectSpy.mockResolvedValue({}); + listSpy.mockResolvedValue([{ id: validUuidv4, s3VersionId: validUuidv4 }]); + listAllObjectVersionsSpy.mockResolvedValue({ + DeleteMarkers: [{}], + Versions: [{ IsLatest: true, VersionId: validUuidv4 }] + }); + updateIsLatestSpy.mockResolvedValue([{}]); + + const result = await service.syncVersions(comsObject); + + expect(result).toBeTruthy(); + expect(Array.isArray(result)).toBeTruthy(); + expect(result).toHaveLength(2); + expect(result).toEqual(expect.arrayContaining([expect.objectContaining({ + modified: true, + version: expect.any(Object) + })])); + + expect(Version.startTransaction).toHaveBeenCalledTimes(1); + expect(createSpy).toHaveBeenCalledTimes(1); + expect(Version.delete).toHaveBeenCalledTimes(0); + expect(headObjectSpy).toHaveBeenCalledTimes(0); + expect(listSpy).toHaveBeenCalledTimes(1); + expect(listSpy).toHaveBeenCalledWith(validUuidv4, expect.any(Object)); + expect(listAllObjectVersionsSpy).toHaveBeenCalledTimes(1); + expect(listAllObjectVersionsSpy).toHaveBeenCalledWith(expect.objectContaining({ + filePath: comsObject.path, + bucketId: comsObject.bucketId + })); + expect(readSpy).toHaveBeenCalledTimes(0); + expect(updateSpy).toHaveBeenCalledTimes(0); + expect(updateIsLatestSpy).toHaveBeenCalledTimes(1); + expect(updateIsLatestSpy).toHaveBeenCalledWith(validUuidv4, expect.any(Object)); + expect(versionTrx.commit).toHaveBeenCalledTimes(1); + }); + + it('should update nothing when version record not modified', async () => { + createSpy.mockResolvedValue({}); + headObjectSpy.mockResolvedValue({}); + listSpy.mockResolvedValue([{ id: validUuidv4, s3VersionId: validUuidv4 }]); + listAllObjectVersionsSpy.mockResolvedValue({ + DeleteMarkers: [{}], + Versions: [{ VersionId: validUuidv4 }] + }); + updateIsLatestSpy.mockResolvedValue([{}]); + + const result = await service.syncVersions(comsObject); + + expect(result).toBeTruthy(); + expect(Array.isArray(result)).toBeTruthy(); + expect(result).toEqual(expect.arrayContaining([expect.objectContaining({ + version: expect.any(Object) + })])); + + expect(Version.startTransaction).toHaveBeenCalledTimes(1); + expect(createSpy).toHaveBeenCalledTimes(1); + expect(Version.delete).toHaveBeenCalledTimes(0); + expect(headObjectSpy).toHaveBeenCalledTimes(0); + expect(listSpy).toHaveBeenCalledTimes(1); + expect(listSpy).toHaveBeenCalledWith(validUuidv4, expect.any(Object)); + expect(listAllObjectVersionsSpy).toHaveBeenCalledTimes(1); + expect(listAllObjectVersionsSpy).toHaveBeenCalledWith(expect.objectContaining({ + filePath: comsObject.path, + bucketId: comsObject.bucketId + })); + expect(readSpy).toHaveBeenCalledTimes(0); + expect(updateSpy).toHaveBeenCalledTimes(0); + expect(updateIsLatestSpy).toHaveBeenCalledTimes(0); + expect(versionTrx.commit).toHaveBeenCalledTimes(1); + }); + }); +}); + +describe('syncTags', () => { + const associateTagsSpy = jest.spyOn(tagService, 'associateTags'); + const dissociateTagsSpy = jest.spyOn(tagService, 'dissociateTags'); + const fetchTagsForVersionSpy = jest.spyOn(tagService, 'fetchTagsForVersion'); + + const comsVersion = { + id: validUuidv4, + objectId: validUuidv4, + s3VersionId: validUuidv4, + isLatest: true + }; + + beforeEach(() => { + associateTagsSpy.mockReset(); + dissociateTagsSpy.mockReset(); + fetchTagsForVersionSpy.mockReset(); + }); + + afterAll(() => { + associateTagsSpy.mockRestore(); + dissociateTagsSpy.mockRestore(); + fetchTagsForVersionSpy.mockRestore(); + }); + + it('should short circuit if version is delete marker', async () => { + getSpy.mockResolvedValue({ deleteMarker: true, ...comsVersion }); + + const result = await service.syncTags(validUuidv4, path, bucketId); + + expect(result).toBeTruthy(); + expect(Array.isArray(result)).toBeTruthy(); + expect(result).toHaveLength(0); + + expect(Version.startTransaction).toHaveBeenCalledTimes(1); + expect(associateTagsSpy).toHaveBeenCalledTimes(0); + expect(dissociateTagsSpy).toHaveBeenCalledTimes(0); + expect(fetchTagsForVersionSpy).toHaveBeenCalledTimes(0); + expect(getObjectTaggingSpy).toHaveBeenCalledTimes(0); + expect(getSpy).toHaveBeenCalledTimes(1); + expect(getSpy).toHaveBeenCalledWith(expect.objectContaining({ versionId: validUuidv4 }), expect.any(Object)); + expect(putObjectTaggingSpy).toHaveBeenCalledTimes(0); + expect(versionTrx.commit).toHaveBeenCalledTimes(0); + }); + + it('should look up COMS version when given a versionId', async () => { + fetchTagsForVersionSpy.mockResolvedValue([{}]); + getObjectTaggingSpy.mockResolvedValue({}); + getSpy.mockResolvedValue(comsVersion); + putObjectTaggingSpy.mockResolvedValue({}); + + const result = await service.syncTags(validUuidv4, path, bucketId); + + expect(result).toBeTruthy(); + expect(Array.isArray(result)).toBeTruthy(); + expect(result).toHaveLength(1); + expect(result).toEqual(expect.arrayContaining([ + expect.objectContaining({ key: 'coms-id', value: validUuidv4 }) + ])); + + expect(Version.startTransaction).toHaveBeenCalledTimes(1); + expect(associateTagsSpy).toHaveBeenCalledTimes(1); + expect(associateTagsSpy).toHaveBeenCalledWith(comsVersion.id, expect.any(Array), expect.any(String), expect.any(Object)); + expect(dissociateTagsSpy).toHaveBeenCalledTimes(0); + expect(fetchTagsForVersionSpy).toHaveBeenCalledTimes(1); + expect(fetchTagsForVersionSpy).toHaveBeenCalledWith(expect.objectContaining({ + versionIds: comsVersion.id + }), expect.any(Object)); + expect(getObjectTaggingSpy).toHaveBeenCalledTimes(1); + expect(getObjectTaggingSpy).toHaveBeenCalledWith(expect.objectContaining({ + filePath: path, + s3VersionId: comsVersion.s3VersionId, + bucketId: bucketId + })); + expect(getSpy).toHaveBeenCalledTimes(1); + expect(getSpy).toHaveBeenCalledWith(expect.objectContaining({ versionId: validUuidv4 }), expect.any(Object)); + expect(putObjectTaggingSpy).toHaveBeenCalledTimes(1); + expect(putObjectTaggingSpy).toHaveBeenCalledWith(expect.objectContaining({ + filePath: path, + tags: expect.arrayContaining([{ + Key: 'coms-id', + Value: comsVersion.objectId + }]), + bucketId: bucketId, + })); + expect(versionTrx.commit).toHaveBeenCalledTimes(1); + }); + + it('should use COMS version when given a version', async () => { + fetchTagsForVersionSpy.mockResolvedValue([{}]); + getObjectTaggingSpy.mockResolvedValue({}); + putObjectTaggingSpy.mockResolvedValue({}); + + const result = await service.syncTags(comsVersion, path, bucketId); + + expect(result).toBeTruthy(); + expect(Array.isArray(result)).toBeTruthy(); + expect(result).toHaveLength(1); + expect(result).toEqual(expect.arrayContaining([ + expect.objectContaining({ key: 'coms-id', value: validUuidv4 }) + ])); + + expect(Version.startTransaction).toHaveBeenCalledTimes(1); + expect(associateTagsSpy).toHaveBeenCalledTimes(1); + expect(associateTagsSpy).toHaveBeenCalledWith(comsVersion.id, expect.any(Array), expect.any(String), expect.any(Object)); + expect(dissociateTagsSpy).toHaveBeenCalledTimes(0); + expect(fetchTagsForVersionSpy).toHaveBeenCalledTimes(1); + expect(fetchTagsForVersionSpy).toHaveBeenCalledWith(expect.objectContaining({ + versionIds: comsVersion.id + }), expect.any(Object)); + expect(getObjectTaggingSpy).toHaveBeenCalledTimes(1); + expect(getObjectTaggingSpy).toHaveBeenCalledWith(expect.objectContaining({ + filePath: path, + s3VersionId: comsVersion.s3VersionId, + bucketId: bucketId + })); + expect(getSpy).toHaveBeenCalledTimes(0); + expect(putObjectTaggingSpy).toHaveBeenCalledTimes(1); + expect(putObjectTaggingSpy).toHaveBeenCalledWith(expect.objectContaining({ + filePath: path, + tags: expect.arrayContaining([{ + Key: 'coms-id', + Value: comsVersion.objectId + }]), + bucketId: bucketId, + })); + expect(versionTrx.commit).toHaveBeenCalledTimes(1); + }); + + it('should not write coms-id tag when coms version is not latest', async () => { + fetchTagsForVersionSpy.mockResolvedValue([{}]); + getObjectTaggingSpy.mockResolvedValue({}); + putObjectTaggingSpy.mockResolvedValue({}); + + comsVersion.isLatest = false; + const result = await service.syncTags(comsVersion, path, bucketId); + // reset for other tests + comsVersion.isLatest = true; + + expect(result).toBeTruthy(); + expect(Array.isArray(result)).toBeTruthy(); + expect(result).toHaveLength(0); + + expect(Version.startTransaction).toHaveBeenCalledTimes(1); + expect(associateTagsSpy).toHaveBeenCalledTimes(0); + expect(dissociateTagsSpy).toHaveBeenCalledTimes(0); + expect(fetchTagsForVersionSpy).toHaveBeenCalledTimes(1); + expect(fetchTagsForVersionSpy).toHaveBeenCalledWith(expect.objectContaining({ + versionIds: comsVersion.id + }), expect.any(Object)); + expect(getObjectTaggingSpy).toHaveBeenCalledTimes(1); + expect(getObjectTaggingSpy).toHaveBeenCalledWith(expect.objectContaining({ + filePath: path, + s3VersionId: comsVersion.s3VersionId, + bucketId: bucketId + })); + expect(getSpy).toHaveBeenCalledTimes(0); + expect(putObjectTaggingSpy).toHaveBeenCalledTimes(0); + expect(versionTrx.commit).toHaveBeenCalledTimes(1); + }); + + it('should write coms-id tag when coms version is latest', async () => { + fetchTagsForVersionSpy.mockResolvedValue([{}]); + getObjectTaggingSpy.mockResolvedValue({}); + putObjectTaggingSpy.mockResolvedValue({}); + + const result = await service.syncTags(comsVersion, path, bucketId); + + expect(result).toBeTruthy(); + expect(Array.isArray(result)).toBeTruthy(); + expect(result).toHaveLength(1); + + expect(Version.startTransaction).toHaveBeenCalledTimes(1); + expect(associateTagsSpy).toHaveBeenCalledTimes(1); + expect(dissociateTagsSpy).toHaveBeenCalledTimes(0); + expect(fetchTagsForVersionSpy).toHaveBeenCalledTimes(1); + expect(fetchTagsForVersionSpy).toHaveBeenCalledWith(expect.objectContaining({ + versionIds: comsVersion.id + }), expect.any(Object)); + expect(getObjectTaggingSpy).toHaveBeenCalledTimes(1); + expect(getObjectTaggingSpy).toHaveBeenCalledWith(expect.objectContaining({ + filePath: path, + s3VersionId: comsVersion.s3VersionId, + bucketId: bucketId + })); + expect(getSpy).toHaveBeenCalledTimes(0); + expect(putObjectTaggingSpy).toHaveBeenCalledTimes(1); + expect(putObjectTaggingSpy).toHaveBeenCalledWith(expect.objectContaining({ + filePath: path, + tags: expect.arrayContaining([{ + Key: 'coms-id', + Value: comsVersion.objectId + }]), + bucketId: bucketId, + })); + expect(versionTrx.commit).toHaveBeenCalledTimes(1); + }); + + it('should not write coms-id tag when there are already 10 tags', async () => { + fetchTagsForVersionSpy.mockResolvedValue([{}]); + getObjectTaggingSpy.mockResolvedValue({ + TagSet: [{}, {}, {}, {}, {}, {}, {}, {}, {}, {}] + }); + putObjectTaggingSpy.mockResolvedValue({}); + + const result = await service.syncTags(comsVersion, path, bucketId); + + expect(result).toBeTruthy(); + expect(Array.isArray(result)).toBeTruthy(); + expect(result).toHaveLength(10); + + expect(Version.startTransaction).toHaveBeenCalledTimes(1); + expect(associateTagsSpy).toHaveBeenCalledTimes(1); + expect(associateTagsSpy).toHaveBeenCalledWith(comsVersion.id, expect.any(Array), expect.any(String), expect.any(Object)); + expect(dissociateTagsSpy).toHaveBeenCalledTimes(0); + expect(fetchTagsForVersionSpy).toHaveBeenCalledTimes(1); + expect(fetchTagsForVersionSpy).toHaveBeenCalledWith(expect.objectContaining({ + versionIds: comsVersion.id + }), expect.any(Object)); + expect(getObjectTaggingSpy).toHaveBeenCalledTimes(1); + expect(getObjectTaggingSpy).toHaveBeenCalledWith(expect.objectContaining({ + filePath: path, + s3VersionId: comsVersion.s3VersionId, + bucketId: bucketId + })); + expect(getSpy).toHaveBeenCalledTimes(0); + expect(putObjectTaggingSpy).toHaveBeenCalledTimes(0); + expect(versionTrx.commit).toHaveBeenCalledTimes(1); + }); + + it('should not write coms-id tag when it already exists', async () => { + fetchTagsForVersionSpy.mockResolvedValue([{}]); + getObjectTaggingSpy.mockResolvedValue({ + TagSet: [{ + Key: 'coms-id', + Value: comsVersion.objectId + }] + }); + putObjectTaggingSpy.mockResolvedValue({}); + + const result = await service.syncTags(comsVersion, path, bucketId); + + expect(result).toBeTruthy(); + expect(Array.isArray(result)).toBeTruthy(); + expect(result).toHaveLength(1); + expect(result).toEqual(expect.arrayContaining([ + expect.objectContaining({ key: 'coms-id', value: validUuidv4 }) + ])); + + expect(Version.startTransaction).toHaveBeenCalledTimes(1); + expect(associateTagsSpy).toHaveBeenCalledTimes(1); + expect(associateTagsSpy).toHaveBeenCalledWith(comsVersion.id, expect.any(Array), expect.any(String), expect.any(Object)); + expect(dissociateTagsSpy).toHaveBeenCalledTimes(0); + expect(fetchTagsForVersionSpy).toHaveBeenCalledTimes(1); + expect(fetchTagsForVersionSpy).toHaveBeenCalledWith(expect.objectContaining({ + versionIds: comsVersion.id + }), expect.any(Object)); + expect(getObjectTaggingSpy).toHaveBeenCalledTimes(1); + expect(getObjectTaggingSpy).toHaveBeenCalledWith(expect.objectContaining({ + filePath: path, + s3VersionId: comsVersion.s3VersionId, + bucketId: bucketId + })); + expect(getSpy).toHaveBeenCalledTimes(0); + expect(putObjectTaggingSpy).toHaveBeenCalledTimes(0); + expect(versionTrx.commit).toHaveBeenCalledTimes(1); + }); + + it('should dissociate and associate tags appropriately', async () => { + fetchTagsForVersionSpy.mockResolvedValue([{ + tagset: [{ + key: 'currentKey', + value: 'currentValue' + }, + { + key: 'oldKey', + value: 'oldValue' + }] + }]); + getObjectTaggingSpy.mockResolvedValue({ + TagSet: [{ + Key: 'coms-id', + Value: comsVersion.objectId + }, + { + Key: 'currentKey', + Value: 'currentValue' + }, + { + Key: 'newKey', + Value: 'newValue' + }] + }); + putObjectTaggingSpy.mockResolvedValue({}); + + const result = await service.syncTags(comsVersion, path, bucketId); + + expect(result).toBeTruthy(); + expect(Array.isArray(result)).toBeTruthy(); + expect(result).toHaveLength(3); + expect(result).toEqual(expect.arrayContaining([ + expect.objectContaining({ key: 'coms-id', value: validUuidv4 }), + expect.objectContaining({ key: 'currentKey', value: 'currentValue' }), + expect.objectContaining({ key: 'newKey', value: 'newValue' }) + ])); + + expect(Version.startTransaction).toHaveBeenCalledTimes(1); + expect(associateTagsSpy).toHaveBeenCalledTimes(1); + expect(associateTagsSpy).toHaveBeenCalledWith(comsVersion.id, expect.arrayContaining([expect.objectContaining({ + key: 'newKey', + value: 'newValue' + })]), expect.any(String), expect.any(Object)); + expect(dissociateTagsSpy).toHaveBeenCalledTimes(1); + expect(dissociateTagsSpy).toHaveBeenCalledWith(comsVersion.id, expect.arrayContaining([expect.objectContaining({ + key: 'oldKey', + value: 'oldValue' + })]), expect.any(Object)); + expect(fetchTagsForVersionSpy).toHaveBeenCalledTimes(1); + expect(fetchTagsForVersionSpy).toHaveBeenCalledWith(expect.objectContaining({ + versionIds: comsVersion.id + }), expect.any(Object)); + expect(getObjectTaggingSpy).toHaveBeenCalledTimes(1); + expect(getObjectTaggingSpy).toHaveBeenCalledWith(expect.objectContaining({ + filePath: path, + s3VersionId: comsVersion.s3VersionId, + bucketId: bucketId + })); + expect(getSpy).toHaveBeenCalledTimes(0); + expect(putObjectTaggingSpy).toHaveBeenCalledTimes(0); + expect(versionTrx.commit).toHaveBeenCalledTimes(1); + }); +}); + +describe('syncMetadata', () => { + const associateMetadataSpy = jest.spyOn(metadataService, 'associateMetadata'); + const dissociateMetadataSpy = jest.spyOn(metadataService, 'dissociateMetadata'); + const fetchMetadataForVersionSpy = jest.spyOn(metadataService, 'fetchMetadataForVersion'); + + const comsVersion = { + id: validUuidv4, + objectId: validUuidv4, + s3VersionId: validUuidv4 + }; + + beforeEach(() => { + associateMetadataSpy.mockReset(); + dissociateMetadataSpy.mockReset(); + fetchMetadataForVersionSpy.mockReset(); + }); + + afterAll(() => { + associateMetadataSpy.mockRestore(); + dissociateMetadataSpy.mockRestore(); + fetchMetadataForVersionSpy.mockRestore(); + }); + + it('should short circuit if version is delete marker', async () => { + getSpy.mockResolvedValue({ deleteMarker: true, ...comsVersion }); + + const result = await service.syncMetadata(validUuidv4, path, bucketId); + + expect(result).toBeTruthy(); + expect(Array.isArray(result)).toBeTruthy(); + expect(result).toHaveLength(0); + + expect(Version.startTransaction).toHaveBeenCalledTimes(1); + expect(associateMetadataSpy).toHaveBeenCalledTimes(0); + expect(dissociateMetadataSpy).toHaveBeenCalledTimes(0); + expect(fetchMetadataForVersionSpy).toHaveBeenCalledTimes(0); + expect(getObjectTaggingSpy).toHaveBeenCalledTimes(0); + expect(getSpy).toHaveBeenCalledTimes(1); + expect(getSpy).toHaveBeenCalledWith(expect.objectContaining({ versionId: validUuidv4 }), expect.any(Object)); + expect(putObjectTaggingSpy).toHaveBeenCalledTimes(0); + expect(versionTrx.commit).toHaveBeenCalledTimes(0); + }); + + it('should look up COMS version when given a versionId', async () => { + fetchMetadataForVersionSpy.mockResolvedValue([]); + getSpy.mockResolvedValue(comsVersion); + headObjectSpy.mockResolvedValue({}); + + const result = await service.syncMetadata(validUuidv4, path, bucketId); + + expect(result).toBeTruthy(); + expect(Array.isArray(result)).toBeTruthy(); + expect(result).toHaveLength(0); + + expect(Version.startTransaction).toHaveBeenCalledTimes(1); + expect(associateMetadataSpy).toHaveBeenCalledTimes(0); + expect(dissociateMetadataSpy).toHaveBeenCalledTimes(0); + expect(fetchMetadataForVersionSpy).toHaveBeenCalledTimes(1); + expect(fetchMetadataForVersionSpy).toHaveBeenCalledWith(expect.objectContaining({ + versionIds: comsVersion.id + }), expect.any(Object)); + expect(getSpy).toHaveBeenCalledTimes(1); + expect(getSpy).toHaveBeenCalledWith(expect.objectContaining({ versionId: validUuidv4 }), expect.any(Object)); + expect(headObjectSpy).toHaveBeenCalledTimes(1); + expect(headObjectSpy).toHaveBeenCalledWith(expect.objectContaining({ + filePath: path, + s3VersionId: comsVersion.s3VersionId, + bucketId: bucketId + })); + expect(versionTrx.commit).toHaveBeenCalledTimes(1); + }); + + it('should use COMS version when given a version', async () => { + fetchMetadataForVersionSpy.mockResolvedValue([]); + headObjectSpy.mockResolvedValue({}); + + const result = await service.syncMetadata(comsVersion, path, bucketId); + + expect(result).toBeTruthy(); + expect(Array.isArray(result)).toBeTruthy(); + expect(result).toHaveLength(0); + + expect(Version.startTransaction).toHaveBeenCalledTimes(1); + expect(associateMetadataSpy).toHaveBeenCalledTimes(0); + expect(dissociateMetadataSpy).toHaveBeenCalledTimes(0); + expect(fetchMetadataForVersionSpy).toHaveBeenCalledTimes(1); + expect(fetchMetadataForVersionSpy).toHaveBeenCalledWith(expect.objectContaining({ + versionIds: comsVersion.id + }), expect.any(Object)); + expect(getSpy).toHaveBeenCalledTimes(0); + expect(headObjectSpy).toHaveBeenCalledTimes(1); + expect(headObjectSpy).toHaveBeenCalledWith(expect.objectContaining({ + filePath: path, + s3VersionId: comsVersion.s3VersionId, + bucketId: bucketId + })); + expect(versionTrx.commit).toHaveBeenCalledTimes(1); + }); + + it('should dissociate and associate metadata appropriately', async () => { + fetchMetadataForVersionSpy.mockResolvedValue([{ + metadata: [{ + key: 'currentKey', + value: 'currentValue' + }, + { + key: 'oldKey', + value: 'oldValue' + }] + }]); + headObjectSpy.mockResolvedValue({ + Metadata: { + currentKey: 'currentValue', + newKey: 'newValue' + } + }); + + const result = await service.syncMetadata(comsVersion, path, bucketId); + + expect(result).toBeTruthy(); + expect(Array.isArray(result)).toBeTruthy(); + expect(result).toHaveLength(2); + expect(result).toEqual(expect.arrayContaining([ + expect.objectContaining({ key: 'currentKey', value: 'currentValue' }), + expect.objectContaining({ key: 'newKey', value: 'newValue' }) + ])); + + expect(Version.startTransaction).toHaveBeenCalledTimes(1); + expect(associateMetadataSpy).toHaveBeenCalledTimes(1); + expect(associateMetadataSpy).toHaveBeenCalledWith(comsVersion.id, expect.arrayContaining([expect.objectContaining({ + key: 'newKey', + value: 'newValue' + })]), expect.any(String), expect.any(Object)); + expect(dissociateMetadataSpy).toHaveBeenCalledTimes(1); + expect(dissociateMetadataSpy).toHaveBeenCalledWith(comsVersion.id, expect.arrayContaining([expect.objectContaining({ + key: 'oldKey', + value: 'oldValue' + })]), expect.any(Object)); + expect(fetchMetadataForVersionSpy).toHaveBeenCalledTimes(1); + expect(fetchMetadataForVersionSpy).toHaveBeenCalledWith(expect.objectContaining({ + versionIds: comsVersion.id + }), expect.any(Object)); + expect(getSpy).toHaveBeenCalledTimes(0); + expect(headObjectSpy).toHaveBeenCalledTimes(1); + expect(headObjectSpy).toHaveBeenCalledWith(expect.objectContaining({ + filePath: path, + s3VersionId: comsVersion.s3VersionId, + bucketId: bucketId + })); + expect(versionTrx.commit).toHaveBeenCalledTimes(1); + }); +}); diff --git a/comsapi/app/tests/unit/services/tag.spec.js b/comsapi/app/tests/unit/services/tag.spec.js new file mode 100644 index 00000000..9eead114 --- /dev/null +++ b/comsapi/app/tests/unit/services/tag.spec.js @@ -0,0 +1,302 @@ +const { NIL: OBJECT_ID, NIL: SYSTEM_USER, NIL: VERSION_ID } = require('uuid'); + +const { resetModel, trxBuilder } = require('../../common/helper'); +const ObjectModel = require('../../../src/db/models/tables/objectModel'); +const Tag = require('../../../src/db/models/tables/tag'); +const Version = require('../../../src/db/models/tables/version'); +const VersionTag = require('../../../src/db/models/tables/versionTag'); +const utils = require('../../../src/components/utils'); + +const objectModelTrx = trxBuilder(); +jest.mock('../../../src/db/models/tables/objectModel', () => ({ + startTransaction: jest.fn(), + then: jest.fn(), + + allowGraph: jest.fn(), + modify: jest.fn(), + modifyGraph: jest.fn(), + query: jest.fn(), + select: jest.fn(), + withGraphJoined: jest.fn() +})); + +const tagTrx = trxBuilder(); +jest.mock('../../../src/db/models/tables/tag', () => ({ + startTransaction: jest.fn(), + then: jest.fn(), + + allowGraph: jest.fn(), + delete: jest.fn(), + find: jest.fn(), + insert: jest.fn(), + joinRelated: jest.fn(), + map: jest.fn(), + modify: jest.fn(), + query: jest.fn(), + returning: jest.fn(), + select: jest.fn(), + where: jest.fn(), + whereIn: jest.fn(), + whereNull: jest.fn(), + withGraphJoined: jest.fn() +})); + +const versionTrx = trxBuilder(); +jest.mock('../../../src/db/models/tables/version', () => ({ + startTransaction: jest.fn(), + then: jest.fn(), + + allowGraph: jest.fn(), + modify: jest.fn(), + modifyGraph: jest.fn(), + orderBy: jest.fn(), + query: jest.fn(), + select: jest.fn(), + withGraphJoined: jest.fn() +})); + +const versionTagTrx = trxBuilder(); +jest.mock('../../../src/db/models/tables/versionTag', () => ({ + startTransaction: jest.fn(), + then: jest.fn(), + + allowGraph: jest.fn(), + delete: jest.fn(), + modify: jest.fn(), + query: jest.fn(), + some: jest.fn(), + where: jest.fn(), + withGraphJoined: jest.fn() +})); + +const service = require('../../../src/services/tag'); + +const params = { tagset: [{ key: 'C', value: '10' }], objectIds: [OBJECT_ID], userId: SYSTEM_USER }; +const tags = [{ key: 'A', value: '1' }, { key: 'B', value: '2' }]; + +beforeEach(() => { + jest.clearAllMocks(); + resetModel(ObjectModel, objectModelTrx); + resetModel(Tag, tagTrx); + resetModel(Version, versionTrx); + resetModel(VersionTag, versionTagTrx); +}); + +describe('dissociateTags', () => { + it('Dissociates all provided tags from a versionId', async () => { + Tag.whereNull.mockResolvedValue([ + { + ...tags, + map: jest.fn() + } + ]); + + await service.dissociateTags(VERSION_ID, tags); + + // expect(Tag.startTransaction).toHaveBeenCalledTimes(1); + expect(VersionTag.query).toHaveBeenCalledTimes(2); + expect(VersionTag.query).toHaveBeenCalledWith(expect.anything()); + expect(VersionTag.allowGraph).toHaveBeenCalledTimes(2); + expect(VersionTag.allowGraph).toBeCalledWith('tag'); + expect(VersionTag.withGraphJoined).toHaveBeenCalledTimes(2); + expect(VersionTag.withGraphJoined).toBeCalledWith('tag'); + expect(VersionTag.where).toHaveBeenCalledTimes(2); + expect(VersionTag.modify).toHaveBeenCalledTimes(2); + expect(VersionTag.modify).toBeCalledWith('filterVersionId', VERSION_ID); + expect(VersionTag.delete).toHaveBeenCalledTimes(2); + expect(tagTrx.commit).toHaveBeenCalledTimes(1); + }); +}); + +describe('replaceTags', () => { + const associateTagsSpy = jest.spyOn(service, 'associateTags'); + const getObjectsByKeyValueSpy = jest.spyOn(utils, 'getObjectsByKeyValue'); + const dissociateTagsSpy = jest.spyOn(service, 'dissociateTags'); + + beforeEach(() => { + associateTagsSpy.mockReset(); + dissociateTagsSpy.mockReset(); + getObjectsByKeyValueSpy.mockReset(); + }); + + afterAll(() => { + associateTagsSpy.mockRestore(); + dissociateTagsSpy.mockReset(); + getObjectsByKeyValueSpy.mockRestore(); + }); + + it('Makes the incoming list of tags the definitive set associated with versionId', async () => { + associateTagsSpy.mockResolvedValue(...tags); + dissociateTagsSpy.mockResolvedValue([]); + getObjectsByKeyValueSpy.mockResolvedValue(...tags); + Tag.where.mockResolvedValue([ + { + ...tags, + filter: jest.fn() + } + ]); + await service.replaceTags(VERSION_ID, tags); + + expect(Tag.startTransaction).toHaveBeenCalledTimes(1); + expect(Tag.query).toHaveBeenCalledTimes(1); + expect(Tag.query).toHaveBeenCalledWith(expect.anything()); + expect(Tag.joinRelated).toHaveBeenCalledTimes(1); + expect(Tag.joinRelated).toBeCalledWith('versionTag'); + expect(Tag.where).toHaveBeenCalledTimes(1); + expect(Tag.where).toBeCalledWith('versionId', VERSION_ID); + expect(tagTrx.commit).toHaveBeenCalledTimes(1); + }); +}); + +describe('associateTags', () => { + const createTagsSpy = jest.spyOn(service, 'createTags'); + + beforeEach(() => { + createTagsSpy.mockReset(); + }); + + afterAll(() => { + createTagsSpy.mockRestore(); + }); + + it('CreateTags to create new Tag records associates new tags to the versionId', async () => { + createTagsSpy.mockResolvedValue([{ key: 'C', value: '10' }]); + VersionTag.modify.mockResolvedValue([ + { + some: jest.fn() + } + ]); + + await service.associateTags(VERSION_ID, tags); + + expect(Tag.startTransaction).toHaveBeenCalledTimes(1); + expect(VersionTag.query).toHaveBeenCalledTimes(1); + expect(VersionTag.query).toHaveBeenCalledWith(expect.anything()); + expect(VersionTag.modify).toHaveBeenCalledTimes(1); + expect(VersionTag.modify).toHaveBeenCalledWith('filterVersionId', VERSION_ID); + expect(tagTrx.commit).toHaveBeenCalledTimes(1); + }); +}); + +describe('pruneOrphanedTags', () => { + it('Deletes tag records if they are no longer related to any versions', async () => { + Tag.whereNull.mockResolvedValue([ + { + ...tags, + map: jest.fn() + } + ]); + + await service.pruneOrphanedTags(); + + expect(Tag.startTransaction).toHaveBeenCalledTimes(1); + expect(Tag.query).toHaveBeenCalledTimes(2); + expect(Tag.query).toHaveBeenCalledWith(expect.anything()); + expect(Tag.allowGraph).toHaveBeenCalledTimes(1); + expect(Tag.allowGraph).toBeCalledWith('versionTag'); + expect(Tag.withGraphJoined).toHaveBeenCalledTimes(1); + expect(Tag.withGraphJoined).toBeCalledWith('versionTag'); + expect(Tag.select).toHaveBeenCalledTimes(1); + expect(Tag.select).toBeCalledWith('tag.id'); + expect(Tag.whereNull).toHaveBeenCalledTimes(1); + expect(Tag.whereNull).toBeCalledWith('versionTag.tagId'); + expect(Tag.delete).toHaveBeenCalledTimes(1); + expect(Tag.delete).toBeCalledWith(); + expect(Tag.whereIn).toHaveBeenCalledTimes(1); + expect(Tag.whereIn).toBeCalledWith('id', expect.any(Object)); + expect(tagTrx.commit).toHaveBeenCalledTimes(1); + }); +}); + +describe('createTags', () => { + const getObjectsByKeyValueSpy = jest.spyOn(utils, 'getObjectsByKeyValue'); + + beforeEach(() => { + getObjectsByKeyValueSpy.mockReset(); + }); + + afterAll(() => { + getObjectsByKeyValueSpy.mockRestore(); + }); + + it('Inserts any tag records if they dont already exist in db', async () => { + Tag.select.mockResolvedValue([ + { + ...tags, + find: jest.fn() + } + ]); + + getObjectsByKeyValueSpy.mockResolvedValue(...tags); + + await service.createTags(tags); + + expect(Tag.startTransaction).toHaveBeenCalledTimes(1); + expect(Tag.query).toHaveBeenCalledTimes(2); + expect(Tag.query).toHaveBeenCalledWith(expect.anything()); + expect(Tag.select).toHaveBeenCalledTimes(1); + expect(tagTrx.commit).toHaveBeenCalledTimes(1); + }); +}); + +describe('fetchTagsForObject', () => { + it('Fetch matching tags on latest version of provided objects', async () => { + ObjectModel.then.mockResolvedValue([ + { + ...tags, + map: jest.fn() + } + ]); + + service.fetchTagsForObject(params); + + expect(ObjectModel.query).toHaveBeenCalledTimes(1); + expect(ObjectModel.select).toHaveBeenCalledTimes(1); + expect(ObjectModel.select).toBeCalledWith('object.id AS objectId', 'object.bucketId as bucketId'); + expect(ObjectModel.allowGraph).toHaveBeenCalledTimes(1); + expect(ObjectModel.allowGraph).toBeCalledWith('version.tag'); + expect(ObjectModel.withGraphJoined).toHaveBeenCalledTimes(1); + expect(ObjectModel.withGraphJoined).toBeCalledWith('version.tag'); + expect(ObjectModel.modifyGraph).toHaveBeenCalledTimes(2); + expect(ObjectModel.modify).toHaveBeenCalledTimes(3); + expect(ObjectModel.modify).toBeCalledWith('filterIds', [SYSTEM_USER]); + expect(ObjectModel.then).toHaveBeenCalledTimes(1); + }); +}); + +describe('fetchTagsForVersion', () => { + it('Fetch tags for specific versions', async () => { + Version.then.mockResolvedValue([ + { + ...tags, + map: jest.fn() + } + ]); + + await service.fetchTagsForVersion(params); + + expect(Tag.startTransaction).toHaveBeenCalledTimes(1); + expect(Version.query).toHaveBeenCalledTimes(1); + expect(Version.select).toHaveBeenCalledTimes(1); + expect(Version.select).toBeCalledWith('version.id as versionId', 'version.s3VersionId'); + expect(Version.allowGraph).toHaveBeenCalledTimes(1); + expect(Version.allowGraph).toBeCalledWith('tag as tagset'); + expect(Version.withGraphJoined).toHaveBeenCalledTimes(1); + expect(Version.withGraphJoined).toBeCalledWith('tag as tagset'); + expect(Version.modifyGraph).toHaveBeenCalledTimes(1); + expect(Version.modify).toHaveBeenCalledTimes(2); + expect(Version.orderBy).toHaveBeenCalledTimes(1); + expect(Version.orderBy).toBeCalledWith('version.createdAt', 'desc'); + expect(Version.then).toHaveBeenCalledTimes(1); + expect(tagTrx.commit).toHaveBeenCalledTimes(1); + }); +}); + +describe('searchTags', () => { + it('Search and filter for specific tag keys', async () => { + service.searchTags([]); + + expect(Tag.query).toHaveBeenCalledTimes(1); + expect(Tag.modify).toHaveBeenCalledTimes(1); + }); +}); diff --git a/comsapi/app/tests/unit/services/user.spec.js b/comsapi/app/tests/unit/services/user.spec.js new file mode 100644 index 00000000..d7df451d --- /dev/null +++ b/comsapi/app/tests/unit/services/user.spec.js @@ -0,0 +1,382 @@ +const { NIL: SYSTEM_USER } = require('uuid'); + +const { resetModel, trxBuilder } = require('../../common/helper'); +const utils = require('../../../src/db/models/utils'); +const IdentityProvider = require('../../../src/db/models/tables/identityProvider'); +const User = require('../../../src/db/models/tables/user'); + +const identityProviderTrx = trxBuilder(); +jest.mock('../../../src/db/models/tables/identityProvider', () => ({ + startTransaction: jest.fn(), + then: jest.fn(), + + findById: jest.fn(), + insertAndFetch: jest.fn(), + modify: jest.fn(), + query: jest.fn(), + where: jest.fn() +})); + +const userTrx = trxBuilder(); +jest.mock('../../../src/db/models/tables/user', () => ({ + startTransaction: jest.fn(), + then: jest.fn(), + + first: jest.fn(), + findById: jest.fn(), + insert: jest.fn(), + insertAndFetch: jest.fn(), + modify: jest.fn(), + patchAndFetchById: jest.fn(), + query: jest.fn(), + returning: jest.fn(), + select: jest.fn(), + throwIfNotFound: jest.fn(), + where: jest.fn(), + whereNotNull: jest.fn() +})); + +const service = require('../../../src/services/user'); + +const userId = SYSTEM_USER; +const token = { + sub: userId, + identity_provider_identity: 'jsmith:idir', + preferred_username: 'john@email.com', + given_name: 'john', + family_name: 'smith', + name: 'john smith', + email: 'jsmith@email.com', + identity_provider: 'idir' +}; +const user = { + userId: userId, + identityId: userId, + username: 'jsmith:idir', + firstName: 'john', + lastName: 'smith', + fullName: 'john smith', + email: 'jsmith@email.com', + idp: 'idir' +}; + +beforeEach(() => { + jest.clearAllMocks(); + resetModel(IdentityProvider, identityProviderTrx); + resetModel(User, userTrx); +}); + +describe('_tokenToUser', () => { + // TODO: Add more edge case testing + it('Transforms JWT payload contents into a User Model object', () => { + const expected = { ...user, userId: undefined }; + const newUser = service._tokenToUser(token); + expect(newUser).toEqual(expected); + }); +}); + + +describe('createIdp', () => { + it('Creates an IDP record', async () => { + await service.createIdp('foo'); + + expect(IdentityProvider.startTransaction).toHaveBeenCalledTimes(1); + expect(IdentityProvider.query).toHaveBeenCalledTimes(1); + expect(IdentityProvider.query).toHaveBeenCalledWith(expect.anything()); + expect(IdentityProvider.insertAndFetch).toHaveBeenCalledTimes(1); + expect(IdentityProvider.insertAndFetch).toBeCalledWith( + expect.objectContaining({ + idp: 'foo', + createdBy: expect.any(String), + }) + ); + expect(identityProviderTrx.commit).toHaveBeenCalledTimes(1); + }); +}); + +describe('createUser', () => { + const readIdpSpy = jest.spyOn(service, 'readIdp'); + const createIdpSpy = jest.spyOn(service, 'createIdp'); + + beforeEach(() => { + readIdpSpy.mockReset(); + createIdpSpy.mockReset(); + }); + + afterAll(() => { + readIdpSpy.mockRestore(); + createIdpSpy.mockRestore(); + }); + + it('Creates an idp if no matching idp exists in database', async () => { + User.first.mockResolvedValue(undefined); + readIdpSpy.mockResolvedValue(false); + + await service.createUser(user); + + expect(readIdpSpy).toHaveBeenCalledTimes(1); + expect(readIdpSpy).toHaveBeenCalledWith('idir', userTrx); + expect(createIdpSpy).toHaveBeenCalledTimes(1); + expect(createIdpSpy).toHaveBeenCalledWith('idir', userTrx); + + expect(User.startTransaction).toHaveBeenCalledTimes(1); + expect(User.query).toHaveBeenCalledTimes(2); + expect(User.query).toHaveBeenCalledWith(expect.anything()); + expect(User.insert).toHaveBeenCalledTimes(1); + expect(User.insert).toBeCalledWith( + expect.objectContaining({ + ...user, + userId: expect.any(String) + }) + ); + expect(userTrx.commit).toHaveBeenCalledTimes(1); + }); + + it('Skips creating an idp if matching idp already exists in database', async () => { + User.first.mockResolvedValue(false); + readIdpSpy.mockReturnValue(true); + + await service.createUser(user); + + expect(readIdpSpy).toHaveBeenCalledTimes(1); + expect(createIdpSpy).toHaveBeenCalledTimes(0); + + expect(User.startTransaction).toHaveBeenCalledTimes(1); + expect(User.query).toHaveBeenCalledTimes(2); + expect(User.query).toHaveBeenCalledWith(expect.anything()); + expect(User.insert).toHaveBeenCalledTimes(1); + expect(User.insert).toBeCalledWith( + expect.objectContaining({ + ...user, + userId: expect.any(String) + }) + ); + expect(userTrx.commit).toHaveBeenCalledTimes(1); + }); + + it('Does not create an idp if user has none (eg: \'System\' user)', async () => { + const systemUser = { ...user, idp: undefined }; + await service.createUser(systemUser); + + expect(readIdpSpy).toHaveBeenCalledTimes(0); + expect(createIdpSpy).toHaveBeenCalledTimes(0); + + expect(User.startTransaction).toHaveBeenCalledTimes(1); + expect(User.query).toHaveBeenCalledTimes(1); + expect(User.query).toHaveBeenCalledWith(expect.anything()); + expect(User.insert).toHaveBeenCalledTimes(0); + expect(userTrx.commit).toHaveBeenCalledTimes(1); + }); +}); + +describe('getCurrentUserId', () => { + it('Query user by identityId', async () => { + User.first.mockResolvedValue({ ...user, userId: '123', identityId: '123-idir' }); + const result = await service.getCurrentUserId('123-idir'); + + expect(User.query).toHaveBeenCalledTimes(1); + expect(User.query).toHaveBeenCalledWith(); + expect(User.select).toHaveBeenCalledTimes(1); + expect(User.select).toHaveBeenCalledWith('userId'); + expect(User.where).toHaveBeenCalledTimes(1); + expect(User.where).toHaveBeenCalledWith('identityId', '123-idir'); + expect(User.first).toHaveBeenCalledTimes(1); + expect(User.first).toHaveBeenCalledWith(); + expect(result).toEqual('123'); + }); +}); + +describe('listIdps', () => { + it('Query user by identityId', () => { + const params = { active: true }; + service.listIdps(params); + + expect(IdentityProvider.query).toHaveBeenCalledTimes(1); + expect(IdentityProvider.query).toHaveBeenCalledWith(); + expect(IdentityProvider.modify).toHaveBeenCalledTimes(2); + expect(IdentityProvider.modify).toHaveBeenNthCalledWith(1, 'filterActive', params.active); + expect(IdentityProvider.modify).toHaveBeenNthCalledWith(2, 'orderDefault'); + }); +}); + +describe('login', () => { + const createUserSpy = jest.spyOn(service, 'createUser'); + const tokenToUserSpy = jest.spyOn(service, '_tokenToUser'); + const trxWrapperSpy = jest.spyOn(utils, 'trxWrapper'); + const updateUserSpy = jest.spyOn(service, 'updateUser'); + + beforeEach(() => { + createUserSpy.mockReset(); + tokenToUserSpy.mockReset(); + trxWrapperSpy.mockReset(); + updateUserSpy.mockReset(); + + service._tokenToUser = jest.fn().mockReturnValue(user); + }); + + afterAll(() => { + createUserSpy.mockRestore(); + tokenToUserSpy.mockRestore(); + trxWrapperSpy.mockReset(); + updateUserSpy.mockRestore(); + }); + + it('Adds a new user record', async () => { + User.first.mockResolvedValue(undefined); + createUserSpy.mockResolvedValue(user); + trxWrapperSpy.mockImplementation(callback => callback({})); + + await service.login(token); + + expect(User.query).toHaveBeenCalledTimes(1); + expect(User.query).toHaveBeenCalledWith(expect.any(Object)); + expect(User.where).toHaveBeenCalledTimes(1); + expect(User.where).toHaveBeenCalledWith({ identityId: user.identityId, idp: user.idp }); + expect(User.first).toHaveBeenCalledTimes(1); + expect(User.first).toHaveBeenCalledWith(); + + expect(createUserSpy).toHaveBeenCalledTimes(1); + expect(createUserSpy).toHaveBeenCalledWith(user, expect.any(Object)); + expect(updateUserSpy).toHaveBeenCalledTimes(0); + }); + + it('Updates an existing user record', async () => { + trxWrapperSpy.mockImplementation(callback => callback({})); + User.first.mockResolvedValue({ ...user, userId: 'a96f2809-d6f4-4cef-a02a-3f72edff06d7' }); + + await service.login(token); + + expect(User.query).toHaveBeenCalledTimes(1); + expect(User.query).toHaveBeenCalledWith(expect.any(Object)); + expect(User.where).toHaveBeenCalledTimes(1); + expect(User.where).toHaveBeenCalledWith({ identityId: user.identityId, idp: user.idp }); + expect(User.first).toHaveBeenCalledTimes(1); + expect(User.first).toHaveBeenCalledWith(); + + expect(createUserSpy).toHaveBeenCalledTimes(0); + expect(updateUserSpy).toHaveBeenCalledTimes(1); + expect(updateUserSpy).toHaveBeenCalledWith('a96f2809-d6f4-4cef-a02a-3f72edff06d7', expect.objectContaining(user), expect.any(Object)); + }); +}); + +describe('readIdp', () => { + it('Query identityProvider by code', async () => { + await service.readIdp('idir'); + + expect(IdentityProvider.startTransaction).toHaveBeenCalledTimes(1); + expect(IdentityProvider.startTransaction).toHaveBeenCalledWith(); + expect(IdentityProvider.query).toHaveBeenCalledTimes(1); + expect(IdentityProvider.query).toHaveBeenCalledWith(identityProviderTrx); + expect(IdentityProvider.findById).toHaveBeenCalledTimes(1); + expect(IdentityProvider.findById).toHaveBeenCalledWith('idir'); + expect(identityProviderTrx.commit).toHaveBeenCalledTimes(1); + }); +}); + +describe('readUser', () => { + it('Query user by userId', () => { + service.readUser(SYSTEM_USER); + + expect(User.query).toHaveBeenCalledTimes(1); + expect(User.query).toHaveBeenCalledWith(); + expect(User.findById).toHaveBeenCalledTimes(1); + expect(User.findById).toHaveBeenCalledWith(SYSTEM_USER); + expect(User.throwIfNotFound).toHaveBeenCalledTimes(1); + expect(User.throwIfNotFound).toHaveBeenCalledWith(); + }); +}); + +describe('searchUsers', () => { + it('Query user by identityId', () => { + const params = { ...user, active: true, search: 'foo' }; + service.searchUsers(params); + + expect(User.query).toHaveBeenCalledTimes(1); + expect(User.query).toHaveBeenCalledWith(); + expect(User.modify).toHaveBeenCalledTimes(11); + expect(User.modify).toHaveBeenNthCalledWith(1, 'filterUserId', params.userId); + expect(User.modify).toHaveBeenNthCalledWith(2, 'filterIdentityId', params.identityId); + expect(User.modify).toHaveBeenNthCalledWith(3, 'filterIdp', params.idp); + expect(User.modify).toHaveBeenNthCalledWith(4, 'filterUsername', params.username); + expect(User.modify).toHaveBeenNthCalledWith(5, 'filterEmail', params.email); + expect(User.modify).toHaveBeenNthCalledWith(6, 'filterFirstName', params.firstName); + expect(User.modify).toHaveBeenNthCalledWith(7, 'filterFullName', params.fullName); + expect(User.modify).toHaveBeenNthCalledWith(8, 'filterLastName', params.lastName); + expect(User.modify).toHaveBeenNthCalledWith(9, 'filterActive', params.active); + expect(User.modify).toHaveBeenNthCalledWith(10, 'filterSearch', params.search); + expect(User.modify).toHaveBeenNthCalledWith(11, 'orderLastFirstAscending'); + expect(User.whereNotNull).toHaveBeenCalledTimes(1); + expect(User.whereNotNull).toHaveBeenCalledWith('identityId'); + }); +}); + +describe('updateUser', () => { + const tokenToUserSpy = jest.spyOn(service, '_tokenToUser'); + const readUserSpy = jest.spyOn(service, 'readUser'); + const createIdpSpy = jest.spyOn(service, 'createIdp'); + const readIdpSpy = jest.spyOn(service, 'readIdp'); + + beforeEach(() => { + tokenToUserSpy.mockReset(); + readIdpSpy.mockReset(); + createIdpSpy.mockReset(); + readUserSpy.mockReset(); + + service._tokenToUser = jest.fn().mockReturnValue(user); + }); + + afterAll(() => { + tokenToUserSpy.mockRestore(); + readIdpSpy.mockRestore(); + createIdpSpy.mockRestore(); + readUserSpy.mockRestore(); + }); + + it('Does nothing if user is unchanged', async () => { + readUserSpy.mockResolvedValue(user); + await service.updateUser(userId, user); + + expect(readUserSpy).toHaveBeenCalledTimes(1); + expect(readUserSpy).toHaveBeenCalledWith(userId); + expect(readIdpSpy).toHaveBeenCalledTimes(0); + expect(createIdpSpy).toHaveBeenCalledTimes(0); + + expect(User.query).toHaveBeenCalledTimes(0); + expect(User.patchAndFetchById).toHaveBeenCalledTimes(0); + }); + + it('Updates existing user if properties have changed', async () => { + const oldUser = { ...user, email: 'jsmith@yahoo.com' }; + readUserSpy.mockResolvedValue(oldUser); + await service.updateUser(userId, user); + + expect(readUserSpy).toHaveBeenCalledTimes(1); + expect(readUserSpy).toHaveBeenCalledWith(userId); + expect(readIdpSpy).toHaveBeenCalledTimes(0); + + expect(User.query).toHaveBeenCalledTimes(1); + expect(User.query).toHaveBeenCalledWith(expect.anything()); + expect(User.patchAndFetchById).toHaveBeenCalledTimes(1); + expect(User.patchAndFetchById).toHaveBeenCalledWith(userId, expect.any(Object)); + }); + + it('Creates idp if idp does not exist in db', async () => { + const oldUser = { ...user, email: 'jsmith@yahoo.com', idp: 'newIdp' }; + readUserSpy.mockResolvedValue(oldUser); + readIdpSpy.mockResolvedValue(undefined); + await service.updateUser(userId, user); + + expect(readUserSpy).toHaveBeenCalledTimes(1); + expect(readUserSpy).toHaveBeenCalledWith(userId); + // TODO: For some reason, the spied on functions below are not actually being spy wrapped. + // expect(readIdpSpy).toHaveBeenCalledTimes(1); + // expect(readIdpSpy).toHaveBeenCalledWith(user.idp, expect.anything()); + // expect(createIdpSpy).toHaveBeenCalledTimes(1); + // expect(createIdpSpy).toHaveBeenCalledWith(user.idp, expect.anything()); + + expect(User.query).toHaveBeenCalledTimes(1); + expect(User.query).toHaveBeenCalledWith(expect.anything()); + expect(User.patchAndFetchById).toHaveBeenCalledTimes(1); + expect(User.patchAndFetchById).toHaveBeenCalledWith(userId, expect.any(Object)); + }); +}); diff --git a/comsapi/app/tests/unit/services/version.spec.js b/comsapi/app/tests/unit/services/version.spec.js new file mode 100644 index 00000000..5aef7069 --- /dev/null +++ b/comsapi/app/tests/unit/services/version.spec.js @@ -0,0 +1,210 @@ +const { NIL: OBJECT_ID, NIL: SYSTEM_USER, NIL: S3_VERSION_ID, NIL: VERSION_ID, NIL: ETAG } = require('uuid'); + +const { resetModel, trxBuilder } = require('../../common/helper'); +const Version = require('../../../src/db/models/tables/version'); + +const versionTrx = trxBuilder(); +jest.mock('../../../src/db/models/tables/version', () => ({ + startTransaction: jest.fn(), + then: jest.fn(), + + andWhere: jest.fn(), + delete: jest.fn(), + first: jest.fn(), + insert: jest.fn(), + modify: jest.fn(), + orderBy: jest.fn(), + patch: jest.fn(), + query: jest.fn(), + returning: jest.fn(), + throwIfNotFound: jest.fn(), + where: jest.fn() +})); + +const service = require('../../../src/services/version'); +// const { version } = require('winston'); + +beforeEach(() => { + jest.clearAllMocks(); + resetModel(Version, versionTrx); +}); + +describe('copy', () => { + // skipping these because we don't currently mock the reponse form a query + it.skip('Creates a new version db record from an existing record', async () => { + await service.copy(VERSION_ID, S3_VERSION_ID, OBJECT_ID, ETAG, SYSTEM_USER); + + expect(Version.startTransaction).toHaveBeenCalledTimes(1); + expect(Version.query).toHaveBeenCalledTimes(3); + expect(Version.query).toHaveBeenCalledWith(expect.anything()); + expect(Version.where).toHaveBeenCalledTimes(1); + expect(Version.where).toBeCalledWith( + expect.objectContaining({ + s3VersionId: expect.any(String), + objectId: expect.any(String) + }) + ); + expect(Version.first).toHaveBeenCalledTimes(1); + expect(Version.insert).toBeCalledWith( + expect.objectContaining({ + id: expect.any(String), + s3VersionId: S3_VERSION_ID, + objectId: OBJECT_ID, + mimeType: undefined, + deleteMarker: undefined, + createdBy: SYSTEM_USER + }) + ); + expect(Version.insert).toHaveBeenCalledTimes(1); + expect(versionTrx.commit).toHaveBeenCalledTimes(1); + }); + + it.skip('Creates a new version db record from an existing record - no sourceVersionId provided', async () => { + await service.copy(undefined, S3_VERSION_ID, OBJECT_ID, ETAG, SYSTEM_USER); + + expect(Version.startTransaction).toHaveBeenCalledTimes(1); + expect(Version.query).toHaveBeenCalledTimes(2); + expect(Version.query).toHaveBeenCalledWith(expect.anything()); + expect(Version.where).toHaveBeenCalledTimes(1); + expect(Version.where).toBeCalledWith( + expect.objectContaining({ + objectId: expect.any(String) + }) + ); + expect(Version.orderBy).toHaveBeenCalledTimes(1); + expect(Version.first).toHaveBeenCalledTimes(1); + expect(Version.insert).toHaveBeenCalledTimes(1); + expect(versionTrx.commit).toHaveBeenCalledTimes(1); + }); +}); + +describe('create', () => { + it('Saves a version of an object', async () => { + await service.create({ s3VersionId: S3_VERSION_ID, mimeType: 'mimeType', id: OBJECT_ID, deleteMarker: 'deleteMarker' }, SYSTEM_USER); + + expect(Version.startTransaction).toHaveBeenCalledTimes(1); + expect(Version.query).toHaveBeenCalledTimes(1); + expect(Version.query).toHaveBeenCalledWith(expect.anything()); + expect(Version.insert).toHaveBeenCalledTimes(1); + expect(Version.insert).toBeCalledWith( + expect.objectContaining({ + id: expect.any(String), + s3VersionId: S3_VERSION_ID, + objectId: OBJECT_ID, + mimeType: 'mimeType', + deleteMarker: 'deleteMarker', + createdBy: SYSTEM_USER + }) + ); + expect(Version.returning).toHaveBeenCalledTimes(1); + expect(Version.returning).toBeCalledWith('*'); + }); +}); + +describe('delete', () => { + it.skip('Delete a version record of an object', async () => { + await service.delete(OBJECT_ID, VERSION_ID); + + expect(Version.startTransaction).toHaveBeenCalledTimes(1); + expect(Version.query).toHaveBeenCalledTimes(1); + expect(Version.query).toHaveBeenCalledWith(expect.anything()); + expect(Version.delete).toHaveBeenCalledTimes(1); + expect(Version.where).toHaveBeenCalledTimes(2); + expect(Version.where).toBeCalledWith('objectId', OBJECT_ID); + expect(Version.where).toBeCalledWith('s3VersionId', VERSION_ID); + expect(Version.returning).toHaveBeenCalledTimes(1); + expect(Version.returning).toBeCalledWith('*'); + expect(Version.throwIfNotFound).toHaveBeenCalledTimes(1); + expect(versionTrx.commit).toHaveBeenCalledTimes(1); + }); +}); + +describe('get', () => { + it('Get a given version from the database - s3versionId provided', async () => { + await service.get({ s3VersionId: S3_VERSION_ID, objectId: OBJECT_ID }); + + expect(Version.startTransaction).toHaveBeenCalledTimes(1); + expect(Version.query).toHaveBeenCalledWith(expect.anything()); + expect(Version.where).toHaveBeenCalledTimes(1); + expect(Version.where).toBeCalledWith( + expect.objectContaining({ + s3VersionId: S3_VERSION_ID, + objectId: OBJECT_ID + }) + ); + expect(Version.first).toHaveBeenCalledTimes(1); + expect(versionTrx.commit).toHaveBeenCalledTimes(1); + }); + + it('Get a given version from the database - no s3versionId provided', async () => { + await service.get({ versionId: VERSION_ID, objectId: OBJECT_ID }); + + expect(Version.startTransaction).toHaveBeenCalledTimes(1); + expect(Version.query).toHaveBeenCalledWith(expect.anything()); + expect(Version.where).toHaveBeenCalledTimes(1); + expect(Version.where).toBeCalledWith( + expect.objectContaining({ + id: VERSION_ID, + objectId: OBJECT_ID + }) + ); + expect(Version.first).toHaveBeenCalledTimes(1); + expect(versionTrx.commit).toHaveBeenCalledTimes(1); + }); + + it('Get a given version from the database - no s3versionId and no versionId provided', async () => { + await service.get({ objectId: OBJECT_ID }); + + expect(Version.startTransaction).toHaveBeenCalledTimes(1); + expect(Version.query).toHaveBeenCalledWith(expect.anything()); + expect(Version.where).toHaveBeenCalledTimes(1); + expect(Version.where).toBeCalledWith('objectId', OBJECT_ID); + expect(Version.andWhere).toHaveBeenCalledTimes(1); + expect(Version.andWhere).toBeCalledWith('deleteMarker', false); + expect(Version.orderBy).toHaveBeenCalledTimes(1); + expect(Version.orderBy).toBeCalledWith('createdAt', 'desc'); + expect(Version.first).toHaveBeenCalledTimes(1); + expect(versionTrx.commit).toHaveBeenCalledTimes(1); + }); +}); + +describe('list', () => { + it('Query versions by objectId', async () => { + await service.list('abc'); + + expect(Version.startTransaction).toHaveBeenCalledTimes(1); + expect(Version.query).toHaveBeenCalledTimes(1); + expect(Version.query).toHaveBeenCalledWith(expect.anything()); + expect(Version.modify).toHaveBeenCalledTimes(1); + expect(Version.modify).toHaveBeenCalledWith('filterObjectId', 'abc'); + expect(Version.orderBy).toHaveBeenCalledTimes(1); + expect(Version.orderBy).toHaveBeenCalledWith('createdAt', 'DESC'); + }); +}); + +describe('update', () => { + it('Updates a version of an object', async () => { + await service.update({ id: OBJECT_ID, s3VersionId: S3_VERSION_ID, mimeType: 'mimeType' }); + + expect(Version.startTransaction).toHaveBeenCalledTimes(1); + expect(Version.query).toHaveBeenCalledTimes(1); + expect(Version.where).toHaveBeenCalledTimes(1); + expect(Version.where).toHaveBeenCalledWith( + { + objectId: OBJECT_ID, + s3VersionId: S3_VERSION_ID + } + ); + expect(Version.patch).toHaveBeenCalledTimes(1); + expect(Version.patch).toHaveBeenCalledWith( + { + objectId: OBJECT_ID, + updatedBy: SYSTEM_USER, + mimeType: 'mimeType' + } + ); + expect(Version.first).toHaveBeenCalledTimes(1); + expect(Version.returning).toHaveBeenCalledTimes(1); + expect(versionTrx.commit).toHaveBeenCalledTimes(1); + }); +}); diff --git a/comsapi/app/tests/unit/validators/bucket.spec.js b/comsapi/app/tests/unit/validators/bucket.spec.js new file mode 100644 index 00000000..18168d11 --- /dev/null +++ b/comsapi/app/tests/unit/validators/bucket.spec.js @@ -0,0 +1,299 @@ +const jestJoi = require('jest-joi'); +expect.extend(jestJoi.matchers); + +const { schema } = require('../../../src/validators/bucket'); +const { scheme, type } = require('../../../src/validators/common'); + +describe('createBucket', () => { + + describe('body', () => { + const body = schema.createBucket.body.describe(); + + describe('endpoint', () => { + const endpoint = body.keys.endpoint; + + it('is a string', () => { + expect(endpoint).toBeTruthy(); + expect(endpoint.type).toEqual('string'); + }); + + it('is a valid uri', () => { + expect(Array.isArray(endpoint.rules)).toBeTruthy(); + expect(endpoint.rules).toHaveLength(2); + expect(endpoint.rules).toEqual(expect.arrayContaining([ + expect.objectContaining({ + args: { + options: { scheme: /https?/ } + }, + name: 'uri' + }), + ])); + }); + + it('has a max length of 255', () => { + expect(Array.isArray(endpoint.rules)).toBeTruthy(); + expect(endpoint.rules).toHaveLength(2); + expect(endpoint.rules).toEqual(expect.arrayContaining([ + expect.objectContaining({ + args: { + limit: 255 + }, + name: 'max' + }), + ])); + }); + + it('is required', () => { + expect(endpoint.flags).toBeTruthy(); + expect(endpoint.flags).toEqual(expect.objectContaining({ + presence: 'required' + })); + }); + }); + + describe('key', () => { + const key = body.keys.key; + + it('is a string', () => { + expect(key).toBeTruthy(); + expect(key.type).toEqual('string'); + }); + + it('trims whitespace', () => { + expect(Array.isArray(key.rules)).toBeTruthy(); + expect(key.rules).toHaveLength(2); + expect(key.rules).toEqual(expect.arrayContaining([ + expect.objectContaining({ + args: { + enabled: true + }, + name: 'trim' + }), + ])); + }); + + it('has a max length of 255', () => { + expect(Array.isArray(key.rules)).toBeTruthy(); + expect(key.rules).toHaveLength(2); + expect(key.rules).toEqual(expect.arrayContaining([ + expect.objectContaining({ + args: { + limit: 255 + }, + name: 'max' + }), + ])); + }); + }); + + it('should match the schema', () => { + const value = { + body: { + secretAccessKey: 'xyz', + accessKeyId: 'bbb', + bucket: 'ccc', + endpoint: 'https://s3.ca', + bucketName: 'My Bucket', + active: true + } + }; + expect(value).toMatchSchema(schema.createBucket); + }); + + + it('is required', () => { + expect(body.flags).toBeTruthy(); + expect(body.flags).toEqual(expect.objectContaining({ presence: 'required' })); + }); + }); +}); + +describe('deleteBucket', () => { + + describe('params', () => { + const params = schema.deleteBucket.params.describe(); + + describe('bucketId', () => { + const bucketId = params.keys.bucketId; + + it('is the expected schema', () => { + expect(bucketId).toEqual(type.uuidv4.describe()); + }); + }); + }); +}); + +describe('headBucket', () => { + + describe('params', () => { + const params = schema.headBucket.params.describe(); + + describe('bucketId', () => { + const bucketId = params.keys.bucketId; + + it('is the expected schema', () => { + expect(bucketId).toEqual({ + flags: { presence: 'required' }, + rules: [{ args: { options: { version: 'uuidv4' } }, name: 'guid' }], + type: 'string' + }); + }); + }); + }); + +}); + +describe('readBucket', () => { + + describe('params', () => { + const params = schema.readBucket.params.describe(); + + describe('bucketId', () => { + const bucketId = params.keys.bucketId; + + it('is the expected schema', () => { + expect(bucketId).toEqual(type.uuidv4.required().describe()); + }); + }); + }); + +}); + +describe('searchBuckets', () => { + + describe('query', () => { + const query = schema.searchBuckets.query.describe(); + + describe('bucketId', () => { + const bucketId = query.keys.bucketId; + + it('is the expected schema', () => { + expect(bucketId).toEqual(scheme.guid.describe()); + }); + }); + + describe('bucketName', () => { + const bucketName = query.keys.bucketName; + + it('is the expected schema', () => { + expect(bucketName.type).toEqual('string'); + }); + + it('has a max length of 255', () => { + expect(Array.isArray(bucketName.rules)).toBeTruthy(); + expect(bucketName.rules).toHaveLength(1); + expect(bucketName.rules).toEqual(expect.arrayContaining([ + expect.objectContaining({ 'args': { 'limit': 255 }, 'name': 'max' }), + ])); + }); + }); + + describe('key', () => { + const key = query.keys.key; + + it('is the expected schema', () => { + expect(key.type).toEqual('string'); + }); + + it('has a max length of 255', () => { + expect(Array.isArray(key.rules)).toBeTruthy(); + expect(key.rules).toHaveLength(1); + expect(key.rules).toEqual(expect.arrayContaining([ + expect.objectContaining({ 'args': { 'limit': 255 }, 'name': 'max' }), + ])); + }); + }); + + describe('active', () => { + const active = query.keys.active; + + it('is the expected schema', () => { + expect(active).toEqual(type.truthy.describe()); + }); + }); + }); +}); + +describe('syncBucket', () => { + + describe('params', () => { + const params = schema.syncBucket.params.describe(); + + describe('bucketId', () => { + const bucketId = params.keys.bucketId; + + it('is the expected schema', () => { + expect(bucketId).toEqual(type.uuidv4.required().describe()); + }); + }); + }); +}); + +describe('updateBucket', () => { + + describe('body', () => { + const body = schema.updateBucket.body.describe(); + + describe('endpoint', () => { + const endpoint = body.keys.endpoint; + + it('is a string', () => { + expect(endpoint).toBeTruthy(); + expect(endpoint.type).toEqual('string'); + }); + + it('is a valid uri', () => { + expect(Array.isArray(endpoint.rules)).toBeTruthy(); + expect(endpoint.rules).toHaveLength(2); + expect(endpoint.rules).toEqual(expect.arrayContaining([ + expect.objectContaining({ + args: { + options: { scheme: /https?/ } + }, + name: 'uri' + }), + ])); + }); + + it('has a max length of 255', () => { + expect(Array.isArray(endpoint.rules)).toBeTruthy(); + expect(endpoint.rules).toHaveLength(2); + expect(endpoint.rules).toEqual(expect.arrayContaining([ + expect.objectContaining({ + args: { + limit: 255 + }, + name: 'max' + }), + ])); + }); + }); + + it('to be an object ', () => { + expect(body).toBeTruthy(); + expect(body.type).toEqual('object'); + }); + + it('value to match schema', () => { + const value = { + body: { + bucketName: 'My Re-named Bucket', + } + }; + expect(value).toMatchSchema(schema.updateBucket); + }); + }); + + describe('params', () => { + const params = schema.updateBucket.params.describe(); + + describe('bucketId', () => { + const bucketId = params.keys.bucketId; + + it('is the expected schema', () => { + expect(bucketId).toEqual(type.uuidv4.describe()); + }); + }); + }); + +}); diff --git a/comsapi/app/tests/unit/validators/bucketPermission.spec.js b/comsapi/app/tests/unit/validators/bucketPermission.spec.js new file mode 100644 index 00000000..78130be1 --- /dev/null +++ b/comsapi/app/tests/unit/validators/bucketPermission.spec.js @@ -0,0 +1,187 @@ +const Joi = require('joi'); +const jestJoi = require('jest-joi'); +expect.extend(jestJoi.matchers); + +const { schema } = require('../../../src/validators/bucketPermission'); +const { scheme, type } = require('../../../src/validators/common'); +const { Permissions } = require('../../../src/components/constants'); + +describe('searchPermissions', () => { + + describe('query', () => { + const query = schema.searchPermissions.query.describe(); + + describe('userId', () => { + const userId = query.keys.userId; + + // TODO: test against schema in our code instead of recreating object + it('is the expected schema', () => { + expect(userId).toEqual(Joi.alternatives() + .conditional('objectPerms', { + is: true, + then: type.uuidv4 + .required() + .messages({ + 'string.guid': 'One userId required when `objectPerms=true`', + }), + otherwise: scheme.guid }).describe()); + }); + }); + + describe('bucketId', () => { + const bucketId = query.keys.bucketId; + + it('is the expected schema', () => { + expect(bucketId).toEqual(scheme.guid.describe()); + }); + }); + + describe('permCode', () => { + const permCode = query.keys.permCode; + + it('is the expected schema', () => { + expect(permCode).toEqual(scheme.permCode.describe()); + }); + }); + + describe('objectPerms', () => { + const objectPerms = query.keys.objectPerms; + + it('is the expected schema', () => { + expect(objectPerms).toBeTruthy(); + }); + }); + }); +}); + +describe('listPermissions', () => { + + describe('query', () => { + const params = schema.listPermissions.params.describe(); + const query = schema.listPermissions.query.describe(); + + describe('userId', () => { + const userId = query.keys.userId; + + it('is the expected schema', () => { + expect(userId).toEqual(scheme.guid.describe()); + }); + }); + + describe('bucketId', () => { + const bucketId = params.keys.bucketId; + + it('is the expected schema', () => { + expect(bucketId).toEqual(scheme.guid.describe()); + }); + }); + + describe('permCode', () => { + const permCode = query.keys.permCode; + + it('is the expected schema', () => { + expect(permCode).toEqual(scheme.permCode.describe()); + }); + }); + }); +}); + +describe('addPermissions', () => { + + describe('params', () => { + const params = schema.addPermissions.params.describe(); + + describe('bucketId', () => { + const bucketId = params.keys.bucketId; + + it('is the expected schema', () => { + expect(bucketId).toEqual(type.uuidv4.describe()); + }); + }); + }); + + describe('body', () => { + const body = schema.addPermissions.body.describe(); + + it('is an array', () => { + expect(body).toBeTruthy(); + expect(body.type).toEqual('array'); + expect(Array.isArray(body.items)).toBeTruthy(); + }); + + it('is required', () => { + expect(body.flags).toBeTruthy(); + expect(body.flags).toEqual(expect.objectContaining({ presence: 'required' })); + }); + + it('should contain userId', () => { + expect(body.items).toEqual(expect.arrayContaining([ + expect.objectContaining({ + keys: expect.objectContaining({ + userId: expect.objectContaining({ + type: 'string', + flags: expect.objectContaining({ presence: 'required' }), + rules: expect.arrayContaining([ + expect.objectContaining({ + name: 'guid', + args: { + options: { version: 'uuidv4' } + } + }) + ]) + }) + }) + }) + ])); + }); + + it('should contain a valid permCode', () => { + expect(body.items).toEqual(expect.arrayContaining([ + expect.objectContaining({ + keys: expect.objectContaining({ + permCode: expect.objectContaining({ + type: 'string', + flags: expect.objectContaining({ presence: 'required' }), + allow: expect.arrayContaining(Object.values(Permissions)) + }) + }) + }) + ])); + }); + }); +}); + +describe('removePermissions', () => { + + describe('params', () => { + const params = schema.removePermissions.params.describe(); + + describe('bucketId', () => { + const bucketId = params.keys.bucketId; + + it('is the expected schema', () => { + expect(bucketId).toEqual(type.uuidv4.describe()); + }); + }); + }); + + describe('query', () => { + const query = schema.listPermissions.query.describe(); + + describe('userId', () => { + const userId = query.keys.userId; + + it('is the expected schema', () => { + expect(userId).toEqual(scheme.guid.describe()); + }); + }); + + describe('permCode', () => { + const permCode = query.keys.permCode; + + it('is the expected schema', () => { + expect(permCode).toEqual(scheme.permCode.describe()); + }); + }); + }); +}); diff --git a/comsapi/app/tests/unit/validators/common.spec.js b/comsapi/app/tests/unit/validators/common.spec.js new file mode 100644 index 00000000..68ed3bae --- /dev/null +++ b/comsapi/app/tests/unit/validators/common.spec.js @@ -0,0 +1,427 @@ +const crypto = require('crypto'); +const jestJoi = require('jest-joi'); +expect.extend(jestJoi.matchers); + +const { EMAILREGEX, Permissions } = require('../../../src/components/constants'); +const { scheme, type } = require('../../../src/validators/common'); + +describe('type', () => { + const longStr = crypto.randomBytes(256).toString('hex'); + + describe('alphanum', () => { + const model = type.alphanum.describe(); + + it('is a string', () => { + expect(model).toBeTruthy(); + expect(model.type).toEqual('string'); + }); + + it('is an alphanum', () => { + expect(Array.isArray(model.rules)).toBeTruthy(); + expect(model.rules).toHaveLength(2); + expect(model.rules).toEqual(expect.arrayContaining([ + expect.objectContaining({ + 'name': 'alphanum' + }) + ])); + }); + + it('has a max length of 255', () => { + expect(Array.isArray(model.rules)).toBeTruthy(); + expect(model.rules).toHaveLength(2); + expect(model.rules).toEqual(expect.arrayContaining([ + expect.objectContaining({ + 'args': { + 'limit': 255 + }, + 'name': 'max' + }), + ])); + }); + + it('matches the schema', () => { + expect('someuser').toMatchSchema(type.alphanum); + }); + + it('must be less than or equal to 255 characters long', () => { + expect(longStr).not.toMatchSchema(type.alphanum); + }); + }); + + describe('email', () => { + const model = type.email.describe(); + it('is a string', () => { + expect(model).toBeTruthy(); + expect(model.type).toEqual('string'); + }); + + it('is an email', () => { + expect(Array.isArray(model.rules)).toBeTruthy(); + expect(model.rules).toHaveLength(2); + expect(model.rules).toEqual(expect.arrayContaining([ + expect.objectContaining({ + 'args': { + 'regex': new RegExp(EMAILREGEX).toString() + }, + 'name': 'pattern' + }), + expect.objectContaining({ + 'args': { + 'limit': 255 + }, + 'name': 'max' + }) + ])); + }); + + it('is a string', () => { + expect(model).toBeTruthy(); + expect(model.type).toEqual('string'); + }); + + it('matches the schema', () => { + expect('test@test.com').toMatchSchema(type.email); + }); + + it('rejects the schema when not a valid email', () => { + expect('test_at_test_dot_com').not.toMatchSchema(type.email); + }); + + it('is not greater than 255 characters', () => { + expect(longStr).not.toMatchSchema(type.email); + }); + }); + + describe('truthy', () => { + const model = type.truthy.describe(); + + it('is a boolean', () => { + expect(model).toBeTruthy(); + expect(model.type).toEqual('boolean'); + }); + + it('contains truthy array', () => { + expect(Array.isArray(model.truthy)).toBeTruthy(); + expect(model.truthy).toHaveLength(12); + }); + + it.each([ + true, 1, 'true', 'TRUE', 't', 'T', 'yes', 'yEs', 'y', 'Y', '1', + false, 0, 'false', 'FALSE', 'f', 'F', 'no', 'nO', 'n', 'N', '0' + ])('accepts the schema given %j', (value) => { + expect(value).toMatchSchema(type.truthy); + }); + }); + + describe('uuidv4', () => { + const model = type.uuidv4.describe(); + + it('is a uuidv4', () => { + expect(model).toBeTruthy(); + expect(model.type).toEqual('string'); + expect(Array.isArray(model.rules)).toBeTruthy(); + expect(model.rules).toHaveLength(1); + expect(model.rules).toEqual(expect.arrayContaining([ + expect.objectContaining({ + name: 'guid', + args: { + options: { version: 'uuidv4' } + } + }) + ])); + }); + + it('matches the schema with single guid', () => { + expect('11bf5b37-e0b8-42e0-8dcf-dc8c4aefc000').toMatchSchema(type.uuidv4); + }); + + it('rejects the schema with incorrect guid', () => { + expect('notauuidv4').not.toMatchSchema(type.uuidv4); + }); + }); + + describe('metadata', () => { + const func = type.metadata(1, 1); + const model = func.describe(); + + it('enforces general metadata pattern', () => { + expect(model.patterns).toEqual(expect.arrayContaining([ + expect.objectContaining({ + regex: '/^x-amz-meta-\\S+$/i', + rule: expect.objectContaining({ + type: 'string', + rules: expect.arrayContaining([ + expect.objectContaining({ + name: 'min', + args: expect.objectContaining({ + limit: 1 + }) + }) + ]) + }) + }) + ])); + }); + }); + + describe('tagset', () => { + const func = type.tagset({ maxKeyCount: 1, minKeyCount: 1 }); + const model = func.describe(); + + it('is an object', () => { + expect(model).toBeTruthy(); + expect(model.type).toEqual('object'); + }); + + it('enforces general tagset pattern', () => { + expect(model.patterns).toEqual(expect.arrayContaining([ + expect.objectContaining({ + matches: expect.objectContaining({ + rules: expect.arrayContaining([ + expect.objectContaining({ + name: 'min', + args: expect.objectContaining({ + limit: 1 + }) + }), + expect.objectContaining({ + name: 'max', + args: expect.objectContaining({ + limit: 1 + }) + }) + ]), + type: 'array' + }), + regex: '/^(?!coms-id$).{1,255}$/', + rule: expect.objectContaining({ + type: 'string', + rules: expect.arrayContaining([ + expect.objectContaining({ + name: 'min', + args: expect.objectContaining({ + limit: 0 + }) + }), + expect.objectContaining({ + name: 'max', + args: expect.objectContaining({ + limit: 255 + }) + }) + ]), + }), + }) + ])); + }); + }); + +}); + +describe('scheme', () => { + + describe('guid', () => { + const model = scheme.guid.describe(); + + it('is of type alternatives', () => { + expect(model).toBeTruthy(); + expect(model.type).toEqual('alternatives'); + expect(Array.isArray(model.matches)).toBeTruthy(); + expect(model.matches).toHaveLength(2); + }); + + it('allows array containing guid of type uuidv4', () => { + expect(model.matches).toEqual(expect.arrayContaining([ + expect.objectContaining({ + schema: expect.objectContaining({ + type: 'csvArray', + items: expect.arrayContaining([ + expect.objectContaining({ + type: 'string', + rules: expect.arrayContaining([ + expect.objectContaining({ + name: 'guid', + args: { + options: { version: 'uuidv4' } + } + }) + ]) + }) + ]) + }) + }) + ])); + }); + + it('allows single guid of type uuidv4', () => { + expect(model.matches).toEqual(expect.arrayContaining([ + expect.objectContaining({ + schema: expect.objectContaining({ + type: 'csvArray', + items: expect.arrayContaining([ + expect.objectContaining({ + type: 'string', + rules: expect.arrayContaining([ + expect.objectContaining({ + name: 'guid', + args: { + options: { version: 'uuidv4' } + } + }) + ]) + }) + ]) + }) + }) + ])); + }); + + it('matches the schema with array', () => { + expect(['11bf5b37-e0b8-42e0-8dcf-dc8c4aefc000']).toMatchSchema(scheme.guid); + }); + + it('rejects the schema with array', () => { + expect(['11bf5b37-e0b8-42e0-8dcf-dc8c4aefc000', 'notauuidv4']).not.toMatchSchema(scheme.guid); + }); + + it('matches the schema with single guid', () => { + expect('11bf5b37-e0b8-42e0-8dcf-dc8c4aefc000').toMatchSchema(scheme.guid); + }); + + it('rejects the schema with incorrect guid', () => { + expect('notauuidv4').not.toMatchSchema(scheme.guid); + }); + }); + + describe('string', () => { + const model = scheme.string.describe(); + + it('is of type alternatives', () => { + expect(model).toBeTruthy(); + expect(model.type).toEqual('alternatives'); + expect(Array.isArray(model.matches)).toBeTruthy(); + expect(model.matches).toHaveLength(2); + }); + + it('allows array containing strings', () => { + expect(model.matches).toEqual(expect.arrayContaining([ + expect.objectContaining({ + schema: expect.objectContaining({ + type: 'csvArray', + items: expect.arrayContaining([ + expect.objectContaining({ + type: 'string', + rules: expect.arrayContaining([ + expect.objectContaining({ + args: { + limit: 255 + }, + name: 'max' + }) + ]) + }) + ]) + }) + }) + ])); + }); + + it('allows single string', () => { + expect(model.matches).toEqual(expect.arrayContaining([ + expect.objectContaining({ + schema: expect.objectContaining({ + type: 'csvArray', + items: expect.arrayContaining([ + expect.objectContaining({ + type: 'string', + rules: expect.arrayContaining([ + expect.objectContaining({ + args: { + limit: 255 + }, + name: 'max' + }) + ]) + }) + ]) + }) + }) + ])); + }); + + it('matches the schema with array', () => { + expect(['STRING A', 'STRING B']).toMatchSchema(scheme.string); + }); + + it('rejects the schema with array containing non string', () => { + expect(['STRING A', 1234]).not.toMatchSchema(scheme.string); + }); + + it('matches the schema with single string', () => { + expect('STRING A').toMatchSchema(scheme.string); + }); + + it('rejects the schema with non string value', () => { + expect(1234).not.toMatchSchema(scheme.string); + }); + }); + + describe('permCode', () => { + const model = scheme.permCode.describe(); + + it('is of type alternatives', () => { + expect(model).toBeTruthy(); + expect(model.type).toEqual('alternatives'); + expect(Array.isArray(model.matches)).toBeTruthy(); + expect(model.matches).toHaveLength(2); + }); + + it('allows array containing valid permCodes', () => { + expect(model.matches).toEqual(expect.arrayContaining([ + expect.objectContaining({ + schema: expect.objectContaining({ + type: 'csvArray', + items: expect.arrayContaining([ + expect.objectContaining({ + type: 'string', + allow: expect.arrayContaining(Object.values(Permissions)) + }) + ]) + }) + }) + ])); + }); + + it('allows a single valid permCode ', () => { + expect(model.matches).toEqual(expect.arrayContaining([ + expect.objectContaining({ + schema: expect.objectContaining({ + type: 'csvArray', + items: expect.arrayContaining([ + expect.objectContaining({ + type: 'string', + allow: expect.arrayContaining(Object.values(Permissions)) + }) + ]) + }) + }) + ])); + }); + + it('matches the schema with valid permissions array', () => { + expect([Permissions.UPDATE, Permissions.READ]).toMatchSchema(scheme.permCode); + }); + + it('rejects the schema with invalid permissions array', () => { + expect([Permissions.UPDATE, 'BADPERM']).not.toMatchSchema(scheme.permCode); + }); + + it('matches the schema with single permission', () => { + expect(Permissions.UPDATE).toMatchSchema(scheme.permCode); + }); + + it('rejects the schema with single invalid permission', () => { + expect('BADPERM').not.toMatchSchema(scheme.permCode); + }); + }); +}); diff --git a/comsapi/app/tests/unit/validators/metadata.spec.js b/comsapi/app/tests/unit/validators/metadata.spec.js new file mode 100644 index 00000000..6c4596f2 --- /dev/null +++ b/comsapi/app/tests/unit/validators/metadata.spec.js @@ -0,0 +1,15 @@ +const jestJoi = require('jest-joi'); +expect.extend(jestJoi.matchers); + +const { schema } = require('../../../src/validators/metadata'); +const { type } = require('../../../src/validators/common'); + +describe('searchMetadata', () => { + describe('headers', () => { + const headers = schema.searchMetadata.headers.describe(); + + it('is the expected schema', () => { + expect(headers).toEqual(type.metadata(0).describe()); + }); + }); +}); diff --git a/comsapi/app/tests/unit/validators/object.spec.js b/comsapi/app/tests/unit/validators/object.spec.js new file mode 100644 index 00000000..61d62c3e --- /dev/null +++ b/comsapi/app/tests/unit/validators/object.spec.js @@ -0,0 +1,594 @@ +const crypto = require('crypto'); +const Joi = require('joi'); +const jestJoi = require('jest-joi'); +const { DownloadMode } = require('../../../src/components/constants'); +expect.extend(jestJoi.matchers); + +const { schema } = require('../../../src/validators/object'); +const { scheme, type } = require('../../../src/validators/common'); + +jest.mock('config'); + +beforeEach(() => { + jest.resetAllMocks(); +}); + +describe('addMetadata', () => { + + describe('headers', () => { + const headers = schema.addMetadata.headers.describe(); + + it('is the expected schema', () => { + expect(headers).toEqual(type.metadata(1, 1).describe()); + }); + }); + + describe('params', () => { + const params = schema.addMetadata.params.describe(); + + describe('objectId', () => { + const objectId = params.keys.objectId; + + it('is the expected schema', () => { + expect(objectId).toEqual(type.uuidv4.describe()); + }); + }); + }); + + describe('query', () => { + const query = schema.addMetadata.query.describe(); + + describe('s3VersionId', () => { + const s3VersionId = query.keys.s3VersionId; + + it('is the expected schema', () => { + expect(s3VersionId).toEqual(Joi.string().describe()); + }); + }); + }); +}); + +describe('addTags', () => { + + describe('params', () => { + const params = schema.addTags.params.describe(); + + describe('objectId', () => { + const objectId = params.keys.objectId; + + it('is the expected schema', () => { + expect(objectId).toEqual(type.uuidv4.describe()); + }); + }); + }); + + describe('query', () => { + const query = schema.addTags.query.describe(); + + describe('s3VersionId', () => { + const s3VersionId = query.keys.s3VersionId; + + it('is the expected schema', () => { + expect(s3VersionId).toEqual(Joi.string().describe()); + }); + }); + + describe('tagset', () => { + const tagset = query.keys.tagset; + + it('is the expected schema', () => { + expect(tagset).toEqual(type.tagset(1, 1).describe()); + }); + }); + }); +}); + +describe('createObject', () => { + + describe('headers', () => { + const headers = schema.createObject.headers.describe(); + + it('is the expected schema', () => { + expect(headers).toEqual(type.metadata().describe()); + }); + }); + + describe('query', () => { + describe('bucketId', () => { + const bucketId = schema.createObject.query.describe().keys.bucketId; + + it('is the expected schema', () => { + expect(bucketId).toEqual(type.uuidv4.describe()); + }); + }); + + describe('tagset', () => { + const tagset = schema.createObject.query.describe().keys.tagset; + + it('is the expected schema', () => { + expect(tagset).toEqual(type.tagset().describe()); + }); + }); + }); +}); + +describe('deleteMetadata', () => { + + describe('headers', () => { + const headers = schema.deleteMetadata.headers.describe(); + + it('is the expected schema', () => { + expect(headers).toEqual(type.metadata().describe()); + }); + }); + + describe('params', () => { + const params = schema.deleteMetadata.params.describe(); + + describe('objectId', () => { + const objectId = params.keys.objectId; + + it('is the expected schema', () => { + expect(objectId).toEqual(type.uuidv4.describe()); + }); + }); + }); + + describe('query', () => { + const query = schema.deleteMetadata.query.describe(); + + describe('s3VersionId', () => { + const s3VersionId = query.keys.s3VersionId; + + it('is the expected schema', () => { + expect(s3VersionId).toEqual(Joi.string().describe()); + }); + }); + }); +}); + +describe('deleteObject', () => { + + describe('params', () => { + const params = schema.deleteObject.params.describe(); + + describe('objectId', () => { + const objectId = params.keys.objectId; + + it('is the expected schema', () => { + expect(objectId).toEqual(type.uuidv4.describe()); + }); + }); + }); + + describe('query', () => { + const query = schema.deleteObject.query.describe(); + + describe('s3VersionId', () => { + const s3VersionId = query.keys.s3VersionId; + + it('is the expected schema', () => { + expect(s3VersionId).toEqual(Joi.string().describe()); + }); + }); + }); +}); + +describe('deleteTags', () => { + + describe('params', () => { + const params = schema.deleteTags.params.describe(); + + describe('objectId', () => { + const objectId = params.keys.objectId; + + it('is the expected schema', () => { + expect(objectId).toEqual(type.uuidv4.describe()); + }); + }); + }); + + describe('query', () => { + const query = schema.deleteTags.query.describe(); + + describe('s3VersionId', () => { + const s3VersionId = query.keys.s3VersionId; + + it('is the expected schema', () => { + expect(s3VersionId).toEqual(Joi.string().describe()); + }); + }); + + describe('tagset', () => { + const tagset = query.keys.tagset; + + it('is the expected schema', () => { + expect(tagset).toEqual(type.tagset().describe()); + }); + }); + }); +}); + +describe('headObject', () => { + + describe('params', () => { + const params = schema.headObject.params.describe(); + + describe('objectId', () => { + const objectId = params.keys.objectId; + + it('is the expected schema', () => { + expect(objectId).toEqual({ + flags: { presence: 'required' }, + ...type.uuidv4.describe() + }); + }); + }); + }); + + describe('query', () => { + const query = schema.headObject.query.describe(); + + describe('s3VersionId', () => { + const s3VersionId = query.keys.s3VersionId; + + it('is the expected schema', () => { + expect(s3VersionId).toEqual(Joi.string().describe()); + }); + }); + }); +}); + +describe('listObjectVersion', () => { + + describe('params', () => { + const params = schema.listObjectVersion.params.describe(); + + describe('objectId', () => { + const objectId = params.keys.objectId; + + it('is the expected schema', () => { + expect(objectId).toEqual(type.uuidv4.describe()); + }); + }); + }); +}); + +describe('readObject', () => { + + describe('params', () => { + const params = schema.readObject.params.describe(); + + describe('objectId', () => { + const objectId = params.keys.objectId; + + it('is the expected schema', () => { + expect(objectId).toEqual(type.uuidv4.describe()); + }); + }); + }); + + describe('query', () => { + const query = schema.readObject.query.describe(); + + describe('s3VersionId', () => { + const s3VersionId = query.keys.s3VersionId; + + it('is the expected schema', () => { + expect(s3VersionId).toEqual(Joi.string().describe()); + }); + }); + + describe('expiresIn', () => { + const expiresIn = query.keys.expiresIn; + + it('is the expected schema', () => { + expect(expiresIn).toEqual(Joi.number().describe()); + }); + }); + + describe('download', () => { + const download = query.keys.download; + + it('is the expected schema', () => { + expect(download).toEqual(expect.objectContaining({ + type: 'string', + allow: expect.arrayContaining(Object.values(DownloadMode)) + })); + }); + }); + }); +}); + +describe('replaceMetadata', () => { + + describe('headers', () => { + const headers = schema.replaceMetadata.headers.describe(); + + it('is the expected schema', () => { + expect(headers).toEqual(type.metadata().describe()); + }); + }); + + describe('params', () => { + const params = schema.replaceMetadata.params.describe(); + + describe('objectId', () => { + const objectId = params.keys.objectId; + + it('is the expected schema', () => { + expect(objectId).toEqual(type.uuidv4.describe()); + }); + }); + }); + + describe('query', () => { + const query = schema.replaceMetadata.query.describe(); + + describe('s3VersionId', () => { + const s3VersionId = query.keys.s3VersionId; + + it('is the expected schema', () => { + expect(s3VersionId).toEqual(Joi.string().describe()); + }); + }); + }); +}); + + +describe('replaceTags', () => { + + describe('params', () => { + const params = schema.replaceTags.params.describe(); + + describe('objectId', () => { + const objectId = params.keys.objectId; + + it('is the expected schema', () => { + expect(objectId).toEqual(type.uuidv4.describe()); + }); + }); + }); + + describe('query', () => { + const query = schema.replaceTags.query.describe(); + + describe('s3VersionId', () => { + const s3VersionId = query.keys.s3VersionId; + + it('is the expected schema', () => { + expect(s3VersionId).toEqual(Joi.string().describe()); + }); + }); + + describe('tagset', () => { + const tagset = query.keys.tagset; + + it('is the expected schema', () => { + expect(tagset).toEqual(type.tagset().describe()); + }); + }); + }); +}); + +describe('searchObjects', () => { + + describe('headers', () => { + const headers = schema.searchObjects.headers.describe(); + + it('is an object', () => { + expect(headers).toBeTruthy(); + expect(headers.type).toEqual('object'); + }); + + it('permits other attributes', () => { + expect(headers.flags).toBeTruthy(); + expect(headers.flags).toEqual(expect.objectContaining({ + unknown: true + })); + }); + + it('enforces general metadata pattern', () => { + expect(headers.patterns).toEqual(expect.arrayContaining([ + expect.objectContaining({ + regex: '/^x-amz-meta-\\S+$/i', + rule: expect.objectContaining({ + type: 'string', + rules: expect.arrayContaining([ + expect.objectContaining({ + name: 'min', + args: expect.objectContaining({ + limit: 1 + }) + }) + ]) + }) + }) + ])); + }); + }); + + describe('query', () => { + const query = schema.searchObjects.query.describe(); + + describe('objectId', () => { + const objectId = query.keys.objectId; + + it('is the expected schema', () => { + expect(objectId).toEqual(scheme.guid.describe()); + }); + }); + + describe('name', () => { + const name = query.keys.name; + + it('is the expected schema', () => { + expect(name.type).toEqual('string'); + }); + }); + + describe('path', () => { + const path = query.keys.path; + + it('is a string', () => { + expect(path).toBeTruthy(); + expect(path.type).toEqual('string'); + }); + + it('has a max length of 1024', () => { + expect(Array.isArray(path.rules)).toBeTruthy(); + expect(path.rules).toHaveLength(1); + expect(path.rules).toEqual(expect.arrayContaining([ + expect.objectContaining({ + 'args': { + 'limit': 1024 + }, + 'name': 'max' + }), + ])); + }); + + it('matches the schema', () => { + expect('some/path/to/object').toMatchSchema(Joi.string().max(1024)); + }); + + it('must be less than or equal to 1024 characters long', () => { + const reallyLongStr = crypto.randomBytes(1025).toString('hex'); + expect(reallyLongStr).not.toMatchSchema(Joi.string().max(1024)); + }); + }); + + describe('mimeType', () => { + const mimeType = query.keys.mimeType; + + it('is a string', () => { + expect(mimeType).toBeTruthy(); + expect(mimeType.type).toEqual('string'); + }); + + it('has a max length of 255', () => { + expect(Array.isArray(mimeType.rules)).toBeTruthy(); + expect(mimeType.rules).toHaveLength(1); + expect(mimeType.rules).toEqual(expect.arrayContaining([ + expect.objectContaining({ + 'args': { + 'limit': 255 + }, + 'name': 'max' + }), + ])); + }); + + it('matches the schema', () => { + expect('image/jpeg').toMatchSchema(Joi.string().max(255)); + }); + + it('must be less than or equal to 255 characters long', () => { + const longStr = crypto.randomBytes(256).toString('hex'); + expect(longStr).not.toMatchSchema(Joi.string().max(255)); + }); + }); + + describe('tagset', () => { + const tagset = query.keys.tagset; + + it('is the expected schema', () => { + expect(tagset).toEqual(type.tagset().describe()); + }); + }); + + describe('public', () => { + const publicKey = query.keys.public; + + it('is the expected schema', () => { + expect(publicKey).toEqual(type.truthy.describe()); + }); + }); + + describe('active', () => { + const active = query.keys.active; + + it('is the expected schema', () => { + expect(active).toEqual(type.truthy.describe()); + }); + }); + }); +}); + +describe('syncObject', () => { + + describe('params', () => { + const params = schema.syncObject.params.describe(); + + describe('bucketId', () => { + const objectId = params.keys.objectId; + + it('is the expected schema', () => { + expect(objectId).toEqual(type.uuidv4.required().describe()); + }); + }); + }); +}); + +describe('togglePublic', () => { + + describe('params', () => { + const params = schema.togglePublic.params.describe(); + + describe('objectId', () => { + const objectId = params.keys.objectId; + + it('is the expected schema', () => { + expect(objectId).toEqual(type.uuidv4.describe()); + }); + }); + }); + + describe('query', () => { + const query = schema.togglePublic.query.describe(); + + describe('public', () => { + const publicKey = query.keys.public; + + it('is the expected schema', () => { + expect(publicKey).toEqual(type.truthy.describe()); + }); + }); + }); +}); + + +describe('updateObject', () => { + + describe('headers', () => { + const headers = schema.updateObject.headers.describe(); + + it('is the expected schema', () => { + expect(headers).toEqual(type.metadata().describe()); + }); + }); + + describe('params', () => { + const params = schema.updateObject.params.describe(); + + describe('objectId', () => { + const objectId = params.keys.objectId; + + it('is the expected schema', () => { + expect(objectId).toEqual(type.uuidv4.describe()); + }); + }); + }); + + describe('query', () => { + const query = schema.updateObject.query.describe(); + + describe('tagset', () => { + const tagset = query.keys.tagset; + + it('is the expected schema', () => { + expect(tagset).toEqual(type.tagset().describe()); + }); + }); + }); +}); diff --git a/comsapi/app/tests/unit/validators/objectPermission.spec.js b/comsapi/app/tests/unit/validators/objectPermission.spec.js new file mode 100644 index 00000000..c9a22310 --- /dev/null +++ b/comsapi/app/tests/unit/validators/objectPermission.spec.js @@ -0,0 +1,195 @@ +const jestJoi = require('jest-joi'); +const Joi = require('joi'); +expect.extend(jestJoi.matchers); + +const { schema } = require('../../../src/validators/objectPermission'); +const { scheme, type } = require('../../../src/validators/common'); +const { Permissions } = require('../../../src/components/constants'); + +describe('searchPermissions', () => { + + describe('query', () => { + const query = schema.searchPermissions.query.describe(); + + describe('userId', () => { + const userId = query.keys.userId; + + // TODO: test against schema in our code instead of recreating object + it('is the expected schema', () => { + expect(userId).toEqual(Joi.alternatives() + .conditional('bucketPerms', { + is: true, + then: type.uuidv4 + .required() + .messages({ + 'string.guid': 'One userId required when `bucketPerms=true`', + }), + otherwise: scheme.guid }).describe()); + }); + }); + + describe('objectId', () => { + const objectId = query.keys.objectId; + + it('is the expected schema', () => { + expect(objectId).toEqual(scheme.guid.describe()); + }); + }); + + describe('bucketId', () => { + const bucketId = query.keys.bucketId; + + it('is the expected schema', () => { + expect(bucketId).toEqual(scheme.guid.describe()); + }); + }); + + describe('permCode', () => { + const permCode = query.keys.permCode; + + it('is the expected schema', () => { + expect(permCode).toEqual(scheme.permCode.describe()); + }); + }); + + describe('bucketPerms', () => { + const bucketPerms = query.keys.bucketPerms; + + it('is the expected schema', () => { + expect(bucketPerms).toBeTruthy(); + }); + }); + }); +}); + +describe('listPermissions', () => { + + describe('query', () => { + const params = schema.listPermissions.params.describe(); + const query = schema.listPermissions.query.describe(); + + describe('userId', () => { + const userId = query.keys.userId; + + it('is the expected schema', () => { + expect(userId).toEqual(scheme.guid.describe()); + }); + }); + + describe('objectId', () => { + const objectId = params.keys.objectId; + + it('is the expected schema', () => { + expect(objectId).toEqual(scheme.guid.describe()); + }); + }); + + describe('permCode', () => { + const permCode = query.keys.permCode; + + it('is the expected schema', () => { + expect(permCode).toEqual(scheme.permCode.describe()); + }); + }); + }); +}); + +describe('addPermissions', () => { + + describe('params', () => { + const params = schema.addPermissions.params.describe(); + + describe('objectId', () => { + const objectId = params.keys.objectId; + + it('is the expected schema', () => { + expect(objectId).toEqual(type.uuidv4.describe()); + }); + }); + }); + + describe('body', () => { + const body = schema.addPermissions.body.describe(); + + it('is an array', () => { + expect(body).toBeTruthy(); + expect(body.type).toEqual('array'); + expect(Array.isArray(body.items)).toBeTruthy(); + }); + + it('is required', () => { + expect(body.flags).toBeTruthy(); + expect(body.flags).toEqual(expect.objectContaining({ presence: 'required' })); + }); + + it('should contain userId', () => { + expect(body.items).toEqual(expect.arrayContaining([ + expect.objectContaining({ + keys: expect.objectContaining({ + userId: expect.objectContaining({ + type: 'string', + flags: expect.objectContaining({ presence: 'required' }), + rules: expect.arrayContaining([ + expect.objectContaining({ + name: 'guid', + args: { + options: { version: 'uuidv4' } + } + }) + ]) + }) + }) + }) + ])); + }); + + it('should contain a valid permCode', () => { + expect(body.items).toEqual(expect.arrayContaining([ + expect.objectContaining({ + keys: expect.objectContaining({ + permCode: expect.objectContaining({ + type: 'string', + flags: expect.objectContaining({ presence: 'required' }), + allow: expect.arrayContaining(Object.values(Permissions)) + }) + }) + }) + ])); + }); + }); +}); + +describe('removePermissions', () => { + + describe('params', () => { + const params = schema.removePermissions.params.describe(); + + describe('objectId', () => { + const objectId = params.keys.objectId; + + it('is the expected schema', () => { + expect(objectId).toEqual(type.uuidv4.describe()); + }); + }); + }); + + describe('query', () => { + const query = schema.listPermissions.query.describe(); + + describe('userId', () => { + const userId = query.keys.userId; + + it('is the expected schema', () => { + expect(userId).toEqual(scheme.guid.describe()); + }); + }); + + describe('permCode', () => { + const permCode = query.keys.permCode; + + it('is the expected schema', () => { + expect(permCode).toEqual(scheme.permCode.describe()); + }); + }); + }); +}); diff --git a/comsapi/app/tests/unit/validators/tag.spec.js b/comsapi/app/tests/unit/validators/tag.spec.js new file mode 100644 index 00000000..9827fdc2 --- /dev/null +++ b/comsapi/app/tests/unit/validators/tag.spec.js @@ -0,0 +1,21 @@ +const jestJoi = require('jest-joi'); +expect.extend(jestJoi.matchers); + +const { schema } = require('../../../src/validators/tag'); +const { type } = require('../../../src/validators/common'); + + +describe('searchTags', () => { + describe('query', () => { + const query = schema.searchTags.query.describe(); + + + describe('tagset', () => { + const tagset = query.keys.tagset; + + it('is the expected schema', () => { + expect(tagset).toEqual(type.tagset().describe()); + }); + }); + }); +}); diff --git a/comsapi/app/tests/unit/validators/user.spec.js b/comsapi/app/tests/unit/validators/user.spec.js new file mode 100644 index 00000000..0172460e --- /dev/null +++ b/comsapi/app/tests/unit/validators/user.spec.js @@ -0,0 +1,187 @@ +const crypto = require('crypto'); +const jestJoi = require('jest-joi'); +expect.extend(jestJoi.matchers); + +const { schema } = require('../../../src/validators/user'); +const { scheme, type } = require('../../../src/validators/common'); + + +describe('listIdps', () => { + + describe('query', () => { + const query = schema.listIdps.query.describe(); + + describe('active', () => { + const active = query.keys.active; + + it('is a boolean', () => { + expect(active).toBeTruthy(); + expect(active.type).toEqual('boolean'); + }); + + it('contains truthy array', () => { + expect(Array.isArray(active.truthy)).toBeTruthy(); + expect(active.truthy).toHaveLength(12); + }); + + it.each([ + true, 1, 'true', 'TRUE', 't', 'T', 'yes', 'yEs', 'y', 'Y', '1', + false, 0, 'false', 'FALSE', 'f', 'F', 'no', 'nO', 'n', 'N', '0' + ])('accepts the schema given %j', (value) => { + const req = { + query: { + active: value + } + }; + + expect(req).toMatchSchema(schema.listIdps); + }); + }); + }); +}); + +describe('searchUsers', () => { + + describe('query', () => { + const query = schema.searchUsers.query.describe(); + const longStr = crypto.randomBytes(256).toString('hex'); + + it('requires at least 1 parameter', () => { + expect(query.rules).toEqual(expect.arrayContaining([ + expect.objectContaining({ + name: 'min', + args: { + limit: 1 + } + }) + ])); + }); + + describe('active', () => { + const active = query.keys.active; + + it('is the expected schema', () => { + expect(active).toEqual(type.truthy.describe()); + }); + }); + + describe('email', () => { + it('is the expected schema', () => { + expect(query.keys.email).toEqual(type.email.describe()); + }); + }); + + describe('firstName', () => { + it('is the expected schema', () => { + expect(query.keys.firstName).toEqual(type.alphanum.describe()); + }); + }); + + describe('fullName', () => { + const fullName = query.keys.fullName; + + it('is a string', () => { + expect(fullName).toBeTruthy(); + expect(fullName.type).toEqual('string'); + }); + + it('is a regex', () => { + expect(Array.isArray(fullName.rules)).toBeTruthy(); + expect(fullName.rules).toHaveLength(2); + expect(fullName.rules).toEqual(expect.arrayContaining([ + expect.objectContaining({ + 'args': { + 'regex': '/^[\\w\\-\\s]+$/' + }, + 'name': 'pattern' + }) + ])); + }); + + it('has a max length of 255', () => { + expect(Array.isArray(fullName.rules)).toBeTruthy(); + expect(fullName.rules).toHaveLength(2); + expect(fullName.rules).toEqual(expect.arrayContaining([ + expect.objectContaining( + { + 'args': { + 'limit': 255 + }, + 'name': 'max' + }), + ])); + }); + + it('matches the schema', () => { + const req = { + query: { + fullName: 'Bob Smith' + } + }; + + expect(req).toMatchSchema(schema.searchUsers); + }); + + it('must be less than or equal to 255 characters long', () => { + const req = { + query: { + fullName: longStr + } + }; + + expect(req).not.toMatchSchema(schema.searchUsers); + }); + }); + + describe('identityId', () => { + it('is the expected schema', () => { + expect(query.keys.identityId).toEqual(scheme.string.describe()); + }); + }); + + describe('idp', () => { + it('is the expected schema', () => { + expect(query.keys.idp).toEqual(scheme.string.describe()); + }); + }); + + describe('lastName', () => { + it('is the expected schema', () => { + expect(query.keys.lastName).toEqual(type.alphanum.describe()); + }); + }); + + describe('search', () => { + const search = query.keys.search; + + it('is a string', () => { + expect(search).toBeTruthy(); + expect(search.type).toEqual('string'); + }); + + it('matches the schema', () => { + const req = { + query: { + search: 'someMatcher' + } + }; + + expect(req).toMatchSchema(schema.searchUsers); + }); + }); + + describe('userId', () => { + const userId = query.keys.userId; + + it('is the expected schema', () => { + expect(userId).toEqual(scheme.guid.describe()); + }); + }); + + describe('username', () => { + it('is the expected schema', () => { + expect(query.keys.username).toEqual(type.alphanum.describe()); + }); + }); + }); +}); diff --git a/comsapi/app/tests/unit/validators/version.spec.js b/comsapi/app/tests/unit/validators/version.spec.js new file mode 100644 index 00000000..f018a383 --- /dev/null +++ b/comsapi/app/tests/unit/validators/version.spec.js @@ -0,0 +1,57 @@ +const jestJoi = require('jest-joi'); +expect.extend(jestJoi.matchers); + +const { schema } = require('../../../src/validators/version'); +const { scheme, type } = require('../../../src/validators/common'); + +jest.mock('config'); + +beforeEach(() => { + jest.resetAllMocks(); +}); + +describe('fetchMetadata', () => { + describe('headers', () => { + const headers = schema.fetchMetadata.headers.describe(); + + it('is the expected schema', () => { + expect(headers).toEqual(type.metadata().describe()); + }); + }); + + describe('query', () => { + const query = schema.fetchMetadata.query.describe(); + + describe('s3VersionId', () => { + const s3VersionId = query.keys.s3VersionId; + + it('is the expected schema', () => { + expect(s3VersionId).toEqual(scheme.string.describe()); + }); + }); + }); +}); + + +describe('fetchTags', () => { + + describe('query', () => { + + const query = schema.fetchTags.query.describe(); + + describe('s3VersionId', () => { + const s3VersionId = query.keys.s3VersionId; + + it('is the expected schema', () => { + expect(s3VersionId).toEqual(scheme.string.describe()); + }); + }); + + const tagset = query.keys.tagset; + + it('is the expected schema', () => { + expect(tagset).toEqual(type.tagset().describe()); + }); + + }); +}); diff --git a/comsapi/bcgovpubcode.yml b/comsapi/bcgovpubcode.yml new file mode 100644 index 00000000..cb5dcad2 --- /dev/null +++ b/comsapi/bcgovpubcode.yml @@ -0,0 +1,50 @@ +--- +data_management_roles: + data_custodian: Fraser Marshall + product_owner: Sharolyn Hurley +product_external_dependencies: {} +product_information: + api_specifications: + - >- + https://github.com/bcgov/common-object-management-service/blob/master/app/README.md#openapi-specification + - https://coms.api.gov.bc.ca/api/v1/docs + business_capabilities_standard: + - Manage Object Storage Objects (CRUD) + - Manage Object Storage Objects (Search/Filter) + - Manage Object Storage Objects (Permission Management) + - Manage Object Storage Objects (Share publicly) + ministry: + - Water, Land and Resource Stewardship + product_acronym: COMS + product_description: >- + A microservice for managing access control, metadata and tags to S3 Objects + in object storage + product_name: Common Object Management Service + product_status: maturing + product_urls: + - https://coms.api.gov.bc.ca + - https://bcgov.github.io/common-service-showcase/services/coms.html + program_area: >- + Natural Resource Information and Digital Services - Development and Digital + Services +product_technology_information: + backend_frameworks: + - name: Express + version: 4.18.2 + - name: Other + version: Knex + - name: Other + version: Objection + backend_languages_version: + - name: JavaScript + version: ecmaVersion 9 / es2018 + ci_cd_tools: + - GitHub-Actions + - Helm + data_storage_platforms: + - Postgresql + - Object-Storage + frontend_languages: [] + hosting_platforms: + - Private-Cloud-Openshift +version: 1 diff --git a/comsapi/charts/coms/.helmignore b/comsapi/charts/coms/.helmignore new file mode 100644 index 00000000..0e8a0eb3 --- /dev/null +++ b/comsapi/charts/coms/.helmignore @@ -0,0 +1,23 @@ +# Patterns to ignore when building packages. +# This supports shell glob matching, relative path matching, and +# negation (prefixed with !). Only one pattern per line. +.DS_Store +# Common VCS dirs +.git/ +.gitignore +.bzr/ +.bzrignore +.hg/ +.hgignore +.svn/ +# Common backup files +*.swp +*.bak +*.tmp +*.orig +*~ +# Various IDEs +.project +.idea/ +*.tmproj +.vscode/ diff --git a/comsapi/charts/coms/Chart.yaml b/comsapi/charts/coms/Chart.yaml new file mode 100644 index 00000000..c31fe71a --- /dev/null +++ b/comsapi/charts/coms/Chart.yaml @@ -0,0 +1,48 @@ +apiVersion: v2 +name: common-object-management-service +# This is the chart version. This version number should be incremented each time you make changes +# to the chart and its templates, including the app version. +# Versions are expected to follow Semantic Versioning (https://semver.org/) +version: 0.0.20 +kubeVersion: ">= 1.13.0" +description: A microservice for managing access control to S3 Objects +# A chart can be either an 'application' or a 'library' chart. +# Application charts are a collection of templates that can be packaged into versioned archives +# to be deployed. +# Library charts provide useful utilities or functions for the chart developer. They're included as +# a dependency of application charts to inject those utilities and functions into the rendering +# pipeline. Library charts do not define any templates and therefore cannot be deployed. +type: application +keywords: + - nodejs + - javascript + - docker + - microservice + - s3 + - document-management + - access-control + - object-storage + - domo +home: https://bcgov.github.io/common-object-management-service +sources: + - https://github.com/bcgov/common-object-management-service +dependencies: + - name: patroni + version: ~0.0.4 + repository: https://bcgov.github.io/nr-patroni-chart + condition: patroni.enabled + tags: + - patroni + # by default, the object created will be named <your-app>-patroni. You can use an alias to override the -patroni suffix + # alias: postgres +maintainers: + - name: NR Common Service Showcase Team + email: NR.CommonServiceShowcase@gov.bc.ca + url: https://bcgov.github.io/common-service-showcase/team.html +# This is the version number of the application being deployed. This version number should be +# incremented each time you make changes to the application. Versions are not expected to +# follow Semantic Versioning. They should reflect the version the application is using. +# It is recommended to use it with quotes. +appVersion: "0.7.0" +deprecated: false +annotations: {} diff --git a/comsapi/charts/coms/README.md b/comsapi/charts/coms/README.md new file mode 100644 index 00000000..dc4ef5df --- /dev/null +++ b/comsapi/charts/coms/README.md @@ -0,0 +1,81 @@ +# common-object-management-service + +![Version: 0.0.20](https://img.shields.io/badge/Version-0.0.20-informational?style=flat-square) ![Type: application](https://img.shields.io/badge/Type-application-informational?style=flat-square) ![AppVersion: 0.7.0](https://img.shields.io/badge/AppVersion-0.7.0-informational?style=flat-square) + +A microservice for managing access control to S3 Objects + +**Homepage:** <https://bcgov.github.io/common-object-management-service> + +## Maintainers + +| Name | Email | Url | +| ---- | ------ | --- | +| NR Common Service Showcase Team | <NR.CommonServiceShowcase@gov.bc.ca> | <https://bcgov.github.io/common-service-showcase/team.html> | + +## Source Code + +* <https://github.com/bcgov/common-object-management-service> + +## Requirements + +Kubernetes: `>= 1.13.0` + +| Repository | Name | Version | +|------------|------|---------| +| https://bcgov.github.io/nr-patroni-chart | patroni | ~0.0.4 | + +## Values + +| Key | Type | Default | Description | +|-----|------|---------|-------------| +| autoscaling.behavior | object | `{"scaleDown":{"policies":[{"periodSeconds":120,"type":"Pods","value":1}],"selectPolicy":"Max","stabilizationWindowSeconds":120},"scaleUp":{"policies":[{"periodSeconds":30,"type":"Pods","value":2}],"selectPolicy":"Max","stabilizationWindowSeconds":0}}` | behavior configures the scaling behavior of the target in both Up and Down directions (scaleUp and scaleDown fields respectively). | +| autoscaling.enabled | bool | `false` | Specifies whether the Horizontal Pod Autoscaler should be created | +| autoscaling.maxReplicas | int | `16` | | +| autoscaling.minReplicas | int | `2` | | +| autoscaling.targetCPUUtilizationPercentage | int | `80` | | +| basicAuthSecretOverride.password | string | `nil` | | +| basicAuthSecretOverride.username | string | `nil` | | +| config.configMap | object | `{"DB_PORT":"5432","KC_IDENTITYKEY":null,"KC_PUBLICKEY":null,"KC_REALM":null,"KC_SERVERURL":null,"OBJECTSTORAGE_BUCKET":null,"OBJECTSTORAGE_ENDPOINT":null,"OBJECTSTORAGE_KEY":null,"SERVER_LOGLEVEL":"http","SERVER_PORT":"3000","SERVER_TEMP_EXPIRESIN":"300"}` | These values will be wholesale added to the configmap as is; refer to the coms documentation for what each of these values mean and whether you need them defined. Ensure that all values are represented explicitly as strings, as non-string values will not translate over as expected into container environment variables. For configuration keys named `*_ENABLED`, either leave them commented/undefined, or set them to string value "true". | +| config.enabled | bool | `false` | | +| config.releaseScoped | bool | `false` | This should be set to true if and only if you require configmaps and secrets to be release scoped. In the event you want all instances in the same namespace to share a similar configuration, this should be set to false | +| dbSecretOverride.password | string | `nil` | | +| dbSecretOverride.username | string | `nil` | | +| failurePolicy | string | `"Retry"` | | +| features.basicAuth | bool | `false` | Specifies whether basic auth is enabled | +| features.defaultBucket | bool | `false` | Specifies whether a default bucket is enabled | +| features.oidcAuth | bool | `false` | Specifies whether oidc auth is enabled | +| fullnameOverride | string | `nil` | String to fully override fullname | +| image.pullPolicy | string | `"IfNotPresent"` | | +| image.repository | string | `"docker.io/bcgovimages"` | | +| image.tag | string | `nil` | | +| imagePullSecrets | list | `[]` | Specify docker-registry secret names as an array | +| keycloakSecretOverride.password | string | `nil` | | +| keycloakSecretOverride.username | string | `nil` | | +| nameOverride | string | `nil` | String to partially override fullname | +| networkPolicy.enabled | bool | `true` | Specifies whether a network policy should be created | +| objectStorageSecretOverride.password | string | `nil` | | +| objectStorageSecretOverride.username | string | `nil` | | +| patroni.enabled | bool | `false` | | +| podAnnotations | object | `{}` | Annotations for coms pods | +| podSecurityContext | object | `{}` | | +| replicaCount | int | `2` | | +| resources.limits.cpu | string | `"200m"` | | +| resources.limits.memory | string | `"512Mi"` | | +| resources.requests.cpu | string | `"50m"` | | +| resources.requests.memory | string | `"128Mi"` | | +| route.annotations | object | `{}` | Annotations to add to the route | +| route.enabled | bool | `true` | Specifies whether a route should be created | +| route.host | string | `"chart-example.local"` | | +| route.tls.insecureEdgeTerminationPolicy | string | `"Redirect"` | | +| route.tls.termination | string | `"edge"` | | +| route.wildcardPolicy | string | `"None"` | | +| securityContext | object | `{}` | | +| service.port | int | `3000` | | +| service.portName | string | `"http"` | | +| service.type | string | `"ClusterIP"` | | +| serviceAccount.annotations | object | `{}` | Annotations to add to the service account | +| serviceAccount.enabled | bool | `false` | Specifies whether a service account should be created | +| serviceAccount.name | string | `nil` | The name of the service account to use. If not set and create is true, a name is generated using the fullname template | + +---------------------------------------------- +Autogenerated from chart metadata using [helm-docs v1.11.0](https://github.com/norwoodj/helm-docs/releases/v1.11.0) diff --git a/comsapi/charts/coms/templates/NOTES.txt b/comsapi/charts/coms/templates/NOTES.txt new file mode 100644 index 00000000..a5b85f30 --- /dev/null +++ b/comsapi/charts/coms/templates/NOTES.txt @@ -0,0 +1,24 @@ +{{- $configMapName := printf "%s-%s" (include "coms.configname" .) "config" }} +{{- $configMap := (lookup "v1" "ConfigMap" .Release.Namespace $configMapName ) }} +Get the application URL by running these commands: +{{- if .Values.route.enabled }} + http{{ if $.Values.route.tls }}s{{ end }}://{{ .Values.route.host }}{{ .Values.route.path }} +{{- else if contains "NodePort" .Values.service.type }} + export NODE_PORT=$(kubectl get --namespace {{ .Release.Namespace }} -o jsonpath="{.spec.ports[0].nodePort}" services {{ include "coms.fullname" . }}) + export NODE_IP=$(kubectl get nodes --namespace {{ .Release.Namespace }} -o jsonpath="{.items[0].status.addresses[0].address}") + echo http://$NODE_IP:$NODE_PORT +{{- else if contains "LoadBalancer" .Values.service.type }} + NOTE: It may take a few minutes for the LoadBalancer IP to be available. + You can watch the status of by running 'kubectl get --namespace {{ .Release.Namespace }} svc -w {{ include "coms.fullname" . }}' + export SERVICE_IP=$(kubectl get svc --namespace {{ .Release.Namespace }} {{ include "coms.fullname" . }} --template "{{"{{ range (index .status.loadBalancer.ingress 0) }}{{.}}{{ end }}"}}") + echo http://$SERVICE_IP:{{ .Values.service.port }} +{{- else if contains "ClusterIP" .Values.service.type }} + export POD_NAME=$(kubectl get pods --namespace {{ .Release.Namespace }} -l "app.kubernetes.io/name={{ include "coms.name" . }},app.kubernetes.io/instance={{ .Release.Name }}" -o jsonpath="{.items[0].metadata.name}") + export CONTAINER_PORT=$(kubectl get pod --namespace {{ .Release.Namespace }} $POD_NAME -o jsonpath="{.spec.containers[0].ports[0].containerPort}") + echo "Visit http://127.0.0.1:8080 to use your application" + kubectl --namespace {{ .Release.Namespace }} port-forward $POD_NAME 8080:$CONTAINER_PORT +{{- end }} +{{- if not $configMap }} + +Make sure that ConfigMap "{{ $configMapName }}" is defined in the namespace; the deployment will fail to run without it! +{{- end }} diff --git a/comsapi/charts/coms/templates/_helpers.tpl b/comsapi/charts/coms/templates/_helpers.tpl new file mode 100644 index 00000000..743d6727 --- /dev/null +++ b/comsapi/charts/coms/templates/_helpers.tpl @@ -0,0 +1,77 @@ +{{/* +Expand the name of the chart. +*/}} +{{- define "coms.name" -}} +{{- default .Chart.Name .Values.nameOverride | trunc 63 | trimSuffix "-" }} +{{- end }} + +{{/* +Create a default fully qualified app name. +We truncate at 63 chars because some Kubernetes name fields are limited to this (by the DNS naming spec). +If release name contains chart name it will be used as a full name. +*/}} +{{- define "coms.fullname" -}} +{{- if .Values.fullnameOverride }} +{{- .Values.fullnameOverride | trunc 63 | trimSuffix "-" }} +{{- else }} +{{- $name := default .Chart.Name .Values.nameOverride }} +{{- if contains $name .Release.Name }} +{{- .Release.Name | trunc 63 | trimSuffix "-" }} +{{- else }} +{{- printf "%s-%s" $name .Release.Name | trunc 63 | trimSuffix "-" }} +{{- end }} +{{- end }} +{{- end }} + +{{/* +Define the config pattern of the chart based on options. +*/}} +{{- define "coms.configname" -}} +{{- if .Values.config.releaseScoped }} +{{- include "coms.fullname" . }} +{{- else }} +{{- include "coms.name" . }} +{{- end }} +{{- end }} + +{{/* +Create chart name and version as used by the chart label. +*/}} +{{- define "coms.chart" -}} +{{- printf "%s-%s" .Chart.Name .Chart.Version | replace "+" "_" | trunc 63 | trimSuffix "-" }} +{{- end }} + +{{/* +Common labels +*/}} +{{- define "coms.labels" -}} +helm.sh/chart: {{ include "coms.chart" . }} +app: {{ include "coms.fullname" . }} +{{ include "coms.selectorLabels" . }} +{{- if .Chart.AppVersion }} +app.kubernetes.io/version: {{ .Chart.AppVersion | quote }} +{{- end }} +app.kubernetes.io/component: backend +app.kubernetes.io/managed-by: {{ .Release.Service }} +app.kubernetes.io/part-of: {{ .Release.Name }} +app.openshift.io/runtime: nodejs +{{- end }} + +{{/* +Selector labels +*/}} +{{- define "coms.selectorLabels" -}} +app.kubernetes.io/name: {{ include "coms.name" . }} +app.kubernetes.io/instance: {{ .Release.Name }} +{{- end }} + +{{/* +Create the name of the service account to use +*/}} +{{- define "coms.serviceAccountName" -}} +{{- if .Values.serviceAccount.create }} +{{- default (include "coms.fullname" .) .Values.serviceAccount.name }} +{{- else }} +{{- default "default" .Values.serviceAccount.name }} +{{- end }} +{{- end }} diff --git a/comsapi/charts/coms/templates/configmap.yaml b/comsapi/charts/coms/templates/configmap.yaml new file mode 100644 index 00000000..4219995f --- /dev/null +++ b/comsapi/charts/coms/templates/configmap.yaml @@ -0,0 +1,12 @@ +{{- if .Values.config.enabled }} +--- +apiVersion: v1 +kind: ConfigMap +metadata: + name: {{ include "coms.configname" . }}-config + {{- if not .Values.config.releaseScoped }} + annotations: + "helm.sh/resource-policy": keep + {{- end }} +data: {{ toYaml .Values.config.configMap | nindent 2 }} +{{- end }} diff --git a/comsapi/charts/coms/templates/deploymentconfig.yaml b/comsapi/charts/coms/templates/deploymentconfig.yaml new file mode 100644 index 00000000..09dd8a34 --- /dev/null +++ b/comsapi/charts/coms/templates/deploymentconfig.yaml @@ -0,0 +1,175 @@ +{{ $dbHostName := .Values.config.configMap.DB_HOST }} +{{ $dbSecretName := printf "%s-%s" (include "coms.fullname" .) "passphrase" }} +{{- if .Values.patroni.enabled }} +{{ $dbHostName = include "patroni.fullname" .Subcharts.patroni }} +{{ $dbSecretName = include "patroni.fullname" .Subcharts.patroni }} +{{- end }} + +{{- define "coms.connectsTo" -}} +apiVersion: apps/v1 +kind: StatefulSet +name: {{ include "patroni.fullname" .Subcharts.patroni }} +{{- end }} +--- +apiVersion: apps.openshift.io/v1 +kind: DeploymentConfig +metadata: + name: {{ include "coms.fullname" . }} + labels: + {{- include "coms.labels" . | nindent 4 }} + {{- if .Values.patroni.enabled }} + annotations: + app.openshift.io/connects-to: '[{{ include "coms.connectsTo" . | fromYaml | toJson }}]' + {{- end }} +spec: + replicas: {{ .Values.replicaCount }} + revisionHistoryLimit: 10 + selector: + {{- include "coms.selectorLabels" . | nindent 4 }} + strategy: + resources: + {{- toYaml .Values.resources | nindent 6 }} + rollingParams: + timeoutSeconds: 600 + {{- if or .Values.patroni.enabled .Values.config.configMap.DB_ENABLED }} + pre: + failurePolicy: {{ .Values.failurePolicy }} + execNewPod: + command: + - npm + - run + - migrate + containerName: app + env: + - name: NODE_ENV + value: production + - name: DB_DATABASE + valueFrom: + secretKeyRef: + key: app-db-name + name: {{ $dbSecretName }} + - name: DB_HOST + value: {{ $dbHostName }} + - name: DB_USERNAME + valueFrom: + secretKeyRef: + key: app-db-username + name: {{ $dbSecretName }} + - name: DB_PASSWORD + valueFrom: + secretKeyRef: + key: app-db-password + name: {{ $dbSecretName }} + {{- end }} + type: Rolling + template: + metadata: + labels: {{ include "coms.selectorLabels" . | nindent 8 }} + spec: + {{- with .Values.imagePullSecrets }} + imagePullSecrets: {{ toYaml . | nindent 8 }} + {{- end }} + {{- if .Values.serviceAccount.create }} + serviceAccountName: {{ include "coms.serviceAccountName" . }} + {{- end }} + {{- with .Values.podSecurityContext }} + securityContext: {{ toYaml . | nindent 8 }} + {{- end }} + containers: + - name: app + {{- with .Values.securityContext }} + securityContext: {{ toYaml . | nindent 12 }} + {{- end }} + image: "{{ .Values.image.repository }}/{{ .Chart.Name }}:{{ .Values.image.tag | default .Chart.AppVersion }}" + imagePullPolicy: {{ .Values.image.pullPolicy }} + ports: + - containerPort: {{ .Values.service.port }} + protocol: TCP + livenessProbe: + failureThreshold: 3 + httpGet: + path: {{ .Values.route.path }} + port: {{ .Values.service.port }} + scheme: HTTP + initialDelaySeconds: 10 + timeoutSeconds: 1 + readinessProbe: + failureThreshold: 3 + httpGet: + path: {{ .Values.route.path }} + port: {{ .Values.service.port }} + scheme: HTTP + initialDelaySeconds: 10 + timeoutSeconds: 1 + resources: {{ toYaml .Values.resources | nindent 12 }} + env: + - name: NODE_ENV + value: production + {{- if or .Values.features.basicAuth .Values.config.configMap.BASICAUTH_ENABLED }} + - name: BASICAUTH_USERNAME + valueFrom: + secretKeyRef: + key: username + name: {{ include "coms.fullname" . }}-basicauth + - name: BASICAUTH_PASSWORD + valueFrom: + secretKeyRef: + key: password + name: {{ include "coms.fullname" . }}-basicauth + {{- end }} + {{- if or .Values.patroni.enabled .Values.config.configMap.DB_ENABLED }} + - name: DB_DATABASE + valueFrom: + secretKeyRef: + key: app-db-name + name: {{ $dbSecretName }} + - name: DB_HOST + value: {{ $dbHostName }} + - name: DB_USERNAME + valueFrom: + secretKeyRef: + key: app-db-username + name: {{ $dbSecretName }} + - name: DB_PASSWORD + valueFrom: + secretKeyRef: + key: app-db-password + name: {{ $dbSecretName }} + {{- end }} + {{- if or .Values.features.oidcAuth .Values.config.configMap.KC_ENABLED }} + - name: KC_CLIENTID + valueFrom: + secretKeyRef: + key: username + name: {{ include "coms.configname" . }}-keycloak + - name: KC_CLIENTSECRET + valueFrom: + secretKeyRef: + key: password + name: {{ include "coms.configname" . }}-keycloak + {{- end }} + {{- if or .Values.features.defaultBucket .Values.config.configMap.OBJECTSTORAGE_ENABLED }} + - name: OBJECTSTORAGE_ACCESSKEYID + valueFrom: + secretKeyRef: + key: username + name: {{ include "coms.configname" . }}-objectstorage + - name: OBJECTSTORAGE_SECRETACCESSKEY + valueFrom: + secretKeyRef: + key: password + name: {{ include "coms.configname" . }}-objectstorage + {{- end }} + - name: SERVER_PASSPHRASE + valueFrom: + secretKeyRef: + key: password + name: {{ include "coms.fullname" . }}-passphrase + envFrom: + - configMapRef: + name: {{ include "coms.configname" . }}-config + restartPolicy: Always + terminationGracePeriodSeconds: 30 + test: false + triggers: + - type: ConfigChange diff --git a/comsapi/charts/coms/templates/hpa.yaml b/comsapi/charts/coms/templates/hpa.yaml new file mode 100644 index 00000000..e4b7c545 --- /dev/null +++ b/comsapi/charts/coms/templates/hpa.yaml @@ -0,0 +1,37 @@ +{{- if .Values.autoscaling.enabled }} +--- +apiVersion: autoscaling/v2 +kind: HorizontalPodAutoscaler +metadata: + name: {{ include "coms.fullname" . }} + labels: + {{- include "coms.labels" . | nindent 4 }} +spec: + scaleTargetRef: + apiVersion: apps.openshift.io/v1 + kind: DeploymentConfig + name: {{ include "coms.fullname" . }} + minReplicas: {{ .Values.autoscaling.minReplicas }} + maxReplicas: {{ .Values.autoscaling.maxReplicas }} + metrics: + {{- if .Values.autoscaling.targetCPUUtilizationPercentage }} + - type: Resource + resource: + name: cpu + target: + type: Utilization + averageUtilization: {{ .Values.autoscaling.targetCPUUtilizationPercentage }} + {{- end }} + {{- if .Values.autoscaling.targetMemoryUtilizationPercentage }} + - type: Resource + resource: + name: memory + target: + type: Utilization + averageUtilization: {{ .Values.autoscaling.targetMemoryUtilizationPercentage }} + {{- end }} + {{- with .Values.autoscaling.behavior }} + behavior: + {{- toYaml . | nindent 4 }} + {{- end }} +{{- end }} diff --git a/comsapi/charts/coms/templates/networkpolicy.yaml b/comsapi/charts/coms/templates/networkpolicy.yaml new file mode 100644 index 00000000..2d619083 --- /dev/null +++ b/comsapi/charts/coms/templates/networkpolicy.yaml @@ -0,0 +1,57 @@ +{{- if .Values.networkPolicy.enabled }} +--- +apiVersion: networking.k8s.io/v1 +kind: NetworkPolicy +metadata: + name: allow-openshift-ingress-to-{{ include "coms.fullname" . }}-app + labels: + {{- include "coms.labels" . | nindent 4 }} +spec: + ingress: + - from: + - namespaceSelector: + matchLabels: + network.openshift.io/policy-group: ingress + - podSelector: + matchLabels: {{ include "coms.selectorLabels" . | nindent 14 }} + ports: + - port: {{ default "8080" .Values.config.configMap.SERVER_PORT | atoi }} + protocol: TCP + podSelector: + matchLabels: {{- include "coms.selectorLabels" . | nindent 6 }} +{{- if .Values.patroni.enabled }} +--- +apiVersion: networking.k8s.io/v1 +kind: NetworkPolicy +metadata: + name: allow-{{ include "coms.fullname" . }}-app-to-{{ include "patroni.fullname" .Subcharts.patroni }}-cluster + labels: {{ include "patroni.labels" . | nindent 4 }} +spec: + ingress: + - from: + - podSelector: + matchLabels: {{ include "coms.selectorLabels" . | nindent 14 }} + ports: + - port: {{ default "5432" .Values.config.configMap.DB_PORT | atoi }} + protocol: TCP + podSelector: + matchLabels: {{ include "patroni.selectorLabels" .Subcharts.patroni | nindent 6 }} +--- +apiVersion: networking.k8s.io/v1 +kind: NetworkPolicy +metadata: + name: allow-pre-hook-to-{{ include "patroni.fullname" .Subcharts.patroni }}-cluster + labels: {{ include "patroni.labels" . | nindent 4 }} +spec: + ingress: + - from: + - podSelector: + matchLabels: + openshift.io/deployer-pod.type: hook-pre + ports: + - port: {{ default "5432" .Values.config.configMap.DB_PORT | atoi }} + protocol: TCP + podSelector: + matchLabels: {{ include "patroni.selectorLabels" .Subcharts.patroni | nindent 6 }} +{{- end }} +{{- end }} diff --git a/comsapi/charts/coms/templates/route.yaml b/comsapi/charts/coms/templates/route.yaml new file mode 100644 index 00000000..418e70ad --- /dev/null +++ b/comsapi/charts/coms/templates/route.yaml @@ -0,0 +1,28 @@ +{{- if .Values.route.enabled -}} +--- +apiVersion: route.openshift.io/v1 +kind: Route +metadata: + name: {{ include "coms.fullname" . }} + labels: + {{- include "coms.labels" . | nindent 4 }} + {{- with .Values.route.annotations }} + annotations: + {{- toYaml . | nindent 4 }} + {{- end }} +spec: + host: {{ .Values.route.host | quote }} + {{- if .Values.route.path }} + path: {{ .Values.route.path }} + {{- end }} + port: + targetPort: {{ .Values.service.portName }} + tls: + insecureEdgeTerminationPolicy: {{ .Values.route.tls.insecureEdgeTerminationPolicy }} + termination: {{ .Values.route.tls.termination }} + to: + kind: Service + name: {{ include "coms.fullname" . }} + weight: 100 + wildcardPolicy: {{ .Values.route.wildcardPolicy }} +{{- end }} diff --git a/comsapi/charts/coms/templates/secret.yaml b/comsapi/charts/coms/templates/secret.yaml new file mode 100644 index 00000000..d12245f6 --- /dev/null +++ b/comsapi/charts/coms/templates/secret.yaml @@ -0,0 +1,80 @@ +{{- $baPassword := (randAlphaNum 32) }} +{{- $baUsername := (randAlphaNum 32) }} +{{- $dbPassword := (randAlphaNum 32) }} +{{- $dbUsername := (randAlphaNum 32) }} + +{{- $baSecretName := printf "%s-%s" (include "coms.fullname" .) "basicauth" }} +{{- $baSecret := (lookup "v1" "Secret" .Release.Namespace $baSecretName ) }} +{{- $dbSecretName := printf "%s-%s" (include "coms.fullname" .) "passphrase" }} +{{- $dbSecret := (lookup "v1" "Secret" .Release.Namespace $dbSecretName ) }} +{{- $kcSecretName := printf "%s-%s" (include "coms.fullname" .) "keycloak" }} +{{- $kcSecret := (lookup "v1" "Secret" .Release.Namespace $kcSecretName ) }} +{{- $osSecretName := printf "%s-%s" (include "coms.fullname" .) "objectstorage" }} +{{- $osSecret := (lookup "v1" "Secret" .Release.Namespace $osSecretName ) }} + +{{- if not $baSecret }} +--- +apiVersion: v1 +kind: Secret +metadata: + {{- if not .Values.config.releaseScoped }} + annotations: + "helm.sh/resource-policy": keep + {{- end }} + name: {{ $baSecretName }} + labels: {{ include "coms.labels" . | nindent 4 }} +type: kubernetes.io/basic-auth +data: + password: {{ .Values.basicAuthSecretOverride.password | default $baPassword | b64enc | quote }} + username: {{ .Values.basicAuthSecretOverride.username | default $baUsername | b64enc | quote }} +{{- end }} +{{- if not $dbSecret }} +--- +apiVersion: v1 +kind: Secret +metadata: + {{- if not .Values.config.releaseScoped }} + annotations: + "helm.sh/resource-policy": keep + {{- end }} + name: {{ $dbSecretName }} + labels: {{ include "coms.labels" . | nindent 4 }} +type: Opaque +data: + password: {{ .Values.dbSecretOverride.password | default $dbPassword | b64enc | quote }} + username: {{ .Values.dbSecretOverride.username | default $dbUsername | b64enc | quote }} + app-db-password: {{ .Values.dbSecretOverride.password | default $dbPassword | b64enc | quote }} + app-db-username: {{ .Values.dbSecretOverride.username | default $dbUsername | b64enc | quote }} +{{- end }} +{{- if and (not $kcSecret) (and .Values.keycloakSecretOverride.password .Values.keycloakSecretOverride.username) }} +--- +apiVersion: v1 +kind: Secret +metadata: + {{- if not .Values.config.releaseScoped }} + annotations: + "helm.sh/resource-policy": keep + {{- end }} + name: {{ $kcSecretName }} + labels: {{ include "coms.labels" . | nindent 4 }} +type: kubernetes.io/basic-auth +data: + password: {{ .Values.keycloakSecretOverride.password | b64enc | quote }} + username: {{ .Values.keycloakSecretOverride.username | b64enc | quote }} +{{- end }} +{{- if and .Values.features.defaultBucket (not $osSecret) (and .Values.objectStorageSecretOverride.password .Values.objectStorageSecretOverride.username) }} +--- +apiVersion: v1 +kind: Secret +metadata: + {{- if not .Values.config.releaseScoped }} + annotations: + "helm.sh/resource-policy": keep + {{- end }} + name: {{ $osSecretName }} + labels: {{ include "coms.labels" . | nindent 4 }} +type: kubernetes.io/basic-auth +data: + password: {{ .Values.objectStorageSecretOverride.password | b64enc | quote }} + username: {{ .Values.objectStorageSecretOverride.username | b64enc | quote }} +{{- end }} diff --git a/comsapi/charts/coms/templates/service.yaml b/comsapi/charts/coms/templates/service.yaml new file mode 100644 index 00000000..f101646c --- /dev/null +++ b/comsapi/charts/coms/templates/service.yaml @@ -0,0 +1,16 @@ +--- +apiVersion: v1 +kind: Service +metadata: + name: {{ include "coms.fullname" . }} + labels: + {{- include "coms.labels" . | nindent 4 }} +spec: + type: {{ .Values.service.type }} + ports: + - name: {{ .Values.service.portName }} + port: {{ .Values.service.port }} + protocol: TCP + targetPort: {{ .Values.service.port }} + selector: + {{- include "coms.selectorLabels" . | nindent 4 }} diff --git a/comsapi/charts/coms/templates/serviceaccount.yaml b/comsapi/charts/coms/templates/serviceaccount.yaml new file mode 100644 index 00000000..85205c5c --- /dev/null +++ b/comsapi/charts/coms/templates/serviceaccount.yaml @@ -0,0 +1,13 @@ +{{- if .Values.serviceAccount.enabled -}} +--- +apiVersion: v1 +kind: ServiceAccount +metadata: + name: {{ include "coms.serviceAccountName" . }} + labels: + {{- include "coms.labels" . | nindent 4 }} + {{- with .Values.serviceAccount.annotations }} + annotations: + {{- toYaml . | nindent 4 }} + {{- end }} +{{- end }} diff --git a/comsapi/charts/coms/values.yaml b/comsapi/charts/coms/values.yaml new file mode 100644 index 00000000..1234a636 --- /dev/null +++ b/comsapi/charts/coms/values.yaml @@ -0,0 +1,188 @@ +# Default values for coms. +# This is a YAML-formatted file. +# Declare variables to be passed into your templates. + +replicaCount: 2 + +image: + repository: docker.io/bcgovimages + pullPolicy: IfNotPresent + # Overrides the image tag whose default is the chart appVersion. + tag: ~ + +# -- Specify docker-registry secret names as an array +imagePullSecrets: [] +# -- String to partially override fullname +nameOverride: ~ +# -- String to fully override fullname +fullnameOverride: ~ + +# DeploymentConfig pre-hook failure behavior +failurePolicy: Retry + +# -- Annotations for coms pods +podAnnotations: {} + +podSecurityContext: {} + # fsGroup: 2000 + +securityContext: {} + # capabilities: + # drop: + # - ALL + # readOnlyRootFilesystem: true + # runAsNonRoot: true + # runAsUser: 1000 + +autoscaling: + # -- Specifies whether the Horizontal Pod Autoscaler should be created + enabled: false + + # -- behavior configures the scaling behavior of the target in both Up and Down directions (scaleUp and scaleDown fields respectively). + behavior: + scaleDown: + stabilizationWindowSeconds: 120 + selectPolicy: Max + policies: + - type: Pods + value: 1 + periodSeconds: 120 + scaleUp: + stabilizationWindowSeconds: 0 + selectPolicy: Max + policies: + - type: Pods + value: 2 + periodSeconds: 30 + minReplicas: 2 + maxReplicas: 16 + targetCPUUtilizationPercentage: 80 + # targetMemoryUtilizationPercentage: 80 + +serviceAccount: + # -- Specifies whether a service account should be created + enabled: false + # -- Annotations to add to the service account + annotations: {} + # -- The name of the service account to use. + # If not set and create is true, a name is generated using the fullname template + name: ~ + +networkPolicy: + # -- Specifies whether a network policy should be created + enabled: true + +service: + type: ClusterIP + port: 3000 + portName: http + +route: + # -- Specifies whether a route should be created + enabled: true + # -- Annotations to add to the route + annotations: {} + # kubernetes.io/ingress.class: nginx + # kubernetes.io/tls-acme: "true" + host: coms-e38158-dev.apps.silver.devops.gov.bc.ca + # path: / + tls: + insecureEdgeTerminationPolicy: Redirect + termination: edge + wildcardPolicy: None + +resources: + # We usually recommend not to specify default resources and to leave this as a conscious + # choice for the user. This also increases chances charts run on environments with little + # resources, such as Minikube. If you do want to specify resources, uncomment the following + # lines, adjust them as necessary, and remove the curly braces after 'resources:'. + limits: + cpu: 200m + memory: 512Mi + requests: + cpu: 50m + memory: 128Mi + +features: + # -- Specifies whether basic auth is enabled + basicAuth: false + # -- Specifies whether a default bucket is enabled + defaultBucket: false + # -- Specifies whether oidc auth is enabled + oidcAuth: false + +config: + # Set to true if you want to let Helm manage and overwrite your configmaps. + enabled: false + + # -- This should be set to true if and only if you require configmaps and secrets to be release + # scoped. In the event you want all instances in the same namespace to share a similar + # configuration, this should be set to false + releaseScoped: false + + # -- These values will be wholesale added to the configmap as is; refer to the coms + # documentation for what each of these values mean and whether you need them defined. + # Ensure that all values are represented explicitly as strings, as non-string values will + # not translate over as expected into container environment variables. + # For configuration keys named `*_ENABLED`, either leave them commented/undefined, or set them + # to string value "true". + configMap: + # BASICAUTH_ENABLED: "true" + + # DB_HOST: ~ + DB_PORT: "5432" + # DB_POOL_MIN: "2" + # DB_POOL_MAX: "10" + + # KC_ENABLED: "true" + KC_IDENTITYKEY: "idir_user_guid,bceid_user_guid,github_id" + KC_PUBLICKEY: "MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAvoHS3+T2OCyclTh2pw6lMfoLsa1DG9QlU7qunf5+M/ualGtXqoXA7T+G0YO7rk+grKZCmxZIulRPjybOIDmaYaT/mD+IalqVvAQ91O5+4wryFr+agkpPGAI3kp3sw1aVfQfgM8xBsPoRjJaGKBhG2yEa3Oy61r/+fDZcnNrId0bDNaXXAXZ20dbywuOFzbB/ctGwBnbMLAte83kZ6ZNeyufvbq101PLkV1TOYe0QA9qZRqt+cqa2gUW5k3cu0AYBsp76SfjnVfvjFtIBGek8DY13FvHX5TkN10yQ9901l6UQWt39BD9bM4nA+KWzhuBphZoDrP44Z9cOUFPCuZrKiQIDAQAB" + KC_REALM: "forms-flow-ai" + KC_SERVERURL: "https://epd-keycloak-dev.apps.silver.devops.gov.bc.ca/auth" + + OBJECTSTORAGE_BUCKET: ~ + # OBJECTSTORAGE_ENABLED: "true" + OBJECTSTORAGE_ENDPOINT: ~ + OBJECTSTORAGE_KEY: ~ + + # SERVER_HARDRESET: "true" + # SERVER_LOGFILE: ~ + SERVER_LOGLEVEL: "http" + SERVER_PORT: "3000" + # SERVER_PRIVACY_MASK: "true" + SERVER_TEMP_EXPIRESIN: "300" + +# Modify the following variables if you need to acquire secret values from a custom-named resource +basicAuthSecretOverride: + username: ~ + password: ~ +dbSecretOverride: + username: ~ + password: ~ +keycloakSecretOverride: + username: ~ + password: ~ +objectStorageSecretOverride: + username: ~ + password: ~ + +# Patroni subchart configuration overrides +patroni: + # Controls whether to enable managing a Patroni db dependency as a part of the helm release + enabled: true + + replicaCount: 3 + resources: + limits: + cpu: 400m + memory: 512Mi + requests: + cpu: 20m + memory: 256Mi + + # Can we set default db name? + + persistentVolume: + # enabled: false + enabled: true + # Can we reduce size? diff --git a/comsapi/k6/README.md b/comsapi/k6/README.md new file mode 100644 index 00000000..49c55e36 --- /dev/null +++ b/comsapi/k6/README.md @@ -0,0 +1,14 @@ +# Load testing with K6 + +[K6](https://k6.io/docs/) is a load testing tool. +Using the K6 command line interface, you can run the scripts found in this directory to test the performance of COMS API features. + +Note: It is important to not run load tests against production environments. Always check with your server administrators before load testing in a shared server environment. + +## Prerequesites + +The simple test scripts (for example: [createObject.js](createObject.js) can be updated with actual values specific to your envionment (for example: your COMS api url, authorization token and bucket ID) or could also pass these values using parameters of the K6 command used to trigger the test. See more K6 details on how [Environment Variables](https://k6.io/docs/using-k6/environment-variables/) work. + +### Command example + +`k6 run -e BUCKET_ID=95fc01c4-c900-4fe6-b5af-b39bfd047036 -e API_PATH=http://localhost:3000/api/v1 -e AUTH_TOKEN=dXNlcjE6cGFzczE= -e FILE_PATH=./test.txt --vus=1 --iterations=10 createObject.js` diff --git a/comsapi/k6/createObject.js b/comsapi/k6/createObject.js new file mode 100644 index 00000000..4edf463c --- /dev/null +++ b/comsapi/k6/createObject.js @@ -0,0 +1,73 @@ +import http from 'k6/http'; +import { check, sleep } from 'k6'; + +// ------------------------------------------------------------------------------------------------- +// Init +// ------------------------------------------------------------------------------------------------- +// https://k6.io/docs/using-k6/environment-variables + +const apiPath = `${__ENV.API_PATH}` +const bucketId = `${__ENV.BUCKET_ID}` +const filePath = `${__ENV.FILE_PATH}` +const authToken = `${__ENV.AUTH_TOKEN}` + +'./file-in-cur-dir.txt' + +// k6 options (https://k6.io/docs/using-k6/k6-options/) +export const options = { + scenarios: { + createObject: { + executor: 'constant-arrival-rate', + rate: 5, // this is per-second, see "timeUnit" below + timeUnit: '1s', + preAllocatedVUs: 1, + maxVUs: 1, + duration: '10s', + }, + }, +}; + +// open() the file as binary (with the 'b' argument, must be declared in init scope) +// ref: https://k6.io/docs/examples/data-uploads/#multipart-request-uploading-a-file +// eslint-disable-next-line +const binFile = open(filePath, 'b'); + +// run k6 +export default function () { + + // create url with random tags + const randomLetter = () => String.fromCharCode(65 + Math.floor(Math.random() * 26)); + const url = `${apiPath}/object?bucketId=${bucketId}&tagset[${randomLetter()}]=${randomLetter()}`; + + // create a random file name + function randomFilename(length) { + let randomFilename = ''; + let counter = 0; + while (counter < 6) { + randomFilename += randomLetter(); + counter += 1; + } + return randomFilename + '.txt'; + } + + const data = { + // attach file, specify file name and content type + file: http.file(binFile, randomFilename(), 'text/plain') + }; + // make the http request + const res = http.put(url, data, { + // Add Authorization header + // note: you can hardcode an auth token here or pass it as a paramter + headers: { + 'Authorization': `Basic ${authToken}`, + 'Content-Disposition': 'attachment; filename="' + randomFilename() + '"', + 'Content-Type': 'text/plain' + } + }); + // tests + check(res, { + 'is status 200': (r) => r.status === 200, + }); + // optional delay (per VU) between iterations + sleep(1); +} diff --git a/comsapi/k6/readObject.js b/comsapi/k6/readObject.js new file mode 100644 index 00000000..f8a2f476 --- /dev/null +++ b/comsapi/k6/readObject.js @@ -0,0 +1,51 @@ +import http from 'k6/http'; +import { check, sleep } from 'k6'; + +// ------------------------------------------------------------------------------------------------- +// Init +// ------------------------------------------------------------------------------------------------- +// https://k6.io/docs/using-k6/environment-variables + +const apiPath = `${__ENV.API_PATH}` +const objectId = `${__ENV.OBJECT_ID}` +const authToken = `${__ENV.AUTH_TOKEN}` + +// k6 options (https://k6.io/docs/using-k6/k6-options/) +export const options = { + vus: 50, + scenarios: { + readObject: { + executor: 'constant-arrival-rate', + rate: 20, + duration: '20s', + preAllocatedVUs: 50, + timeUnit: '1s', + maxVUs: 100, + }, + }, +}; + +// request url +const url = `${apiPath}/object/${objectId}`; + +// Add Authorization header +// note: you can hardcode an auth token here or pass it as a paramter +const params = { + headers: { + 'Authorization': `Basic ${authToken}` + } +}; + +// run k6 +export default function () { + + // make the http request + const res = http.get(url, params); + + // tests + check(res, { + 'is status 200': (r) => r.status === 200, + }); + // optional delay (per VU) between iterations + sleep(1); +} diff --git a/comsapi/k6/searchObject.js b/comsapi/k6/searchObject.js new file mode 100644 index 00000000..09c50d07 --- /dev/null +++ b/comsapi/k6/searchObject.js @@ -0,0 +1,45 @@ +import http from 'k6/http'; +import { check, sleep } from 'k6'; + +// ------------------------------------------------------------------------------------------------- +// Init +// ------------------------------------------------------------------------------------------------- +// https://k6.io/docs/using-k6/environment-variables + +const apiPath = `${__ENV.API_PATH}` +const bucketId = `${__ENV.BUCKET_ID}` +const authToken = `${__ENV.AUTH_TOKEN}` + +// k6 options (https://k6.io/docs/using-k6/k6-options/) +export const options = { + // --- smoke testing (acceptable response times) + vus: 100, + duration: '20s', + thresholds: { + http_req_duration: ['p(99)<1500'], // 99% of requests must complete below 1.5s + }, +}; + +const randomLetter = () => String.fromCharCode(65 + Math.floor(Math.random() * 26)); +const url = `${apiPath}/object?bucketId=${bucketId}&latest=true&deleteMarker=false&tagset[${randomLetter}]=${randomLetter}`; + +// Add Authorization header +// note: you can hardcode an auth token here or pass it as a paramter +const params = { + headers: { + 'Authorization': `Basic ${authToken}` + } +}; + +export default function () { + + // make the http request + const res = http.get(url, params); + + // tests + check(res, { + 'is status 200': (r) => r.status === 200, + }); + // optional delay (per VU) between iterations + sleep(1); +}