diff --git a/.env.dev.example b/.env.dev.example index 1555b9e9..988ff4e7 100644 --- a/.env.dev.example +++ b/.env.dev.example @@ -15,7 +15,7 @@ HTTP_PROTOCOL=https:// NEXTAUTH_URL=https://localhost OAUTH_REDIRECT_URI=https://localhost BACKEND_API_BASE=http://backend:8000 -NEXT_PUBLIC_BACKEND_API_BASE=https://localhost/ph-backend +NEXT_PUBLIC_BACKEND_API_BASE=https://localhost/service NEXT_PUBLIC_NEXTAUTH_PROVIDERS=google,github,gitlab # WARNING: Replace this with a cryptographically strong random value. You can use `openssl rand -hex 32` to generate this. @@ -43,3 +43,6 @@ DATABASE_PORT=5432 DATABASE_NAME=postgres-db-name DATABASE_USER=postgres-user DATABASE_PASSWORD=postgres-password + +# Disable NextJs telemtry +NEXT_TELEMETRY_DISABLED=1 diff --git a/.env.example b/.env.example index b8ecc803..7c1c50f2 100644 --- a/.env.example +++ b/.env.example @@ -36,3 +36,7 @@ DATABASE_PORT=5432 DATABASE_NAME=postgres-db-name DATABASE_USER=postgres-user DATABASE_PASSWORD=a765b221799be364c53c8a32acccf5dd90d5fc832607bdd14fccaaaa0062adfd + + +# Disable NextJs telemtry +NEXT_TELEMETRY_DISABLED=1 diff --git a/.github/ISSUE_TEMPLATE/feature_request.md b/.github/ISSUE_TEMPLATE/feature_request.md index 932235e3..c4d992e2 100644 --- a/.github/ISSUE_TEMPLATE/feature_request.md +++ b/.github/ISSUE_TEMPLATE/feature_request.md @@ -1,34 +1,21 @@ --- -name: Bug report -about: Create a report to help us improve -title: "" -labels: bug -assignees: "" ---- - -### Describe the bug +name: Feature request +about: Suggest a feature for Phase +labels: enhancement, feature -A clear and concise description of what the bug is. +--- -### To Reproduce +## Is your feature request related to a problem? -Steps to reproduce the behavior: +*Please describe.* -1. Go to '...' -2. Click on '....' -3. Scroll down to '....' -4. See error +## Describe the solution you'd like -### Expected behavior -A clear and concise description of what you expected to happen. -### Screenshots +## Describe alternatives you've considered -If applicable, add screenshots to help explain your problem. -### Platform you are having the issue on: -### Additional context +## Additional context -Add any other context about the problem here. diff --git a/.vscode/settings.json b/.vscode/settings.json index 1a821336..765df626 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -5,7 +5,7 @@ "package-lock.json": true }, "editor.defaultFormatter": "dbaeumer.vscode-eslint", - "editor.formatOnSave": false, + "editor.formatOnSave": true, "editor.codeActionsOnSave": [ "source.addMissingImports", "source.fixAll.eslint" @@ -21,5 +21,9 @@ "editor.formatOnSave": true, "editor.defaultFormatter": "esbenp.prettier-vscode" }, - "prettier.ignorePath": ".gitignore" // Don't run prettier for files listed in .gitignore + "prettier.ignorePath": ".gitignore", + "[python]": { + "editor.defaultFormatter": "ms-python.autopep8", + "editor.formatOnSave": true + } // Don't run prettier for files listed in .gitignore } diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 80595263..825f11c2 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -35,12 +35,22 @@ Most of Phase's code is under the MIT license, though some paid feature restrict Any third party components incorporated into our code are licensed under the original license provided by the applicable component owner. ## Setup local development environment - ### Dev server with hot reload - -1. Create a `.env.dev` file with `cp .env.dev.example .env.dev` and add atleast one OAuth provider. -2. `docker-compose -f dev-docker-compose.yml up`. -3. The Console is now running at `https://localhost` with HMR. +1. Create a **.env.dev** file using + ``` + cp .env.dev.example .env.dev + ``` +2. Add atleast one OAuth provider in your **.env.dev**. Follow the [docs](https://docs.phase.dev/self-hosting/configuration/envars) +3. Verify that **dev-docker-compose.yml** file is populated with the correct environment variables by running + ``` + docker compose -f dev-docker-compose.yml --env-file .env.dev config + ``` +4. Start the containers using + ``` + docker-compose -f dev-docker-compose.yml --env-file .env.dev up + ``` +5. The Console is now running at with [HMR(Hot Module Replacement)](https://webpack.js.org/concepts/hot-module-replacement) and a self-signed certificate. + >**Note : Your browser might warn you about the self-signed certificate. You can safely accept the certificate and proceed. ### Staging env to test production builds diff --git a/README.md b/README.md index 47e0c514..9056da39 100644 --- a/README.md +++ b/README.md @@ -3,7 +3,7 @@ Phase -

Open Source, end-to-end encrypted key management platform for developers to encrypt data in their apps.

+

Open Source, end-to-end encrypted, self-hostable all in one platform for developers to manage secrets and environment variables. From their laptop 💻 to the cloud ☁️.

Website | @@ -13,45 +13,75 @@

+ +
+ -Phase Console +## Console + +Phase Console
- Phase Console + Phase Console         - Phase Console + Phase Console
- -
-[Phase Console](https://phase.dev) is an open source, end-to-end encrypted key management solution for developers to seamlessly encrypt production data in their apps. +- **[Phase Console](https://console.phase.dev)**: Dashboard for seamlessly creating, managing, rotating secrets and environment variables -We're on a mission to make strong encryption accessible to all developers, not just security teams. That means redesigning the entire developer experience from the ground up. - -## Features - -- **[Phase Console](https://console.phase.dev)**: Dashboard for seamlessly creating, managing, rotating and monitoring keys -- **[Phase KMS](https://phase.dev)**: A zero knowledge key management service -- **[Dual-Key Model](https://docs.phase.dev/security#dual-key-model)**: Avoid single point of compromise of the private key via [secret splitting schemes](https://en.wikipedia.org/wiki/Secret_sharing) - **[Hold your keys](https://docs.phase.dev/security/phase-encryption#account-keyring)**: Maintain self-custody of your root keys via 24 word mnemonic phrase +- **Secret management**: Diffs, version control and Point-in-time Recovery +- **RBAC**: Fine-grained, role-based and cryptographic access control, per application, per environment. +- **Service Tokens**: Authenticate CI runners, build tools and production environment with granualar-scope +- **Secret referencing**: Inherit secrets to create sophisticated configurations +- **[Audit Logs]()**: Compelte visibility into every change and access event - **[Self Hosting](https://docs.phase.dev)**: Run Phase on your own infrastructure -- **[Client SDKs](https://docs.phase.dev/sdks)**: Asynchronously encrypt data in the browsers of your users without any external API or sensitive keys [Live Demo](https://phase.dev/#use-cases) -- **[Server SDKs](https://docs.phase.dev/sdks)**: Securely decrypt and process data in memory only when you need to with 3 lines of code -- **[Phase I/O]()**: Self-hosted EaaS (Encryption as a Service) and a transparent proxy encryption (Coming Soon) +- **[Phase KMS](https://phase.dev)**: A zero knowledge key management service +- **[SDKs](https://docs.phase.dev/sdks)**: Encrypt / decrypt data with a few lines of code. And much more. --- -## What about SSE? +## CLI + +```bash +# Your existing secrets +> cat .env +AWS_ACCESS_KEY_ID="AKIA2OGYBAH63UA3VNFG" +AWS_SECRET_ACCESS_KEY="V5yWXDe82Gohf9DYBhpatYZ74a5fiKfJVx8rx6W1" -Relying on automatic database, disk or bucket level encryption has its limitations, since the data is automatically decrypted when retrieved and the keys typically belong to the hosting provider. A breach is a single SQL or a IAM misconfiguration away. +# Import your existing secrets +> phase secrets import .env +Successfully imported and encrypted 2 secrets. +To view them please run: phase secrets list -See: +# View your secrets in Phase +> phase secrets list +KEY 🗝️ | VALUE ✨ +---------------------------------------------------------------------------------------------------- +AWS_ACCESS_KEY_ID | AKI**************NFG +AWS_SECRET_ACCESS_KEY | V5y**********************************6W1 + +🥽 To uncover the secrets, use: phase secrets list --show + +# Get rid of your .env +> rm .env + +# Seamlessly inject secrets during runtime +> phase run yarn dev +$ next dev +ready - started server on 0.0.0.0:3000, url: http://localhost:3000 +``` + + +- **CLI**: Fetch, decrypt and inject secrets and environment variables to your application. Zero code changes required. +- Inject +- Export secrets in a dotenv format +- **Cross platform**: Easily install the Phase CLI on macOS, Ubuntu/Arch/Redhat/Alpine Linux, Windows, Docker. +- **Keyring Integration** - Store keys and credentials securely in [macOS Keychain](https://en.wikipedia.org/wiki/Keychain_%28software%29), [Windows Credential Locker](https://learn.microsoft.com/en-us/windows/uwp/security/credential-locker), [KDE Wallet](https://en.wikipedia.org/wiki/KWallet), [GNOME Keyring](https://en.wikipedia.org/wiki/GNOME_Keyring) etc. +- **[Private Key Sharding](https://docs.phase.dev/security#dual-key-model)**: Avoid single point of compromise of the private key via [secret splitting schemes](https://en.wikipedia.org/wiki/Secret_sharing) -- [OWASP - Cryptographic Failures](https://owasp.org/Top10/A02_2021-Cryptographic_Failures/#example-attack-scenarios) -- [IAM misconfiguration](https://github.com/nagwww/s3-leaks) -- [Problems with S3 encryption](https://www.secwale.com/p/encryption) --- @@ -65,11 +95,12 @@ The quickest and most reliable way to get started is making a new free account o ### Deploy Phase Console on your infrastructure -Deployment options: +- [Docker Compose](https://docs.phase.dev/self-hosting/docker-compose) +- [AWS](https://docs.phase.dev/self-hosting/aws) +- [Azure](https://docs.phase.dev/self-hosting/azure) +- [Google Cloud Platform](https://docs.phase.dev/self-hosting/gcp) +- [DigitalOcean](https://docs.phase.dev/self-hosting/digitalocean) -- Docker-compose -- AWS -- DigitalOcean See: [Self-hosting Phase](https://docs.phase.dev/self-hosting) @@ -83,29 +114,13 @@ See: [Self-hosting Phase](https://docs.phase.dev/self-hosting) More coming soon! -Example: - -```js -// Import & initialize -const Phase = require('@phase.dev/phase-node') -const phase = new Phase(APP_ID, APP_SECRET) - -// Encrypt -const ciphertext = await phase.encrypt('hello world') - -// Decrypt -const plaintext = await phase.decrypt(ciphertext) -console.log(plaintext) -$ hello world -``` - --- ## Community vs Enterprise edition -Phase operates on an [open-core](https://en.wikipedia.org/wiki/Open-core_model) model, similar to that of [GitLab](https://gitlab.com), [Infisical](https://infisical.com), [PostHog](https://posthog.com) etc. +Phase operates on an [open-core](https://en.wikipedia.org/wiki/Open-core_model) model, similar to that of [GitLab](https://gitlab.com). -This repo available under the [MIT expat license](/LICENSE), with the exception of the `ee` directory which will contain premium Pro or Enterprise features requiring a Phase license in the future. +This repo available under the [MIT expat license](/LICENSE), with the exception of the `ee` directory which will contain Pro or Enterprise features requiring a Phase license. --- @@ -121,7 +136,7 @@ For more information see: [SECURITY.md](/SECURITY.md) ## Contributing -Whether it's big or small, we love contributions. See [CONTRIBUTING.md](/CONTRIBUTING.md) +We love contributions. See [CONTRIBUTING.md](/CONTRIBUTING.md) You can join our [Slack](https://join.slack.com/t/phase-community/shared_invite/zt-1tkwzl31z-a6yCB5Uqlj~V2x43ep2Evg) if you have any questions! diff --git a/backend/Dockerfile b/backend/Dockerfile index ec188ffe..dd90314e 100644 --- a/backend/Dockerfile +++ b/backend/Dockerfile @@ -15,6 +15,7 @@ RUN set -ex \ && apk add --virtual rundeps $runDeps \ && apk del .build-deps +RUN apk add --no-cache curl RUN addgroup -S app && adduser -S app -G app ADD . /app WORKDIR /app diff --git a/backend/README.md b/backend/README.md index 24bfad39..99d9b9be 100644 --- a/backend/README.md +++ b/backend/README.md @@ -1,9 +1,9 @@ # Phase Console - Backend -Python Django REST api + Postgres +Django + Graphene + DRF ### Generate graphql schema for frontend ```bash -./manage.py graphql_schema --schema backend.schema.schema --out ../dashboard/apollo/schema.graphql +./manage.py graphql_schema --schema backend.schema.schema --out ../frontend/apollo/schema.graphql ``` diff --git a/backend/api/emails.py b/backend/api/emails.py new file mode 100644 index 00000000..1ed3e42a --- /dev/null +++ b/backend/api/emails.py @@ -0,0 +1,115 @@ +from django.conf import settings +from django.core.mail import send_mail +from django.template.loader import render_to_string +from datetime import datetime +import os +from api.utils import encode_string_to_base64, get_client_ip +from api.models import OrganisationMember + + +def get_org_member_name(org_member): + social_acc = org_member.user.socialaccount_set.first() + + member_name = social_acc.extra_data.get('name') + + if member_name is None: + member_name = org_member.email + + return member_name + + +def send_email(subject, recipient_list, template_name, context): + """ + Send email via SMTP gateway through Django's email backend. + """ + # Load the template + email_html_message = render_to_string(template_name, context) + + # Get the DEFAULT_FROM_EMAIL from settings + default_from_email = getattr(settings, "DEFAULT_FROM_EMAIL") + + # Send the email + send_mail( + subject, + '', # plain text content can be empty as we're sending HTML + default_from_email, + recipient_list, + html_message=email_html_message + ) + + +def send_login_email(request, email, provider): + user_agent = request.META.get('HTTP_USER_AGENT', 'Unknown') + ip_address = get_client_ip(request) + timestamp = datetime.now().strftime('%Y-%m-%d %H:%M:%S') + + # Creating context dictionary + context = { + 'auth': provider, + 'email': email, + 'ip': ip_address, + 'user_agent': user_agent, + 'timestamp': timestamp + } + + send_email( + 'New Login Alert - Phase Console', + [email], + 'backend/api/email_templates/login.html', + context + ) + + +def send_inite_email(invite): + organisation = invite.organisation.name + + invited_by_name = get_org_member_name(invite.invited_by) + + invite_code = encode_string_to_base64(str(invite.id)) + + invite_link = f"{os.getenv('ALLOWED_ORIGINS')}/invite/{invite_code}" + + context = { + 'organisation': organisation, + 'invited_by': invited_by_name, + 'invite_link': invite_link + } + + send_email( + f"Invite - {organisation} on Phase", + [invite.invitee_email], + 'backend/api/email_templates/invite.html', + context + ) + + +def send_user_joined_email(invite, new_member): + organisation = invite.organisation.name + + owner = OrganisationMember.objects.get( + organisation=invite.organisation, role=OrganisationMember.OWNER, deleted_at=None) + + owner_name = get_org_member_name(owner) + + invited_by_name = get_org_member_name(invite.invited_by) + + if owner_name == invited_by_name: + invited_by_name = 'you' + + new_user_name = get_org_member_name(new_member) + + if invited_by_name is None: + invited_by_name = invite.invited_by.user.email + + context = { + 'organisation': organisation, + 'invited_by': invited_by_name, + 'new_user': new_user_name + } + + send_email( + f"A new user has joined {organisation} on Phase", + [owner.user.email], + 'backend/api/email_templates/user_joined_org.html', + context + ) diff --git a/backend/api/migrations/0017_environment_secret_secrettag_secretevent_and_more.py b/backend/api/migrations/0017_environment_secret_secrettag_secretevent_and_more.py new file mode 100644 index 00000000..f9a0c114 --- /dev/null +++ b/backend/api/migrations/0017_environment_secret_secrettag_secretevent_and_more.py @@ -0,0 +1,106 @@ +# Generated by Django 4.2.3 on 2023-07-31 10:52 + +from django.conf import settings +import django.contrib.postgres.fields +from django.db import migrations, models +import django.db.models.deletion +import uuid + + +class Migration(migrations.Migration): + + dependencies = [ + ('api', '0016_organisation_plan'), + ] + + operations = [ + migrations.CreateModel( + name='Environment', + fields=[ + ('id', models.TextField(default=uuid.uuid4, editable=False, primary_key=True, serialize=False)), + ('name', models.CharField(max_length=64)), + ('env_type', models.CharField(choices=[('dev', 'Development'), ('staging', 'Staging'), ('prod', 'Production')], default='dev', max_length=7)), + ('wrapped_seed', models.CharField(max_length=208)), + ('wrapped_salt', models.CharField(max_length=208)), + ('created_at', models.DateTimeField(auto_now_add=True, null=True)), + ('updated_at', models.DateTimeField(auto_now=True)), + ('deleted_at', models.DateTimeField(blank=True, null=True)), + ('is_deleted', models.BooleanField(default=False)), + ('app', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='api.app')), + ], + ), + migrations.CreateModel( + name='Secret', + fields=[ + ('id', models.TextField(default=uuid.uuid4, editable=False, primary_key=True, serialize=False)), + ('collection', models.TextField(blank=True, null=True)), + ('key', models.TextField()), + ('key_digest', models.TextField()), + ('value', models.TextField()), + ('version', models.IntegerField(default=1)), + ('tags', django.contrib.postgres.fields.ArrayField(base_field=models.CharField(max_length=64), size=10)), + ('comment', models.TextField()), + ('created_at', models.DateTimeField(auto_now_add=True, null=True)), + ('updated_at', models.DateTimeField(auto_now=True)), + ('deleted_at', models.DateTimeField(blank=True, null=True)), + ('environment', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='api.environment')), + ('user', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, to=settings.AUTH_USER_MODEL)), + ], + ), + migrations.CreateModel( + name='SecretTag', + fields=[ + ('id', models.TextField(default=uuid.uuid4, editable=False, primary_key=True, serialize=False)), + ('name', models.CharField(max_length=64)), + ('organisation', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='api.organisation')), + ], + ), + migrations.CreateModel( + name='SecretEvent', + fields=[ + ('id', models.TextField(default=uuid.uuid4, editable=False, primary_key=True, serialize=False)), + ('collection', models.TextField(blank=True, null=True)), + ('key', models.TextField()), + ('key_digest', models.TextField()), + ('value', models.TextField()), + ('version', models.IntegerField(default=1)), + ('tags', django.contrib.postgres.fields.ArrayField(base_field=models.CharField(max_length=64), size=10)), + ('comment', models.TextField()), + ('event_type', models.CharField(choices=[('C', 'Create'), ('R', 'Read'), ('U', 'Update'), ('D', 'Delete')], default='C', max_length=1)), + ('timestamp', models.BigIntegerField()), + ('environment', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='api.environment')), + ('secret', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='api.secret')), + ('user', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, to=settings.AUTH_USER_MODEL)), + ], + ), + migrations.CreateModel( + name='EnvironmentSecret', + fields=[ + ('id', models.TextField(default=uuid.uuid4, editable=False, primary_key=True, serialize=False)), + ('identity_key', models.CharField(max_length=256)), + ('environment_token', models.CharField(max_length=64)), + ('wrapped_key_share', models.CharField(max_length=406)), + ('token', models.CharField(max_length=64)), + ('created_at', models.DateTimeField(auto_now_add=True, null=True)), + ('updated_at', models.DateTimeField(auto_now=True)), + ('deleted_at', models.DateTimeField(blank=True, null=True)), + ('environment', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='api.environment')), + ('user', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, to=settings.AUTH_USER_MODEL)), + ], + ), + migrations.CreateModel( + name='EnvironmentKey', + fields=[ + ('id', models.TextField(default=uuid.uuid4, editable=False, primary_key=True, serialize=False)), + ('identity_key', models.CharField(max_length=256)), + ('environment_token', models.CharField(max_length=64)), + ('wrapped_seed', models.CharField(max_length=208)), + ('wrapped_salt', models.CharField(max_length=208)), + ('created_at', models.DateTimeField(auto_now_add=True, null=True)), + ('updated_at', models.DateTimeField(auto_now=True)), + ('deleted_at', models.DateTimeField(blank=True, null=True)), + ('environment', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='api.environment')), + ('user', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, to=settings.AUTH_USER_MODEL)), + ], + ), + ] diff --git a/backend/api/migrations/0018_rename_environment_token_environmentsecret_name_and_more.py b/backend/api/migrations/0018_rename_environment_token_environmentsecret_name_and_more.py new file mode 100644 index 00000000..1f098d87 --- /dev/null +++ b/backend/api/migrations/0018_rename_environment_token_environmentsecret_name_and_more.py @@ -0,0 +1,60 @@ +# Generated by Django 4.2.3 on 2023-08-01 07:57 + +from django.db import migrations, models +import django.db.models.deletion +import uuid + + +class Migration(migrations.Migration): + + dependencies = [ + ('api', '0017_environment_secret_secrettag_secretevent_and_more'), + ] + + operations = [ + migrations.RenameField( + model_name='environmentsecret', + old_name='environment_token', + new_name='name', + ), + migrations.RemoveField( + model_name='environmentkey', + name='environment_token', + ), + migrations.RemoveField( + model_name='secret', + name='collection', + ), + migrations.AddField( + model_name='secrettag', + name='created_at', + field=models.DateTimeField(auto_now_add=True, null=True), + ), + migrations.AddField( + model_name='secrettag', + name='deleted_at', + field=models.DateTimeField(blank=True, null=True), + ), + migrations.AddField( + model_name='secrettag', + name='updated_at', + field=models.DateTimeField(auto_now=True), + ), + migrations.CreateModel( + name='SecretFolder', + fields=[ + ('id', models.TextField(default=uuid.uuid4, editable=False, primary_key=True, serialize=False)), + ('name', models.CharField(max_length=64)), + ('created_at', models.DateTimeField(auto_now_add=True, null=True)), + ('updated_at', models.DateTimeField(auto_now=True)), + ('deleted_at', models.DateTimeField(blank=True, null=True)), + ('environment', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='api.environment')), + ('parent', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='api.secretfolder')), + ], + ), + migrations.AddField( + model_name='secret', + name='folder', + field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.CASCADE, to='api.secretfolder'), + ), + ] diff --git a/backend/api/migrations/0019_remove_secret_user_remove_secretevent_collection_and_more.py b/backend/api/migrations/0019_remove_secret_user_remove_secretevent_collection_and_more.py new file mode 100644 index 00000000..cc4af583 --- /dev/null +++ b/backend/api/migrations/0019_remove_secret_user_remove_secretevent_collection_and_more.py @@ -0,0 +1,27 @@ +# Generated by Django 4.2.3 on 2023-08-02 07:03 + +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + + dependencies = [ + ('api', '0018_rename_environment_token_environmentsecret_name_and_more'), + ] + + operations = [ + migrations.RemoveField( + model_name='secret', + name='user', + ), + migrations.RemoveField( + model_name='secretevent', + name='collection', + ), + migrations.AddField( + model_name='secretevent', + name='folder', + field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.CASCADE, to='api.secretfolder'), + ), + ] diff --git a/backend/api/migrations/0020_remove_organisation_owner_and_more.py b/backend/api/migrations/0020_remove_organisation_owner_and_more.py new file mode 100644 index 00000000..f0a16b88 --- /dev/null +++ b/backend/api/migrations/0020_remove_organisation_owner_and_more.py @@ -0,0 +1,56 @@ +# Generated by Django 4.2.3 on 2023-08-04 09:30 + +from django.conf import settings +from django.db import migrations, models +import django.db.models.deletion +import uuid + +def migrate_org_owners(apps, schema_editor): + OrgModel = apps.get_model('api', 'Organisation') + OrgMemberModel = apps.get_model('api', 'OrganisationMember') + + for org in OrgModel.objects.all(): + OrgMemberModel.objects.create(user=org.owner, organisation=org, role='owner', identity_key=org.identity_key, created_at=org.created_at) + +class Migration(migrations.Migration): + + dependencies = [ + ('api', '0019_remove_secret_user_remove_secretevent_collection_and_more'), + ] + + operations = [ + migrations.CreateModel( + name='OrganisationMember', + fields=[ + ('id', models.TextField(default=uuid.uuid4, editable=False, primary_key=True, serialize=False)), + ('role', models.CharField(choices=[('owner', 'Owner'), ('admin', 'Admin'), ('dev', 'Developer')], default='dev', max_length=5)), + ('identity_key', models.CharField(blank=True, max_length=256, null=True)), + ('wrapped_keyring', models.TextField(blank=True)), + ('created_at', models.DateTimeField(auto_now_add=True, null=True)), + ('updated_at', models.DateTimeField(auto_now=True)), + ('deleted_at', models.DateTimeField(auto_now=True)), + ('organisation', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='users', to='api.organisation')), + ('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='organisation', to=settings.AUTH_USER_MODEL)), + ], + ), + migrations.AlterField( + model_name='environmentkey', + name='user', + field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, to='api.organisationmember'), + ), + migrations.AlterField( + model_name='environmentsecret', + name='user', + field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, to='api.organisationmember'), + ), + migrations.AlterField( + model_name='secretevent', + name='user', + field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, to='api.organisationmember'), + ), + migrations.RunPython(migrate_org_owners), + migrations.RemoveField( + model_name='organisation', + name='owner', + ), + ] diff --git a/backend/api/migrations/0021_remove_secretevent_timestamp.py b/backend/api/migrations/0021_remove_secretevent_timestamp.py new file mode 100644 index 00000000..936f7281 --- /dev/null +++ b/backend/api/migrations/0021_remove_secretevent_timestamp.py @@ -0,0 +1,17 @@ +# Generated by Django 4.2.3 on 2023-08-04 09:42 + +from django.db import migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ('api', '0020_remove_organisation_owner_and_more'), + ] + + operations = [ + migrations.RemoveField( + model_name='secretevent', + name='timestamp', + ), + ] diff --git a/backend/api/migrations/0022_secretevent_timestamp.py b/backend/api/migrations/0022_secretevent_timestamp.py new file mode 100644 index 00000000..ca51b369 --- /dev/null +++ b/backend/api/migrations/0022_secretevent_timestamp.py @@ -0,0 +1,20 @@ +# Generated by Django 4.2.3 on 2023-08-04 09:54 + +from django.db import migrations, models +import django.utils.timezone + + +class Migration(migrations.Migration): + + dependencies = [ + ('api', '0021_remove_secretevent_timestamp'), + ] + + operations = [ + migrations.AddField( + model_name='secretevent', + name='timestamp', + field=models.DateTimeField(auto_now_add=True, default=django.utils.timezone.now), + preserve_default=False, + ), + ] diff --git a/backend/api/migrations/0023_environment_identity_key.py b/backend/api/migrations/0023_environment_identity_key.py new file mode 100644 index 00000000..9724ddf2 --- /dev/null +++ b/backend/api/migrations/0023_environment_identity_key.py @@ -0,0 +1,19 @@ +# Generated by Django 4.2.3 on 2023-08-09 13:23 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('api', '0022_secretevent_timestamp'), + ] + + operations = [ + migrations.AddField( + model_name='environment', + name='identity_key', + field=models.CharField(default='', max_length=256), + preserve_default=False, + ), + ] diff --git a/backend/api/migrations/0024_alter_environment_wrapped_salt_and_more.py b/backend/api/migrations/0024_alter_environment_wrapped_salt_and_more.py new file mode 100644 index 00000000..346581de --- /dev/null +++ b/backend/api/migrations/0024_alter_environment_wrapped_salt_and_more.py @@ -0,0 +1,33 @@ +# Generated by Django 4.2.3 on 2023-08-12 09:16 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('api', '0023_environment_identity_key'), + ] + + operations = [ + migrations.AlterField( + model_name='environment', + name='wrapped_salt', + field=models.CharField(max_length=256), + ), + migrations.AlterField( + model_name='environment', + name='wrapped_seed', + field=models.CharField(max_length=256), + ), + migrations.AlterField( + model_name='environmentkey', + name='wrapped_salt', + field=models.CharField(max_length=256), + ), + migrations.AlterField( + model_name='environmentkey', + name='wrapped_seed', + field=models.CharField(max_length=256), + ), + ] diff --git a/backend/api/migrations/0025_rename_environmentsecret_environmenttoken_usertoken.py b/backend/api/migrations/0025_rename_environmentsecret_environmenttoken_usertoken.py new file mode 100644 index 00000000..7b70a0e3 --- /dev/null +++ b/backend/api/migrations/0025_rename_environmentsecret_environmenttoken_usertoken.py @@ -0,0 +1,33 @@ +# Generated by Django 4.2.3 on 2023-08-12 10:28 + +from django.db import migrations, models +import django.db.models.deletion +import uuid + + +class Migration(migrations.Migration): + + dependencies = [ + ('api', '0024_alter_environment_wrapped_salt_and_more'), + ] + + operations = [ + migrations.RenameModel( + old_name='EnvironmentSecret', + new_name='EnvironmentToken', + ), + migrations.CreateModel( + name='UserToken', + fields=[ + ('id', models.TextField(default=uuid.uuid4, editable=False, primary_key=True, serialize=False)), + ('name', models.CharField(max_length=64)), + ('identity_key', models.CharField(max_length=256)), + ('token', models.CharField(max_length=64)), + ('wrapped_key_share', models.CharField(max_length=406)), + ('created_at', models.DateTimeField(auto_now_add=True, null=True)), + ('updated_at', models.DateTimeField(auto_now=True)), + ('deleted_at', models.DateTimeField(blank=True, null=True)), + ('user', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, to='api.organisationmember')), + ], + ), + ] diff --git a/backend/api/migrations/0026_secretfolder_color_remove_secret_tags_secret_tags.py b/backend/api/migrations/0026_secretfolder_color_remove_secret_tags_secret_tags.py new file mode 100644 index 00000000..85430e62 --- /dev/null +++ b/backend/api/migrations/0026_secretfolder_color_remove_secret_tags_secret_tags.py @@ -0,0 +1,28 @@ +# Generated by Django 4.2.3 on 2023-08-29 08:21 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('api', '0025_rename_environmentsecret_environmenttoken_usertoken'), + ] + + operations = [ + migrations.AddField( + model_name='secretfolder', + name='color', + field=models.CharField(default='', max_length=64), + preserve_default=False, + ), + migrations.RemoveField( + model_name='secret', + name='tags', + ), + migrations.AddField( + model_name='secret', + name='tags', + field=models.ManyToManyField(to='api.secrettag'), + ), + ] diff --git a/backend/api/migrations/0027_remove_secretfolder_color_secrettag_color.py b/backend/api/migrations/0027_remove_secretfolder_color_secrettag_color.py new file mode 100644 index 00000000..b2fc0597 --- /dev/null +++ b/backend/api/migrations/0027_remove_secretfolder_color_secrettag_color.py @@ -0,0 +1,23 @@ +# Generated by Django 4.2.3 on 2023-08-29 08:35 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('api', '0026_secretfolder_color_remove_secret_tags_secret_tags'), + ] + + operations = [ + migrations.RemoveField( + model_name='secretfolder', + name='color', + ), + migrations.AddField( + model_name='secrettag', + name='color', + field=models.CharField(default='', max_length=64), + preserve_default=False, + ), + ] diff --git a/backend/api/migrations/0028_remove_secretevent_tags_secretevent_tags.py b/backend/api/migrations/0028_remove_secretevent_tags_secretevent_tags.py new file mode 100644 index 00000000..0b20cd75 --- /dev/null +++ b/backend/api/migrations/0028_remove_secretevent_tags_secretevent_tags.py @@ -0,0 +1,22 @@ +# Generated by Django 4.2.3 on 2023-08-29 08:42 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('api', '0027_remove_secretfolder_color_secrettag_color'), + ] + + operations = [ + migrations.RemoveField( + model_name='secretevent', + name='tags', + ), + migrations.AddField( + model_name='secretevent', + name='tags', + field=models.ManyToManyField(to='api.secrettag'), + ), + ] diff --git a/backend/api/migrations/0029_servicetoken.py b/backend/api/migrations/0029_servicetoken.py new file mode 100644 index 00000000..19e103a4 --- /dev/null +++ b/backend/api/migrations/0029_servicetoken.py @@ -0,0 +1,32 @@ +# Generated by Django 4.2.3 on 2023-09-06 08:34 + +from django.db import migrations, models +import django.db.models.deletion +import uuid + + +class Migration(migrations.Migration): + + dependencies = [ + ('api', '0028_remove_secretevent_tags_secretevent_tags'), + ] + + operations = [ + migrations.CreateModel( + name='ServiceToken', + fields=[ + ('id', models.TextField(default=uuid.uuid4, editable=False, primary_key=True, serialize=False)), + ('identity_key', models.CharField(max_length=256)), + ('token', models.CharField(max_length=64)), + ('wrapped_key_share', models.CharField(max_length=406)), + ('name', models.CharField(max_length=64)), + ('created_at', models.DateTimeField(auto_now_add=True, null=True)), + ('updated_at', models.DateTimeField(auto_now=True)), + ('deleted_at', models.DateTimeField(blank=True, null=True)), + ('expires_at', models.DateTimeField(null=True)), + ('app', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='api.app')), + ('created_by', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, to='api.organisationmember')), + ('keys', models.ManyToManyField(to='api.environmentkey')), + ], + ), + ] diff --git a/backend/api/migrations/0030_usertoken_expires_at.py b/backend/api/migrations/0030_usertoken_expires_at.py new file mode 100644 index 00000000..96558b80 --- /dev/null +++ b/backend/api/migrations/0030_usertoken_expires_at.py @@ -0,0 +1,18 @@ +# Generated by Django 4.2.3 on 2023-09-09 09:08 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('api', '0029_servicetoken'), + ] + + operations = [ + migrations.AddField( + model_name='usertoken', + name='expires_at', + field=models.DateTimeField(null=True), + ), + ] diff --git a/backend/api/migrations/0031_organisationmemberinvite.py b/backend/api/migrations/0031_organisationmemberinvite.py new file mode 100644 index 00000000..98b901bb --- /dev/null +++ b/backend/api/migrations/0031_organisationmemberinvite.py @@ -0,0 +1,28 @@ +# Generated by Django 4.2.3 on 2023-09-12 08:18 + +from django.db import migrations, models +import django.db.models.deletion +import uuid + + +class Migration(migrations.Migration): + + dependencies = [ + ('api', '0030_usertoken_expires_at'), + ] + + operations = [ + migrations.CreateModel( + name='OrganisationMemberInvite', + fields=[ + ('id', models.TextField(default=uuid.uuid4, editable=False, primary_key=True, serialize=False)), + ('invitee_email', models.EmailField(max_length=254)), + ('valid', models.BooleanField(default=True)), + ('created_at', models.DateTimeField(auto_now_add=True, null=True)), + ('updated_at', models.DateTimeField(auto_now=True)), + ('expires_at', models.DateTimeField()), + ('invited_by', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='api.organisationmember')), + ('organisation', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='invites', to='api.organisation')), + ], + ), + ] diff --git a/backend/api/migrations/0032_organisationmemberinvite_apps.py b/backend/api/migrations/0032_organisationmemberinvite_apps.py new file mode 100644 index 00000000..68f7a410 --- /dev/null +++ b/backend/api/migrations/0032_organisationmemberinvite_apps.py @@ -0,0 +1,18 @@ +# Generated by Django 4.2.3 on 2023-09-12 13:04 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('api', '0031_organisationmemberinvite'), + ] + + operations = [ + migrations.AddField( + model_name='organisationmemberinvite', + name='apps', + field=models.ManyToManyField(to='api.app'), + ), + ] diff --git a/backend/api/migrations/0033_organisationmemberinvite_role.py b/backend/api/migrations/0033_organisationmemberinvite_role.py new file mode 100644 index 00000000..31b73a6a --- /dev/null +++ b/backend/api/migrations/0033_organisationmemberinvite_role.py @@ -0,0 +1,18 @@ +# Generated by Django 4.2.3 on 2023-09-14 08:29 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('api', '0032_organisationmemberinvite_apps'), + ] + + operations = [ + migrations.AddField( + model_name='organisationmemberinvite', + name='role', + field=models.CharField(choices=[('owner', 'Owner'), ('admin', 'Admin'), ('dev', 'Developer')], default='dev', max_length=5), + ), + ] diff --git a/backend/api/migrations/0034_organisationmember_apps.py b/backend/api/migrations/0034_organisationmember_apps.py new file mode 100644 index 00000000..fbbd2642 --- /dev/null +++ b/backend/api/migrations/0034_organisationmember_apps.py @@ -0,0 +1,29 @@ +# Generated by Django 4.2.3 on 2023-09-15 13:19 + +from django.db import migrations, models + + +def set_org_member_apps(apps, schema_editor): + OrgMemberModel = apps.get_model('api', 'OrganisationMember') + AppModel = apps.get_model('api', 'App') + + for org_member in OrgMemberModel.objects.all(): + org_apps = AppModel.objects.filter( + organisation=org_member.organisation, is_deleted=False) + org_member.apps.set(org_apps) + + +class Migration(migrations.Migration): + + dependencies = [ + ('api', '0033_organisationmemberinvite_role'), + ] + + operations = [ + migrations.AddField( + model_name='organisationmember', + name='apps', + field=models.ManyToManyField(to='api.app'), + ), + migrations.RunPython(set_org_member_apps) + ] diff --git a/backend/api/migrations/0035_alter_organisationmember_deleted_at.py b/backend/api/migrations/0035_alter_organisationmember_deleted_at.py new file mode 100644 index 00000000..222284eb --- /dev/null +++ b/backend/api/migrations/0035_alter_organisationmember_deleted_at.py @@ -0,0 +1,27 @@ +# Generated by Django 4.2.3 on 2023-09-16 08:50 + +from django.db import migrations, models + + +def reset_deleted_field(apps, schema_editor): + OrgMemberModel = apps.get_model('api', 'OrganisationMember') + + for org_member in OrgMemberModel.objects.all(): + org_member.deleted_at = None + org_member.save() + + +class Migration(migrations.Migration): + + dependencies = [ + ('api', '0034_organisationmember_apps'), + ] + + operations = [ + migrations.AlterField( + model_name='organisationmember', + name='deleted_at', + field=models.DateTimeField(blank=True, null=True), + ), + migrations.RunPython(reset_deleted_field) + ] diff --git a/backend/api/migrations/0036_alter_organisationmember_apps.py b/backend/api/migrations/0036_alter_organisationmember_apps.py new file mode 100644 index 00000000..e666c0f3 --- /dev/null +++ b/backend/api/migrations/0036_alter_organisationmember_apps.py @@ -0,0 +1,18 @@ +# Generated by Django 4.2.3 on 2023-09-20 06:43 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('api', '0035_alter_organisationmember_deleted_at'), + ] + + operations = [ + migrations.AlterField( + model_name='organisationmember', + name='apps', + field=models.ManyToManyField(related_name='members', to='api.app'), + ), + ] diff --git a/backend/api/migrations/0037_organisationmember_wrapped_recovery.py b/backend/api/migrations/0037_organisationmember_wrapped_recovery.py new file mode 100644 index 00000000..22f22588 --- /dev/null +++ b/backend/api/migrations/0037_organisationmember_wrapped_recovery.py @@ -0,0 +1,18 @@ +# Generated by Django 4.2.3 on 2023-09-27 08:01 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('api', '0036_alter_organisationmember_apps'), + ] + + operations = [ + migrations.AddField( + model_name='organisationmember', + name='wrapped_recovery', + field=models.TextField(blank=True), + ), + ] diff --git a/backend/api/migrations/0038_secretevent_ip_address_secretevent_user_agent.py b/backend/api/migrations/0038_secretevent_ip_address_secretevent_user_agent.py new file mode 100644 index 00000000..8c4d6242 --- /dev/null +++ b/backend/api/migrations/0038_secretevent_ip_address_secretevent_user_agent.py @@ -0,0 +1,23 @@ +# Generated by Django 4.2.3 on 2023-10-05 15:31 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('api', '0037_organisationmember_wrapped_recovery'), + ] + + operations = [ + migrations.AddField( + model_name='secretevent', + name='ip_address', + field=models.GenericIPAddressField(blank=True, null=True), + ), + migrations.AddField( + model_name='secretevent', + name='user_agent', + field=models.TextField(blank=True, null=True), + ), + ] diff --git a/backend/api/models.py b/backend/api/models.py index c5f50c7d..f0d05a3c 100644 --- a/backend/api/models.py +++ b/backend/api/models.py @@ -1,13 +1,15 @@ from django.db import models +from django.contrib.postgres.fields import ArrayField from django.contrib.auth.models import AbstractBaseUser, BaseUserManager, PermissionsMixin from uuid import uuid4 from backend.api.kv import write import json - +from django.utils import timezone from django.conf import settings CLOUD_HOSTED = settings.APP_HOST == 'cloud' + class CustomUserManager(BaseUserManager): def create_user(self, username, email, password=None): """ @@ -72,10 +74,8 @@ class Organisation(models.Model): (PRO_PLAN, 'Pro'), (ENTERPRISE_PLAN, 'Enterprise') ] - + id = models.TextField(default=uuid4, primary_key=True, editable=False) - owner = models.ForeignKey( - CustomUser, related_name='organisation', on_delete=models.CASCADE) name = models.CharField(max_length=64, unique=True) identity_key = models.CharField(max_length=256) created_at = models.DateTimeField(auto_now_add=True, blank=True, null=True) @@ -85,7 +85,7 @@ class Organisation(models.Model): choices=PLAN_TIERS, default=FREE_PLAN, ) - list_display = ('owner', 'name', 'identity_key', 'id') + list_display = ('name', 'identity_key', 'id') def __str__(self): return self.name @@ -94,7 +94,7 @@ def __str__(self): class App(models.Model): id = models.TextField(default=uuid4, primary_key=True, editable=False) organisation = models.ForeignKey(Organisation, on_delete=models.CASCADE) - name = name = models.CharField(max_length=64) + name = models.CharField(max_length=64) identity_key = models.CharField(max_length=256) app_version = models.IntegerField(null=False, blank=False, default=1) app_token = models.CharField(max_length=64) @@ -122,3 +122,222 @@ def save(self, *args, **kwargs): def __str__(self): return self.name + + +class OrganisationMember(models.Model): + OWNER = 'owner' + ADMIN = 'admin' + DEVELOPER = 'dev' + + USER_ROLES = [ + (OWNER, 'Owner'), + (ADMIN, 'Admin'), + (DEVELOPER, 'Developer') + ] + + id = models.TextField(default=uuid4, primary_key=True, editable=False) + user = models.ForeignKey( + CustomUser, related_name='organisation', on_delete=models.CASCADE) + organisation = models.ForeignKey( + Organisation, related_name='users', on_delete=models.CASCADE) + role = models.CharField( + max_length=5, + choices=USER_ROLES, + default=DEVELOPER, + ) + apps = models.ManyToManyField(App, related_name='members') + identity_key = models.CharField(max_length=256, null=True, blank=True) + wrapped_keyring = models.TextField(blank=True) + wrapped_recovery = models.TextField(blank=True) + created_at = models.DateTimeField(auto_now_add=True, blank=True, null=True) + updated_at = models.DateTimeField(auto_now=True) + deleted_at = models.DateTimeField(null=True, blank=True) + + def delete(self, *args, **kwargs): + """ + Soft delete the object by setting the 'deleted_at' field. + """ + self.deleted_at = timezone.now() + self.save() + + +class OrganisationMemberInvite(models.Model): + id = models.TextField(default=uuid4, primary_key=True, editable=False) + organisation = models.ForeignKey( + Organisation, related_name='invites', on_delete=models.CASCADE) + apps = models.ManyToManyField(App) + role = models.CharField( + max_length=5, + choices=OrganisationMember.USER_ROLES, + default=OrganisationMember.DEVELOPER, + ) + invited_by = models.ForeignKey( + OrganisationMember, on_delete=models.CASCADE) + invitee_email = models.EmailField() + valid = models.BooleanField(default=True) + created_at = models.DateTimeField(auto_now_add=True, blank=True, null=True) + updated_at = models.DateTimeField(auto_now=True) + expires_at = models.DateTimeField() + + +class Environment(models.Model): + + DEVELOPMENT = "dev" + STAGING = "staging" + PRODUCTION = "prod" + + ENV_TYPES = [ + (DEVELOPMENT, 'Development'), + (STAGING, 'Staging'), + (PRODUCTION, 'Production') + ] + + id = models.TextField(default=uuid4, primary_key=True, editable=False) + app = models.ForeignKey(App, on_delete=models.CASCADE) + name = models.CharField(max_length=64) + env_type = models.CharField( + max_length=7, + choices=ENV_TYPES, + default=DEVELOPMENT, + ) + identity_key = models.CharField(max_length=256) + wrapped_seed = models.CharField(max_length=256) + wrapped_salt = models.CharField(max_length=256) + created_at = models.DateTimeField(auto_now_add=True, blank=True, null=True) + updated_at = models.DateTimeField(auto_now=True) + deleted_at = models.DateTimeField(blank=True, null=True) + is_deleted = models.BooleanField(default=False) + + +class EnvironmentKey(models.Model): + id = models.TextField(default=uuid4, primary_key=True, editable=False) + environment = models.ForeignKey(Environment, on_delete=models.CASCADE) + user = models.ForeignKey( + OrganisationMember, on_delete=models.CASCADE, blank=True, null=True) + identity_key = models.CharField(max_length=256) + wrapped_seed = models.CharField(max_length=256) + wrapped_salt = models.CharField(max_length=256) + created_at = models.DateTimeField(auto_now_add=True, blank=True, null=True) + updated_at = models.DateTimeField(auto_now=True) + deleted_at = models.DateTimeField(blank=True, null=True) + + def delete(self, *args, **kwargs): + self.deleted_at = timezone.now() + self.save() + + +class EnvironmentToken(models.Model): + id = models.TextField(default=uuid4, primary_key=True, editable=False) + environment = models.ForeignKey(Environment, on_delete=models.CASCADE) + user = models.ForeignKey( + OrganisationMember, on_delete=models.CASCADE, blank=True, null=True) + name = models.CharField(max_length=64) + identity_key = models.CharField(max_length=256) + token = models.CharField(max_length=64) + wrapped_key_share = models.CharField(max_length=406) + created_at = models.DateTimeField(auto_now_add=True, blank=True, null=True) + updated_at = models.DateTimeField(auto_now=True) + deleted_at = models.DateTimeField(blank=True, null=True) + + +class ServiceToken(models.Model): + id = models.TextField(default=uuid4, primary_key=True, editable=False) + app = models.ForeignKey(App, on_delete=models.CASCADE) + keys = models.ManyToManyField(EnvironmentKey) + identity_key = models.CharField(max_length=256) + token = models.CharField(max_length=64) + wrapped_key_share = models.CharField(max_length=406) + name = models.CharField(max_length=64) + created_by = models.ForeignKey( + OrganisationMember, on_delete=models.CASCADE, blank=True, null=True) + created_at = models.DateTimeField(auto_now_add=True, blank=True, null=True) + updated_at = models.DateTimeField(auto_now=True) + deleted_at = models.DateTimeField(blank=True, null=True) + expires_at = models.DateTimeField(null=True) + + +class UserToken(models.Model): + id = models.TextField(default=uuid4, primary_key=True, editable=False) + user = models.ForeignKey( + OrganisationMember, on_delete=models.CASCADE, blank=True, null=True) + name = models.CharField(max_length=64) + identity_key = models.CharField(max_length=256) + token = models.CharField(max_length=64) + wrapped_key_share = models.CharField(max_length=406) + created_at = models.DateTimeField(auto_now_add=True, blank=True, null=True) + updated_at = models.DateTimeField(auto_now=True) + deleted_at = models.DateTimeField(blank=True, null=True) + expires_at = models.DateTimeField(null=True) + + +class SecretFolder(models.Model): + id = models.TextField(default=uuid4, primary_key=True, editable=False) + environment = models.ForeignKey(Environment, on_delete=models.CASCADE) + parent = models.ForeignKey('self', on_delete=models.CASCADE) + name = models.CharField(max_length=64) + created_at = models.DateTimeField(auto_now_add=True, blank=True, null=True) + updated_at = models.DateTimeField(auto_now=True) + deleted_at = models.DateTimeField(blank=True, null=True) + + +class SecretTag(models.Model): + id = models.TextField(default=uuid4, primary_key=True, editable=False) + organisation = models.ForeignKey(Organisation, on_delete=models.CASCADE) + name = models.CharField(max_length=64) + color = models.CharField(max_length=64) + created_at = models.DateTimeField(auto_now_add=True, blank=True, null=True) + updated_at = models.DateTimeField(auto_now=True) + deleted_at = models.DateTimeField(blank=True, null=True) + + +class Secret(models.Model): + id = models.TextField(default=uuid4, primary_key=True, editable=False) + environment = models.ForeignKey(Environment, on_delete=models.CASCADE) + folder = models.ForeignKey( + SecretFolder, on_delete=models.CASCADE, null=True) + key = models.TextField() + key_digest = models.TextField() + value = models.TextField() + version = models.IntegerField(default=1) + tags = models.ManyToManyField(SecretTag) + comment = models.TextField() + created_at = models.DateTimeField(auto_now_add=True, blank=True, null=True) + updated_at = models.DateTimeField(auto_now=True) + deleted_at = models.DateTimeField(blank=True, null=True) + + +class SecretEvent(models.Model): + + CREATE = "C" + READ = "R" + UPDATE = "U" + DELETE = "D" + + EVENT_TYPES = [ + (CREATE, 'Create'), + (READ, 'Read'), + (UPDATE, 'Update'), + (DELETE, 'Delete') + ] + + id = models.TextField(default=uuid4, primary_key=True, editable=False) + secret = models.ForeignKey(Secret, on_delete=models.CASCADE) + environment = models.ForeignKey(Environment, on_delete=models.CASCADE) + folder = models.ForeignKey( + SecretFolder, on_delete=models.CASCADE, null=True) + user = models.ForeignKey( + OrganisationMember, on_delete=models.SET_NULL, blank=True, null=True) + key = models.TextField() + key_digest = models.TextField() + value = models.TextField() + version = models.IntegerField(default=1) + tags = models.ManyToManyField(SecretTag) + comment = models.TextField() + event_type = models.CharField( + max_length=1, + choices=EVENT_TYPES, + default=CREATE, + ) + timestamp = models.DateTimeField(auto_now_add=True) + ip_address = models.GenericIPAddressField(null=True, blank=True) + user_agent = models.TextField(null=True, blank=True) diff --git a/backend/api/serializers.py b/backend/api/serializers.py index 365174f9..a3e44c36 100644 --- a/backend/api/serializers.py +++ b/backend/api/serializers.py @@ -1,8 +1,15 @@ -from rest_framework.serializers import ModelSerializer -from .models import CustomUser, Organisation +from rest_framework import serializers +from .models import CustomUser, Environment, EnvironmentKey, Organisation, Secret, ServiceToken, UserToken -class CustomUserSerializer(ModelSerializer): +def find_index_by_id(dictionaries, target_id): + for index, dictionary in enumerate(dictionaries): + if dictionary.get('id') == target_id: + return index + return -1 + + +class CustomUserSerializer(serializers.ModelSerializer): class Meta: model = CustomUser fields = [ @@ -22,10 +29,108 @@ def create(self, validated_data): return user -class OrganisationSerializer(ModelSerializer): +class OrganisationSerializer(serializers.ModelSerializer): class Meta: model = Organisation fields = ['id', 'name', 'identity_key', 'created_at'] def create(self, validated_data): return Organisation(**validated_data) + + +class SecretSerializer(serializers.ModelSerializer): + class Meta: + model = Secret + fields = '__all__' + + +class EnvironmentSerializer(serializers.ModelSerializer): + class Meta: + model = Environment + fields = ['id', 'name', 'env_type'] + + +class EnvironmentKeySerializer(serializers.ModelSerializer): + environment = EnvironmentSerializer() + + class Meta: + model = EnvironmentKey + fields = '__all__' + + +class UserTokenSerializer(serializers.ModelSerializer): + apps = EnvironmentKeySerializer(many=True, read_only=True) + + # New field 'userId' + user_id = serializers.UUIDField(source='user.id', read_only=True) + + # New field 'offline_enabled' with default value False + offline_enabled = serializers.BooleanField(default=False, read_only=True) + + class Meta: + model = UserToken + fields = ['wrapped_key_share', 'user_id', 'offline_enabled', 'apps'] + + def to_representation(self, instance): + representation = super().to_representation(instance) + + # Filter environment_keys to include only those associated with the same user + user = instance.user + + if user is not None: + environment_keys = EnvironmentKey.objects.filter( + user=user, environment__app__deleted_at=None) + apps = [] + for key in environment_keys: + + serializer = EnvironmentKeySerializer(key) + index = find_index_by_id(apps, key.environment.app.id) + + app_data = { + 'id': key.environment.app.id, + 'name': key.environment.app.name, + 'encryption': 'E2E', # Adding encryption to each app + } + + if index == -1: + app_data['environment_keys'] = [serializer.data] + apps.append(app_data) + else: + apps[index]['environment_keys'].append(serializer.data) + + representation['apps'] = apps + + return representation + + +class ServiceTokenSerializer(serializers.ModelSerializer): + apps = EnvironmentKeySerializer(many=True, read_only=True) + + class Meta: + model = ServiceToken + fields = ['wrapped_key_share', 'apps'] + + def to_representation(self, instance): + representation = super().to_representation(instance) + + environment_keys = instance.keys.all() + apps = [] + for key in environment_keys: + serializer = EnvironmentKeySerializer(key) + index = find_index_by_id(apps, key.environment.app.id) + + app_data = { + 'id': key.environment.app.id, + 'name': key.environment.app.name, + 'encryption': 'E2E', # Adding encryption to each app + } + + if index == -1: + app_data['environment_keys'] = [serializer.data] + apps.append(app_data) + else: + apps[index]['environment_keys'].append(serializer.data) + + representation['apps'] = apps + + return representation diff --git a/backend/api/templates/backend/api/email_templates/invite.html b/backend/api/templates/backend/api/email_templates/invite.html new file mode 100644 index 00000000..591bec38 --- /dev/null +++ b/backend/api/templates/backend/api/email_templates/invite.html @@ -0,0 +1,149 @@ + + + + Phase Invite + + + +
+ + + + + +
+ + +

Phase Invite

+
+
+

+ You have been invited to join {{ organisation }} on + Phase by {{ invited_by }}. +

+
+ +

Click the link below to accept the invite

+

Join

+
+
+ +
+ + diff --git a/backend/api/templates/backend/api/email_templates/login.html b/backend/api/templates/backend/api/email_templates/login.html new file mode 100644 index 00000000..62a82109 --- /dev/null +++ b/backend/api/templates/backend/api/email_templates/login.html @@ -0,0 +1,90 @@ + + + + New Login Alert + + + + + + + + +
+ + +

Login alert

+
+
+

You have logged into the Phase Console via {{ auth }} on {{ email }}.

+
+ + + + + + + + + + + + + +
IP address:{{ ip }}
User Agent:{{ user_agent }}
Timestamp:{{ timestamp }}
+
+

If this wasn't you, please check your account security or contact support.

+
+ + + diff --git a/backend/api/templates/backend/api/email_templates/user_joined_org.html b/backend/api/templates/backend/api/email_templates/user_joined_org.html new file mode 100644 index 00000000..56e373cc --- /dev/null +++ b/backend/api/templates/backend/api/email_templates/user_joined_org.html @@ -0,0 +1,146 @@ + + + + Phase Invite + + + +
+ + + + + +
+ + +

{{ new_user }} has joined your organisation on Phase

+
+
+

+ {{ new_user }} has accepted the invitation from {{ invited_by }} and + joined {{ organisation }} on Phase. +

+
+
+
+ +
+ + diff --git a/backend/api/utils.py b/backend/api/utils.py index 29a2fa52..7fc5cbcc 100644 --- a/backend/api/utils.py +++ b/backend/api/utils.py @@ -1,7 +1,73 @@ +from api.models import EnvironmentToken, ServiceToken, UserToken +from django.utils import timezone +import base64 + + def get_client_ip(request): x_forwarded_for = request.META.get('HTTP_X_FORWARDED_FOR') if x_forwarded_for: ip = x_forwarded_for.split(',')[0] else: ip = request.META.get('REMOTE_ADDR') - return ip \ No newline at end of file + return ip + + +def get_resolver_request_meta(request): + user_agent = request.META.get('HTTP_USER_AGENT', 'Unknown') + ip_address = get_client_ip(request) + + return ip_address, user_agent + + +def get_token_type(auth_token): + return auth_token.split(" ")[1] + + +def get_env_from_service_token(auth_token): + token = auth_token.split(" ")[2] + + if not token: + return False + + try: + env_token = EnvironmentToken.objects.get(token=token) + return env_token.environment, env_token.user + except Exception as ex: + return False + + +def get_org_member_from_user_token(auth_token): + token = auth_token.split(" ")[2] + + if not token: + return False + + try: + user_token = UserToken.objects.get(token=token) + return user_token.user + except Exception as ex: + return False + + +def token_is_expired_or_deleted(auth_token): + prefix, token_type, token_value = auth_token.split(" ") + + if token_type == 'User': + token = UserToken.objects.get(token=token_value) + else: + token = ServiceToken.objects.get(token=token_value) + + return token.deleted_at is not None or (token.expires_at is not None and token.expires_at < timezone.now()) + + +def encode_string_to_base64(s): + # Convert string to bytes + byte_representation = s.encode('utf-8') + + # Base64 encode the bytes + base64_bytes = base64.b64encode(byte_representation) + + # Convert the encoded bytes back to a string + base64_string = base64_bytes.decode('utf-8') + + return base64_string diff --git a/backend/api/views.py b/backend/api/views.py index 3389bfd7..e15bfdef 100644 --- a/backend/api/views.py +++ b/backend/api/views.py @@ -1,5 +1,9 @@ from datetime import datetime +import json +from api.serializers import EnvironmentKeySerializer, SecretSerializer, ServiceTokenSerializer, UserTokenSerializer +from api.emails import send_login_email +from backend.graphene.utils.permissions import user_can_access_environment from dj_rest_auth.registration.views import SocialLoginView from django.contrib.auth.mixins import LoginRequiredMixin from graphene_django.views import GraphQLView @@ -8,9 +12,9 @@ from rest_framework.permissions import AllowAny from rest_framework.response import Response from django.http import JsonResponse, HttpResponse -from api.utils import get_client_ip +from api.utils import get_client_ip, get_env_from_service_token, get_org_member_from_user_token, get_resolver_request_meta, get_token_type, token_is_expired_or_deleted from logs.models import KMSDBLog -from .models import App +from .models import App, Environment, EnvironmentKey, EnvironmentToken, Secret, SecretEvent, SecretTag, ServiceToken, UserToken import jwt import requests from django.contrib.auth import logout @@ -23,10 +27,17 @@ from allauth.socialaccount.providers.google.views import GoogleOAuth2Adapter from allauth.socialaccount.providers.github.views import GitHubOAuth2Adapter from allauth.socialaccount.providers.oauth2.views import OAuth2Adapter +from rest_framework.views import APIView +from rest_framework import status +from django.views.decorators.csrf import csrf_exempt +from django.utils import timezone + CLOUD_HOSTED = settings.APP_HOST == 'cloud' # for custom gitlab adapter class + + def _check_errors(response): # 403 error's are presented as user-facing errors if response.status_code == 403: @@ -76,9 +87,18 @@ def complete_login(self, request, app, token, response, **kwargs): raise OAuth2Error("Invalid id_token") from e login = self.get_provider().sociallogin_from_response(request, identity_data) email = login.email_addresses[0] + if CLOUD_HOSTED and not CustomUser.objects.filter(email=email).exists(): - # new user - notify_slack(f"New user signup: {email}") + try: + # Notify Slack + notify_slack(f"New user signup: {email}") + except Exception as e: + print(f"Error notifying Slack: {e}") + + try: + send_login_email(request, email, 'Google') + except Exception as e: + print(f"Error sending email: {e}") return login @@ -108,9 +128,19 @@ def complete_login(self, request, app, token, **kwargs): extra_data["email"] = self.get_email(headers) email = extra_data["email"] + if CLOUD_HOSTED and not CustomUser.objects.filter(email=email).exists(): - # new user - notify_slack(f"New user signup: {email}") + try: + # Notify Slack + notify_slack(f"New user signup: {email}") + except Exception as e: + print(f"Error notifying Slack: {e}") + + try: + send_login_email(request, email, 'GitHub') + except Exception as e: + print(f"Error sending email: {e}") + return self.get_provider().sociallogin_from_response(request, extra_data) @@ -128,7 +158,6 @@ class CustomGitLabOAuth2Adapter(OAuth2Adapter): provider_base_url, provider_api_version) def complete_login(self, request, app, token, response): - print('logging in') response = requests.get(self.profile_url, params={ "access_token": token.token}) data = _check_errors(response) @@ -136,9 +165,19 @@ def complete_login(self, request, app, token, response): email = login.email_addresses[0] - if CLOUD_HOSTED and not CustomUser.objects.filter(email=email).exists(): - # new user - notify_slack(f"New user signup: {email}") + if CLOUD_HOSTED: + # Check if user exists and notify Slack for new user signup + if not CustomUser.objects.filter(email=email).exists(): + try: + notify_slack(f"New user signup: {email}") + except Exception as e: + print(f"Error notifying Slack: {e}") + + try: + send_login_email(request, email, 'GitLab') + except Exception as e: + print(f"Error sending email: {e}") + return login @@ -172,8 +211,8 @@ def logout_view(request): @api_view(['GET']) @permission_classes([AllowAny]) -def health_check(request): - return JsonResponse({ +def health_check(request): + return JsonResponse({ 'status': 'alive' }) @@ -194,7 +233,8 @@ def kms(request, app_id): app = App.objects.get(app_token=app_token) try: timestamp = datetime.now().timestamp() * 1000 - KMSDBLog.objects.create(app_id=app_id, event_type=event_type, phase_node=phase_node, ph_size=float(ph_size), ip_address=ip_address, timestamp=timestamp) + KMSDBLog.objects.create(app_id=app_id, event_type=event_type, phase_node=phase_node, ph_size=float( + ph_size), ip_address=ip_address, timestamp=timestamp) except: pass return JsonResponse({ @@ -204,6 +244,282 @@ def kms(request, app_id): return HttpResponse(status=404) +def user_token_kms(request): + auth_token = request.headers['authorization'] + + token = auth_token.split(' ')[2] + + user_token = UserToken.objects.get(token=token) + + serializer = UserTokenSerializer(user_token) + + return Response(serializer.data, status=status.HTTP_200_OK) + + +def service_token_kms(request): + auth_token = request.headers['authorization'] + + token = auth_token.split(' ')[2] + + service_token = ServiceToken.objects.get(token=token) + + serializer = ServiceTokenSerializer(service_token) + + return Response(serializer.data, status=status.HTTP_200_OK) + + +@api_view(['GET']) +@permission_classes([AllowAny]) +def secrets_tokens(request): + auth_token = request.headers['authorization'] + + if token_is_expired_or_deleted(auth_token): + return HttpResponse(status=403) + + token_type = get_token_type(auth_token) + + if token_type == 'Service': + return service_token_kms(request) + elif token_type == 'User': + return user_token_kms(request) + else: + return HttpResponse(status=403) + + class PrivateGraphQLView(LoginRequiredMixin, GraphQLView): raise_exception = True pass + + +class SecretsView(APIView): + permission_classes = [AllowAny, ] + + @csrf_exempt + def dispatch(self, request, *args): + return super(SecretsView, self).dispatch(request, *args) + + def get(self, request): + auth_token = request.headers['authorization'] + + if token_is_expired_or_deleted(auth_token): + return HttpResponse(status=403) + + token_type = get_token_type(auth_token) + + env_id = request.headers['environment'] + env = Environment.objects.get(id=env_id) + + ip_address, user_agent = get_resolver_request_meta(request) + + if token_type == 'User': + try: + org_member = get_org_member_from_user_token(auth_token) + + if not user_can_access_environment(org_member.user.userId, env_id): + return HttpResponse(status=403) + except Exception as ex: + print('EX:', ex) + return HttpResponse(status=404) + + else: + org_member = None + + if not env.id: + return HttpResponse(status=404) + + secrets_filter = { + 'environment': env, + 'deleted_at': None + } + + try: + key_digest = request.headers['keydigest'] + if key_digest: + secrets_filter['key_digest'] = key_digest + except: + pass + + secrets = Secret.objects.filter(**secrets_filter) + + for secret in secrets: + read_event = SecretEvent.objects.create(secret=secret, environment=secret.environment, user=org_member, key=secret.key, key_digest=secret.key_digest, + value=secret.value, comment=secret.comment, event_type=SecretEvent.READ, ip_address=ip_address, user_agent=user_agent) + read_event.tags.set(secret.tags.all()) + + serializer = SecretSerializer(secrets, many=True) + + return Response(serializer.data, status=status.HTTP_200_OK) + + def post(self, request): + auth_token = request.headers['authorization'] + + if token_is_expired_or_deleted(auth_token): + return HttpResponse(status=403) + + token_type = get_token_type(auth_token) + + env_id = request.headers['environment'] + env = Environment.objects.get(id=env_id) + + if token_type == 'User': + try: + user = get_org_member_from_user_token(auth_token) + + if not user_can_access_environment(user.user.userId, env_id): + return HttpResponse(status=403) + except: + return HttpResponse(status=404) + else: + user = None + + if not env: + return HttpResponse(status=404) + + request_body = json.loads(request.body) + + ip_address, user_agent = get_resolver_request_meta(request) + + for secret in request_body['secrets']: + + tags = SecretTag.objects.filter( + id__in=secret['tags']) + + secret_data = { + 'environment': env, + 'key': secret['key'], + 'key_digest': secret['keyDigest'], + 'value': secret['value'], + 'folder_id': secret['folderId'], + 'version': 1, + 'comment': secret['comment'], + } + + secret_obj = Secret.objects.create(**secret_data) + secret_obj.tags.set(tags) + + event = SecretEvent.objects.create( + **{**secret_data, **{ + 'user': user, + 'secret': secret_obj, + 'event_type': SecretEvent.CREATE, + 'ip_address': ip_address, + 'user_agent': user_agent + }}) + event.tags.set(tags) + + return Response(status=status.HTTP_200_OK) + + def put(self, request): + auth_token = request.headers['authorization'] + + if token_is_expired_or_deleted(auth_token): + return HttpResponse(status=403) + + token_type = get_token_type(auth_token) + + env_id = request.headers['environment'] + env = Environment.objects.get(id=env_id) + + if token_type == 'User': + try: + user = get_org_member_from_user_token(auth_token) + + if not user_can_access_environment(user.user.userId, env_id): + return HttpResponse(status=403) + except: + return HttpResponse(status=404) + + else: + user = None + + request_body = json.loads(request.body) + + ip_address, user_agent = get_resolver_request_meta(request) + + for secret in request_body['secrets']: + secret_obj = Secret.objects.get(id=secret['id']) + + tags = SecretTag.objects.filter( + id__in=secret['tags']) + + secret_data = { + 'environment': env, + 'key': secret['key'], + 'key_digest': secret['keyDigest'], + 'value': secret['value'], + 'folder_id': secret['folderId'], + 'version': secret_obj.version + 1, + 'comment': secret['comment'], + } + + for key, value in secret_data.items(): + setattr(secret_obj, key, value) + + secret_obj.updated_at = timezone.now() + secret_obj.tags.set(tags) + secret_obj.save() + + event = SecretEvent.objects.create( + **{**secret_data, **{ + 'user': user, + 'secret': secret_obj, + 'event_type': SecretEvent.UPDATE, + 'ip_address': ip_address, + 'user_agent': user_agent + }}) + event.tags.set(tags) + + return Response(status=status.HTTP_200_OK) + + def delete(self, request): + auth_token = request.headers['authorization'] + + if token_is_expired_or_deleted(auth_token): + return HttpResponse(status=403) + + token_type = get_token_type(auth_token) + + env_id = request.headers['environment'] + + if token_type == 'User': + try: + user = get_org_member_from_user_token(auth_token) + + if not user_can_access_environment(user.user.userId, env_id): + return HttpResponse(status=403) + except: + return HttpResponse(status=404) + + else: + user = None + + request_body = json.loads(request.body) + + ip_address, user_agent = get_resolver_request_meta(request) + + secrets_to_delete = Secret.objects.filter( + id__in=request_body['secrets']) + + for secret in secrets_to_delete: + if not Secret.objects.filter(id=secret.id).exists(): + return HttpResponse(status=404) + + if user is not None and not user_can_access_environment(user.user.userId, secret.environment.id): + return HttpResponse(status=403) + + for secret in secrets_to_delete: + secret.updated_at = timezone.now() + secret.deleted_at = timezone.now() + secret.save() + + most_recent_event_copy = SecretEvent.objects.filter( + secret=secret).order_by('version').last() + + # setting the pk to None and then saving it creates a copy of the instance with updated fields + most_recent_event_copy.id = None + most_recent_event_copy.event_type = SecretEvent.DELETE + most_recent_event_copy.ip_address = ip_address + most_recent_event_copy.user_agent = user_agent + most_recent_event_copy.save() + + return Response(status=status.HTTP_200_OK) diff --git a/backend/backend/exceptions.py b/backend/backend/exceptions.py index 1b8f2263..61e138ce 100644 --- a/backend/backend/exceptions.py +++ b/backend/backend/exceptions.py @@ -6,6 +6,7 @@ def custom_exception_handler(exc, context): # Call REST framework's default exception handler first, # to get the standard error response. response = exception_handler(exc, context) + print("EXCEPTION", exc) # set 404 as default response code status_code = 404 diff --git a/backend/backend/graphene/mutations/app.py b/backend/backend/graphene/mutations/app.py new file mode 100644 index 00000000..0aa01726 --- /dev/null +++ b/backend/backend/graphene/mutations/app.py @@ -0,0 +1,181 @@ +from backend.api.kv import delete, purge +from backend.graphene.mutations.environment import EnvironmentKeyInput +from backend.graphene.utils.permissions import user_can_access_app, user_is_admin, user_is_org_member +from ee.feature_flags import allow_new_app +import graphene +from django.utils import timezone +from graphql import GraphQLError +from api.models import App, EnvironmentKey, Organisation, OrganisationMember +from backend.graphene.types import AppType +from django.conf import settings + +CLOUD_HOSTED = settings.APP_HOST == 'cloud' + + +class CreateAppMutation(graphene.Mutation): + class Arguments: + id = graphene.ID(required=True) + organisation_id = graphene.ID(required=True) + name = graphene.String(required=True) + identity_key = graphene.String(required=True) + app_token = graphene.String(required=True) + app_seed = graphene.String(required=True) + wrapped_key_share = graphene.String(required=True) + app_version = graphene.Int(required=True) + + app = graphene.Field(AppType) + + @classmethod + def mutate(cls, root, info, id, organisation_id, name, identity_key, app_token, app_seed, wrapped_key_share, app_version): + user = info.context.user + org = Organisation.objects.get(id=organisation_id) + if not user_is_org_member(user.userId, organisation_id): + raise GraphQLError("You don't have access to this organisation") + + if allow_new_app(org) == False: + raise GraphQLError( + 'You have reached the App limit for your current plan. Please upgrade your account to add more.') + + if App.objects.filter(identity_key=identity_key).exists(): + raise GraphQLError("This app already exists") + + app = App.objects.create(id=id, organisation=org, name=name, identity_key=identity_key, + app_token=app_token, app_seed=app_seed, wrapped_key_share=wrapped_key_share, app_version=app_version) + + org_member = OrganisationMember.objects.get( + organisation=org, user=info.context.user, deleted_at=None) + org_member.apps.add(app) + + admin_roles = [OrganisationMember.ADMIN, OrganisationMember.OWNER] + + org_admins = org.users.filter(role__in=admin_roles) + for admin in org_admins: + admin.apps.add(app) + + return CreateAppMutation(app=app) + + +class RotateAppKeysMutation(graphene.Mutation): + class Arguments: + id = graphene.ID(required=True) + app_token = graphene.String(required=True) + wrapped_key_share = graphene.String(required=True) + + app = graphene.Field(AppType) + + @classmethod + def mutate(cls, root, info, id, app_token, wrapped_key_share): + user = info.context.user + app = App.objects.get(id=id) + + if not user_can_access_app(user.userId, app.id): + raise GraphQLError("You don't have access to this app") + + if CLOUD_HOSTED: + # delete current keys from cloudflare KV + deleted = delete(app.app_token) + + # purge keys from cloudflare cache + purged = purge( + f"phApp:v{app.app_version}:{app.identity_key}/{app.app_token}") + + if not deleted or not purged: + raise GraphQLError( + "Failed to delete app keys. Please try again.") + + app.app_token = app_token + app.wrapped_key_share = wrapped_key_share + app.save() + + return RotateAppKeysMutation(app=app) + + +class DeleteAppMutation(graphene.Mutation): + class Arguments: + id = graphene.ID(required=True) + + app = graphene.Field(AppType) + + @classmethod + def mutate(cls, root, info, id): + user = info.context.user + app = App.objects.get(id=id) + + if not user_can_access_app(user.userId, app.id): + raise GraphQLError("You don't have access to this app") + if not user_is_admin(user.userId, app.organisation.id): + raise GraphQLError( + "You don't have permission to perform that action.") + + if CLOUD_HOSTED: + # delete current keys from cloudflare KV + deleted = delete(app.app_token) + + # purge keys from cloudflare cache + purged = purge( + f"phApp:v{app.app_version}:{app.identity_key}/{app.app_token}") + + if not deleted or not purged: + raise GraphQLError( + "Failed to delete app keys. Please try again.") + + app.wrapped_key_share = "" + app.is_deleted = True + app.deleted_at = timezone.now() + app.save() + + return DeleteAppMutation(app=app) + + +class AddAppMemberMutation(graphene.Mutation): + class Arguments: + member_id = graphene.ID() + app_id = graphene.ID() + env_keys = graphene.List(EnvironmentKeyInput) + + app = graphene.Field(AppType) + + @classmethod + def mutate(cls, root, info, member_id, app_id, env_keys): + user = info.context.user + app = App.objects.get(id=app_id) + + if not user_can_access_app(user.userId, app.id): + raise GraphQLError("You don't have access to this app") + + org_member = OrganisationMember.objects.get( + id=member_id, deleted_at=None) + + app.members.add(org_member) + for key in env_keys: + EnvironmentKey.objects.create( + environment_id=key.env_id, user_id=key.user_id, wrapped_seed=key.wrapped_seed, wrapped_salt=key.wrapped_salt, identity_key=key.identity_key) + + return AddAppMemberMutation(app=app) + + +class RemoveAppMemberMutation(graphene.Mutation): + class Arguments: + member_id = graphene.ID() + app_id = graphene.ID() + + app = graphene.Field(AppType) + + @classmethod + def mutate(cls, root, info, member_id, app_id): + user = info.context.user + app = App.objects.get(id=app_id) + + if not user_can_access_app(user.userId, app.id): + raise GraphQLError("You don't have access to this app") + + org_member = OrganisationMember.objects.get( + id=member_id, deleted_at=None) + if org_member not in app.members.all(): + raise GraphQLError("This user is not a member of this app") + else: + app.members.remove(org_member) + EnvironmentKey.objects.filter( + environment__app=app, user_id=member_id).delete() + + return RemoveAppMemberMutation(app=app) diff --git a/backend/backend/graphene/mutations/environment.py b/backend/backend/graphene/mutations/environment.py new file mode 100644 index 00000000..47e7a9ec --- /dev/null +++ b/backend/backend/graphene/mutations/environment.py @@ -0,0 +1,490 @@ +from django.utils import timezone +from api.utils import get_resolver_request_meta +from backend.graphene.utils.permissions import member_can_access_org, user_can_access_app, user_can_access_environment, user_is_org_member +import graphene +from graphql import GraphQLError +from api.models import App, Environment, EnvironmentKey, EnvironmentToken, Organisation, OrganisationMember, Secret, SecretEvent, SecretFolder, SecretTag, UserToken, ServiceToken +from backend.graphene.types import AppType, EnvironmentKeyType, EnvironmentTokenType, EnvironmentType, SecretFolderType, SecretTagType, SecretType, ServiceTokenType, UserTokenType +from datetime import datetime + + +class EnvironmentInput(graphene.InputObjectType): + app_id = graphene.ID(required=True) + name = graphene.String(required=True) + env_type = graphene.String(required=True) + wrapped_seed = graphene.String(required=True) + wrapped_salt = graphene.String(required=True) + identity_key = graphene.String(required=True) + + +class EnvironmentKeyInput(graphene.InputObjectType): + env_id = graphene.ID(required=True) + user_id = graphene.ID(required=False) + identity_key = graphene.String(required=True) + wrapped_seed = graphene.String(required=True) + wrapped_salt = graphene.String(required=True) + + +class SecretInput(graphene.InputObjectType): + env_id = graphene.ID(required=False) + folder_id = graphene.ID(required=False) + key = graphene.String(required=True) + key_digest = graphene.String(required=True) + value = graphene.String(required=True) + tags = graphene.List(graphene.String) + comment = graphene.String() + + +class CreateEnvironmentMutation(graphene.Mutation): + class Arguments: + environment_data = EnvironmentInput(required=True) + admin_keys = graphene.List(EnvironmentKeyInput) + + environment = graphene.Field(EnvironmentType) + + @classmethod + def mutate(cls, root, info, environment_data, admin_keys): + user_id = info.context.user.userId + + if not user_can_access_app(user_id, environment_data.app_id): + raise GraphQLError("You don't have access to this app") + + app = App.objects.get(id=environment_data.app_id) + + environment = Environment.objects.create(app=app, name=environment_data.name, env_type=environment_data.env_type, + identity_key=environment_data.identity_key, wrapped_seed=environment_data.wrapped_seed, wrapped_salt=environment_data.wrapped_salt) + + org_owner = OrganisationMember.objects.get( + organisation=environment.app.organisation, role=OrganisationMember.OWNER, deleted_at=None) + + EnvironmentKey.objects.create(environment=environment, user=org_owner, + identity_key=environment_data.identity_key, wrapped_seed=environment_data.wrapped_seed, wrapped_salt=environment_data.wrapped_salt) + for key in admin_keys: + EnvironmentKey.objects.create( + environment=environment, user_id=key.user_id, wrapped_seed=key.wrapped_seed, wrapped_salt=key.wrapped_salt, identity_key=key.identity_key) + + return CreateEnvironmentMutation(environment=environment) + + +class CreateEnvironmentKeyMutation(graphene.Mutation): + class Arguments: + # id = graphene.ID(required=True) + env_id = graphene.ID(required=True) + user_id = graphene.ID(required=False) + identity_key = graphene.String(required=True) + wrapped_seed = graphene.String(required=True) + wrapped_salt = graphene.String(required=True) + + environment_key = graphene.Field(EnvironmentKeyType) + + @classmethod + def mutate(cls, root, info, env_id, identity_key, wrapped_seed, wrapped_salt, user_id=None,): + + env = Environment.objects.get(id=env_id) + + # check that the user attempting the mutation has access + if not user_can_access_app(info.context.user.userId, env.app.id): + raise GraphQLError("You don't have access to this app") + + # check that the user for whom we are adding a key has access + if not user_id is not None and member_can_access_org(user_id, env.app.organisation.id): + raise GraphQLError("This user doesn't have access to this app") + + if user_id is not None: + org_member = OrganisationMember.objects.get(id=user_id) + + if EnvironmentKey.objects.filter(environment=env, user_id=org_member).exists(): + raise GraphQLError( + "This user already has access to this environment") + + environment_key = EnvironmentKey.objects.create( + environment=env, user_id=user_id, identity_key=identity_key, wrapped_seed=wrapped_seed, wrapped_salt=wrapped_salt) + + return CreateEnvironmentKeyMutation(environment_key=environment_key) + + +class UpdateMemberEnvScopeMutation(graphene.Mutation): + class Arguments: + member_id = graphene.ID() + app_id = graphene.ID() + env_keys = graphene.List(EnvironmentKeyInput) + + app = graphene.Field(AppType) + + @classmethod + def mutate(cls, root, info, member_id, app_id, env_keys): + user = info.context.user + app = App.objects.get(id=app_id) + + if not user_can_access_app(user.userId, app.id): + raise GraphQLError("You don't have access to this app") + + org_member = OrganisationMember.objects.get( + id=member_id, deleted_at=None) + if org_member not in app.members.all(): + raise GraphQLError("This user does not have access to this app") + else: + # delete all existing keys + EnvironmentKey.objects.filter( + environment__app=app, user_id=member_id).delete() + + # set new keys + for key in env_keys: + EnvironmentKey.objects.create( + environment_id=key.env_id, user_id=key.user_id, wrapped_seed=key.wrapped_seed, wrapped_salt=key.wrapped_salt, identity_key=key.identity_key) + + return UpdateMemberEnvScopeMutation(app=app) + + +class CreateEnvironmentTokenMutation(graphene.Mutation): + class Arguments: + env_id = graphene.ID(required=True) + name = graphene.String(required=True) + identity_key = graphene.String(required=True) + token = graphene.String(required=True) + wrapped_key_share = graphene.String(required=True) + + environment_token = graphene.Field(EnvironmentTokenType) + + @classmethod + def mutate(cls, root, info, env_id, name, identity_key, token, wrapped_key_share): + user = info.context.user + if user_can_access_environment(user.userId, env_id): + + env = Environment.objects.get(id=env_id) + org_member = OrganisationMember.objects.get( + organisation=env.app.organisation, user_id=user.userId, deleted_at=None) + + environment_token = EnvironmentToken.objects.create( + environment_id=env_id, user=org_member, name=name, identity_key=identity_key, token=token, wrapped_key_share=wrapped_key_share) + + return CreateEnvironmentTokenMutation(environment_token=environment_token) + + +class CreateUserTokenMutation(graphene.Mutation): + class Arguments: + org_id = graphene.ID(required=True) + name = graphene.String(required=True) + identity_key = graphene.String(required=True) + token = graphene.String(required=True) + wrapped_key_share = graphene.String(required=True) + expiry = graphene.BigInt(required=False) + + ok = graphene.Boolean() + user_token = graphene.Field(UserTokenType) + + @classmethod + def mutate(cls, root, info, org_id, name, identity_key, token, wrapped_key_share, expiry): + user = info.context.user + if user_is_org_member(user.userId, org_id): + + org_member = OrganisationMember.objects.get( + organisation_id=org_id, user_id=user.userId, deleted_at=None) + + if expiry is not None: + expires_at = datetime.fromtimestamp(expiry / 1000) + else: + expires_at = None + + user_token = UserToken.objects.create( + user=org_member, name=name, identity_key=identity_key, token=token, wrapped_key_share=wrapped_key_share, expires_at=expires_at) + + return CreateUserTokenMutation(user_token=user_token, ok=True) + + else: + raise GraphQLError( + "You don't have permission to perform this action") + + +class DeleteUserTokenMutation(graphene.Mutation): + class Arguments: + token_id = graphene.ID(required=True) + + ok = graphene.Boolean() + + @classmethod + def mutate(cls, root, info, token_id): + user = info.context.user + token = UserToken.objects.get(id=token_id) + org = token.user.organisation + + if user_is_org_member(user.userId, org.id): + token.deleted_at = timezone.now() + token.save() + + return DeleteUserTokenMutation(ok=True) + else: + raise GraphQLError( + "You don't have permission to perform this action") + + +class CreateServiceTokenMutation(graphene.Mutation): + class Arguments: + app_id = graphene.ID(required=True) + environment_keys = graphene.List(EnvironmentKeyInput) + identity_key = graphene.String(required=True) + token = graphene.String(required=True) + wrapped_key_share = graphene.String(required=True) + name = graphene.String(required=True) + expiry = graphene.BigInt(required=False) + + service_token = graphene.Field(ServiceTokenType) + + @classmethod + def mutate(cls, root, info, app_id, environment_keys, identity_key, token, wrapped_key_share, name, expiry): + user = info.context.user + app = App.objects.get(id=app_id) + + if user_is_org_member(user.userId, app.organisation.id): + + org_member = OrganisationMember.objects.get( + organisation_id=app.organisation.id, user_id=user.userId, deleted_at=None) + + env_keys = EnvironmentKey.objects.bulk_create([EnvironmentKey( + environment_id=key.env_id, identity_key=key.identity_key, wrapped_seed=key.wrapped_seed, wrapped_salt=key.wrapped_salt) for key in environment_keys]) + + if expiry is not None: + expires_at = datetime.fromtimestamp(expiry / 1000) + else: + expires_at = None + + service_token = ServiceToken.objects.create( + app=app, identity_key=identity_key, token=token, wrapped_key_share=wrapped_key_share, name=name, created_by=org_member, expires_at=expires_at) + + service_token.keys.set(env_keys) + + return CreateServiceTokenMutation(service_token=service_token) + + +class DeleteServiceTokenMutation(graphene.Mutation): + class Arguments: + token_id = graphene.ID(required=True) + + ok = graphene.Boolean() + + @classmethod + def mutate(cls, root, info, token_id): + user = info.context.user + token = ServiceToken.objects.get(id=token_id) + org = token.app.organisation + + if user_is_org_member(user.userId, org.id): + token.deleted_at = timezone.now() + token.save() + + return DeleteServiceTokenMutation(ok=True) + else: + raise GraphQLError( + "You don't have permission to perform this action") + + +class CreateSecretFolderMutation(graphene.Mutation): + class Arguments: + id = graphene.ID(required=True) + env_id = graphene.ID(required=True) + parent_folder_id = graphene.ID(required=False) + name = graphene.String(required=True) + + folder = graphene.Field(SecretFolderType) + + @classmethod + def mutate(cls, root, info, id, env_id, name, parent_folder_id=None): + user = info.context.user + if user_can_access_environment(user.id, env_id): + folder = SecretFolder.objects.create( + id=id, environment_id=env_id, parent_id=parent_folder_id, name=name) + + return CreateSecretFolderMutation(folder=folder) + + +class CreateSecretTagMutation(graphene.Mutation): + class Arguments: + org_id = graphene.ID(required=True) + name = graphene.String(required=True) + color = graphene.String(required=True) + + tag = graphene.Field(SecretTagType) + + @classmethod + def mutate(cls, root, info, org_id, name, color): + + if not user_is_org_member(info.context.user.userId, org_id): + raise GraphQLError( + "You don't have permission to perform this action") + + org = Organisation.objects.get(id=org_id) + + if SecretTag.objects.filter(organisation=org, name=name).exists(): + raise GraphQLError('This tag already exists!') + + tag = SecretTag.objects.create( + organisation=org, name=name, color=color) + + return CreateSecretTagMutation(tag=tag) + + +class CreateSecretMutation(graphene.Mutation): + class Arguments: + secret_data = SecretInput(SecretInput) + + secret = graphene.Field(SecretType) + + @classmethod + def mutate(cls, root, info, secret_data): + env = Environment.objects.get(id=secret_data.env_id) + org = env.app.organisation + if not user_is_org_member(info.context.user.userId, org.id): + raise GraphQLError( + "You don't have permission to perform this action") + + tags = SecretTag.objects.filter( + id__in=secret_data.tags) + + secret_obj_data = { + 'environment_id': env.id, + 'folder_id': secret_data.folder_id, + 'key': secret_data.key, + 'key_digest': secret_data.key_digest, + 'value': secret_data.value, + 'version': 1, + 'comment': secret_data.comment + } + + secret = Secret.objects.create(**secret_obj_data) + secret.tags.set(tags) + + ip_address, user_agent = get_resolver_request_meta(info.context) + + org_member = OrganisationMember.objects.get( + user=info.context.user, organisation=org, deleted_at=None) + + event = SecretEvent.objects.create( + **{**secret_obj_data, **{ + 'user': org_member, + 'secret': secret, + 'event_type': SecretEvent.CREATE, + 'ip_address': ip_address, + 'user_agent': user_agent + }}) + event.tags.set(tags) + + return CreateSecretMutation(secret=secret) + + +class EditSecretMutation(graphene.Mutation): + class Arguments: + id = graphene.ID(required=True) + secret_data = SecretInput(SecretInput) + + secret = graphene.Field(SecretType) + + @classmethod + def mutate(cls, root, info, id, secret_data): + secret = Secret.objects.get(id=id) + env = secret.environment + org = env.app.organisation + if not user_is_org_member(info.context.user.userId, org.id): + raise GraphQLError( + "You don't have permission to perform this action") + + tags = SecretTag.objects.filter( + id__in=secret_data.tags) + + secret_obj_data = { + 'folder_id': secret_data.folder_id, + 'key': secret_data.key, + 'key_digest': secret_data.key_digest, + 'value': secret_data.value, + 'version': secret.version + 1, + 'comment': secret_data.comment + } + + for key, value in secret_obj_data.items(): + setattr(secret, key, value) + + secret.updated_at = timezone.now() + secret.tags.set(tags) + secret.save() + + ip_address, user_agent = get_resolver_request_meta(info.context) + + org_member = OrganisationMember.objects.get( + user=info.context.user, organisation=org, deleted_at=None) + + event = SecretEvent.objects.create( + **{**secret_obj_data, **{ + 'user': org_member, + 'environment': env, + 'secret': secret, + 'event_type': SecretEvent.UPDATE, + 'ip_address': ip_address, + 'user_agent': user_agent + }}) + event.tags.set(tags) + + return EditSecretMutation(secret=secret) + + +class DeleteSecretMutation(graphene.Mutation): + class Arguments: + id = graphene.ID(required=True) + + secret = graphene.Field(SecretType) + + @classmethod + def mutate(cls, root, info, id): + secret = Secret.objects.get(id=id) + env = secret.environment + org = env.app.organisation + + if not user_is_org_member(info.context.user.userId, org.id): + raise GraphQLError( + "You don't have permission to perform this action") + + secret.updated_at = timezone.now() + secret.deleted_at = timezone.now() + secret.save() + + ip_address, user_agent = get_resolver_request_meta(info.context) + + org_member = OrganisationMember.objects.get( + user=info.context.user, organisation=org, deleted_at=None) + + most_recent_event_copy = SecretEvent.objects.filter( + secret=secret).order_by('version').last() + + # setting the pk to None and then saving it creates a copy of the instance with updated fields + most_recent_event_copy.id = None + most_recent_event_copy.event_type = SecretEvent.DELETE + most_recent_event_copy.user = org_member + most_recent_event_copy.ip_address = ip_address + most_recent_event_copy.user_agent = user_agent + most_recent_event_copy.save() + + return DeleteSecretMutation(secret=secret) + + +class ReadSecretMutation(graphene.Mutation): + class Arguments: + id = graphene.ID(required=True) + + ok = graphene.Boolean() + + @classmethod + def mutate(cls, root, info, id): + secret = Secret.objects.get(id=id) + env = secret.environment + org = env.app.organisation + if not user_is_org_member(info.context.user.userId, org.id): + raise GraphQLError( + "You don't have permission to perform this action") + else: + ip_address, user_agent = get_resolver_request_meta(info.context) + + org_member = OrganisationMember.objects.get( + user=info.context.user, organisation=org, deleted_at=None) + + read_event = SecretEvent.objects.create(secret=secret, environment=secret.environment, user=org_member, key=secret.key, key_digest=secret.key_digest, + value=secret.value, comment=secret.comment, event_type=SecretEvent.READ, ip_address=ip_address, user_agent=user_agent) + read_event.tags.set(secret.tags.all()) + return ReadSecretMutation(ok=True) diff --git a/backend/backend/graphene/mutations/organisation.py b/backend/backend/graphene/mutations/organisation.py new file mode 100644 index 00000000..7ab3cdc9 --- /dev/null +++ b/backend/backend/graphene/mutations/organisation.py @@ -0,0 +1,211 @@ +from api.emails import send_inite_email, send_user_joined_email +from backend.graphene.utils.permissions import user_is_admin, user_is_org_member +import graphene +from graphql import GraphQLError +from api.models import App, Organisation, CustomUser, OrganisationMember, OrganisationMemberInvite +from backend.graphene.types import OrganisationMemberInviteType, OrganisationMemberType, OrganisationType +from datetime import datetime, timedelta +from django.utils import timezone + + +class CreateOrganisationMutation(graphene.Mutation): + class Arguments: + id = graphene.ID(required=True) + name = graphene.String(required=True) + identity_key = graphene.String(required=True) + wrapped_keyring = graphene.String(required=True) + wrapped_recovery = graphene.String(required=True) + + organisation = graphene.Field(OrganisationType) + + @classmethod + def mutate(cls, root, info, id, name, identity_key, wrapped_keyring, wrapped_recovery): + if Organisation.objects.filter(name__iexact=name).exists(): + raise GraphQLError('This organisation name is not available.') + + owner = CustomUser.objects.get(userId=info.context.user.userId) + org = Organisation.objects.create( + id=id, name=name, identity_key=identity_key) + OrganisationMember.objects.create( + user=owner, organisation=org, role=OrganisationMember.OWNER, identity_key=identity_key, wrapped_keyring=wrapped_keyring, wrapped_recovery=wrapped_recovery) + + return CreateOrganisationMutation(organisation=org) + + +class UpdateUserWrappedSecretsMutation(graphene.Mutation): + class Arguments: + org_id = graphene.ID(required=True) + wrapped_keyring = graphene.String(required=True) + wrapped_recovery = graphene.String(required=True) + + org_member = graphene.Field(OrganisationMemberType) + + @classmethod + def mutate(cls, root, info, org_id, wrapped_keyring, wrapped_recovery): + + org_member = OrganisationMember.objects.get( + organisation_id=org_id, user=info.context.user, deleted_at=None) + + org_member.wrapped_keyring = wrapped_keyring + org_member.wrapped_recovery = wrapped_recovery + org_member.save() + + return UpdateUserWrappedSecretsMutation(org_member=org_member) + + +class InviteOrganisationMemberMutation(graphene.Mutation): + class Arguments: + org_id = graphene.ID(required=True) + email = graphene.String(required=True) + apps = graphene.List(graphene.String) + role = graphene.String() + + invite = graphene.Field(OrganisationMemberInviteType) + + @classmethod + def mutate(cls, root, info, org_id, email, apps, role): + if user_is_org_member(info.context.user, org_id): + user_already_exists = OrganisationMember.objects.filter( + organisation_id=org_id, user__email=email, deleted_at=None).exists() + if user_already_exists: + raise GraphQLError( + "This user is already a member if your organisation") + + if OrganisationMemberInvite.objects.filter(organisation_id=org_id, invitee_email=email, valid=True, expires_at__gte=timezone.now()).exists(): + raise GraphQLError( + "An active invitiation already exists for this user.") + + invited_by = OrganisationMember.objects.get( + user=info.context.user, organisation_id=org_id, deleted_at=None) + + expiry = datetime.now() + timedelta(days=3) + + app_scope = App.objects.filter(id__in=apps) + + invite = OrganisationMemberInvite.objects.create( + organisation_id=org_id, invited_by=invited_by, role=role.lower(), invitee_email=email, expires_at=expiry) + + invite.apps.set(app_scope) + + try: + send_inite_email(invite) + except Exception as e: + print(f"Error sending invite email: {e}") + + return InviteOrganisationMemberMutation(invite=invite) + else: + raise GraphQLError( + "You don't have permission to perform this action") + + +class DeleteInviteMutation(graphene.Mutation): + class Arguments: + invite_id = graphene.ID(required=True) + + ok = graphene.Boolean() + + @classmethod + def mutate(cls, rooot, info, invite_id): + invite = OrganisationMemberInvite.objects.get(id=invite_id) + + if user_is_org_member(info.context.user, invite.organisation.id): + invite.delete() + + return DeleteInviteMutation(ok=True) + + else: + raise GraphQLError( + "You don't have permission to perform this action") + + +class CreateOrganisationMemberMutation(graphene.Mutation): + class Arguments: + org_id = graphene.ID(required=True) + identity_key = graphene.String(required=True) + wrapped_keyring = graphene.String(required=False) + wrapped_recovery = graphene.String(required=False) + invite_id = graphene.ID(required=True) + + org_member = graphene.Field(OrganisationMemberType) + + @classmethod + def mutate(cls, root, info, org_id, identity_key, wrapped_keyring, wrapped_recovery, invite_id): + if user_is_org_member(info.context.user.userId, org_id): + raise GraphQLError( + "You are already a member of this organisation") + + if OrganisationMemberInvite.objects.filter(id=invite_id, valid=True, expires_at__gte=timezone.now()).exists(): + + invite = OrganisationMemberInvite.objects.get( + id=invite_id, valid=True, expires_at__gte=timezone.now()) + + org = Organisation.objects.get(id=org_id) + + org_member = OrganisationMember.objects.create( + user_id=info.context.user.userId, organisation=org, role=invite.role, identity_key=identity_key, wrapped_keyring=wrapped_keyring, wrapped_recovery=wrapped_recovery) + + org_member.apps.set(invite.apps.all()) # broken + + invite.valid = False + invite.save() + + try: + send_user_joined_email(invite, org_member) + except Exception as e: + print(f"Error sending new user joined email: {e}") + + return CreateOrganisationMemberMutation(org_member=org_member) + else: + raise GraphQLError( + "You need a valid invite to join this organisation") + + +class DeleteOrganisationMemberMutation(graphene.Mutation): + class Arguments: + member_id = graphene.ID(required=True) + + ok = graphene.Boolean() + + @classmethod + def mutate(cls, root, info, member_id): + org_member = OrganisationMember.objects.get( + id=member_id, deleted_at=None) + + if org_member.user == info.context.user: + raise GraphQLError( + "You can't remove yourself from an organisation") + + if user_is_admin(info.context.user.userId, org_member.organisation.id): + org_member.delete() + + return DeleteOrganisationMemberMutation(ok=True) + else: + raise GraphQLError( + "You don't have permission to perform that action") + + +class UpdateOrganisationMemberRole(graphene.Mutation): + class Arguments: + member_id = graphene.ID(required=True) + role = graphene.String(required=True) + + org_member = graphene.Field(OrganisationMemberType) + + @classmethod + def mutate(cls, root, info, member_id, role): + + org_member = OrganisationMember.objects.get( + id=member_id, deleted_at=None) + + if user_is_admin(info.context.user.userId, org_member.organisation.id): + if role.lower() == OrganisationMember.OWNER.lower(): + raise GraphQLError( + 'You cannot set this user as the organisation owner') + + org_member.role = role.lower() + org_member.save() + + return UpdateOrganisationMemberRole(org_member=org_member) + else: + raise GraphQLError( + "You don't have permission to perform this action") diff --git a/backend/backend/graphene/types.py b/backend/backend/graphene/types.py new file mode 100644 index 00000000..2eb1d2cc --- /dev/null +++ b/backend/backend/graphene/types.py @@ -0,0 +1,222 @@ +import graphene +from enum import Enum +from graphene import ObjectType, relay +from graphene_django import DjangoObjectType +from api.models import CustomUser, Environment, EnvironmentKey, EnvironmentToken, Organisation, App, OrganisationMember, OrganisationMemberInvite, Secret, SecretEvent, SecretFolder, SecretTag, ServiceToken, UserToken +from logs.dynamodb_models import KMSLog +from allauth.socialaccount.models import SocialAccount + + +class OrganisationType(DjangoObjectType): + role = graphene.String() + member_id = graphene.ID() + keyring = graphene.String() + recovery = graphene.String() + + class Meta: + model = Organisation + fields = ('id', 'name', 'identity_key', + 'created_at', 'plan', 'role', 'member_id', 'keyring', 'recovery') + + def resolve_role(self, info): + org_member = OrganisationMember.objects.get( + user=info.context.user, organisation=self, deleted_at=None) + return org_member.role + + def resolve_member_id(self, info): + org_member = OrganisationMember.objects.get( + user=info.context.user, organisation=self, deleted_at=None) + return org_member.id + + def resolve_keyring(self, info): + org_member = OrganisationMember.objects.get( + user=info.context.user, organisation=self, deleted_at=None) + return org_member.wrapped_keyring + + def resolve_recovery(self, info): + org_member = OrganisationMember.objects.get( + user=info.context.user, organisation=self, deleted_at=None) + return org_member.wrapped_recovery + + def resolve_identity_key(self, info): + org_member = OrganisationMember.objects.get( + user=info.context.user, organisation=self, deleted_at=None) + return org_member.identity_key + + +class OrganisationMemberType(DjangoObjectType): + email = graphene.String() + username = graphene.String() + full_name = graphene.String() + avatar_url = graphene.String() + self = graphene.Boolean() + + class Meta: + model = OrganisationMember + fields = ('id', 'email', 'username', 'full_name', 'avatar_url', 'role', + 'identity_key', 'wrapped_keyring', 'created_at', 'updated_at') + + def resolve_email(self, info): + return self.user.email + + def resolve_username(self, info): + return self.user.username + + def resolve_full_name(self, info): + social_acc = self.user.socialaccount_set.first() + if social_acc: + return social_acc.extra_data.get('name') + return None + + def resolve_avatar_url(self, info): + social_acc = self.user.socialaccount_set.first() + if social_acc: + if social_acc.provider == 'google': + return social_acc.extra_data.get('picture') + return social_acc.extra_data.get('avatar_url') + return None + + def resolve_self(self, info): + return self.user == info.context.user + + +class OrganisationMemberInviteType(DjangoObjectType): + class Meta: + model = OrganisationMemberInvite + fields = ('id', 'invited_by', 'invitee_email', 'valid', 'organisation', 'apps', 'role', + 'created_at', 'updated_at', 'expires_at') + + +class AppType(DjangoObjectType): + class Meta: + model = App + fields = ('id', 'name', 'identity_key', + 'wrapped_key_share', 'created_at', 'app_token', 'app_seed', 'app_version') + + +class EnvironmentType(DjangoObjectType): + class Meta: + model = Environment + fields = ('id', 'name', 'env_type', 'identity_key', + 'wrapped_seed', 'wrapped_salt', 'created_at', 'updated_at') + + def resolve_wrapped_seed(self, info): + org_member = OrganisationMember.objects.get( + user=info.context.user, organisation=self.app.organisation, deleted_at=None) + user_env_key = EnvironmentKey.objects.get( + environment=self, user=org_member, deleted_at=None) + + return user_env_key.wrapped_seed + + def resolve_wrapped_salt(self, info): + org_member = OrganisationMember.objects.get( + user=info.context.user, organisation=self.app.organisation, deleted_at=None) + user_env_key = EnvironmentKey.objects.get( + environment=self, user=org_member, deleted_at=None) + + return user_env_key.wrapped_salt + + +class EnvironmentKeyType(DjangoObjectType): + class Meta: + model = EnvironmentKey + fields = ('id', 'identity_key', 'wrapped_seed', + 'wrapped_salt', 'created_at', 'updated_at', 'environment') + + +class EnvironmentTokenType(DjangoObjectType): + class Meta: + model = EnvironmentToken + fields = ('id', 'name', 'identity_key', 'token', + 'wrapped_key_share', 'created_at', 'updated_at') + + +class UserTokenType(DjangoObjectType): + class Meta: + model = UserToken + fields = ('id', 'name', 'identity_key', 'token', + 'wrapped_key_share', 'created_at', 'updated_at', 'expires_at') + + +class ServiceTokenType(DjangoObjectType): + class Meta: + model = ServiceToken + fields = ('id', 'keys', 'identity_key', + 'token', 'wrapped_key_share', 'name', 'created_by', 'created_at', 'updated_at', 'expires_at') + + +class SecretFolderType(DjangoObjectType): + class Meta: + model = SecretFolder + fields = ('id', 'environment_id', 'parent_folder_id', + 'name', 'created_at', 'updated_at') + + +class SecretTagType(DjangoObjectType): + class Meta: + model = SecretTag + fields = ('id', 'name', 'color') + + +class SecretEventType(DjangoObjectType): + class Meta: + model = SecretEvent + fields = ('id', 'secret', 'key', 'value', + 'version', 'tags', 'comment', 'event_type', 'timestamp', 'user', 'ip_address', 'user_agent', 'environment') + + +class SecretType(DjangoObjectType): + + history = graphene.List(SecretEventType) + + class Meta: + model = Secret + fields = ('id', 'key', 'value', 'folder', 'version', 'tags', + 'comment', 'created_at', 'updated_at', 'history') + # interfaces = (relay.Node, ) + + def resolve_history(self, info): + return SecretEvent.objects.filter(secret_id=self.id).order_by('timestamp') + + +class KMSLogType(ObjectType): + class Meta: + model = KMSLog + fields = ('id', 'app_id', 'timestamp', 'phase_node', + 'event_type', 'ip_address', 'ph_size', 'edge_location', 'country', 'city', 'latitude', 'longitude') + interfaces = (relay.Node, ) + + id = graphene.ID(required=True) + timestamp = graphene.BigInt() + app_id = graphene.String() + phase_node = graphene.String() + event_type = graphene.String() + ip_address = graphene.String() + ph_size = graphene.Int() + asn = graphene.Int() + isp = graphene.String() + edge_location = graphene.String() + country = graphene.String() + city = graphene.String() + latitude = graphene.Float() + longitude = graphene.Float() + + +class ChartDataPointType(graphene.ObjectType): + index = graphene.Int() + date = graphene.BigInt() + data = graphene.Int() + + +class TimeRange(Enum): + HOUR = 'hour' + DAY = 'day' + WEEK = 'week' + MONTH = 'month' + YEAR = 'year' + ALL_TIME = 'allTime' + + +class LogsResponseType(ObjectType): + kms = graphene.List(KMSLogType) + secrets = graphene.List(SecretEventType) diff --git a/backend/backend/graphene/utils/permissions.py b/backend/backend/graphene/utils/permissions.py new file mode 100644 index 00000000..12a8a43a --- /dev/null +++ b/backend/backend/graphene/utils/permissions.py @@ -0,0 +1,31 @@ +from api.models import App, Environment, EnvironmentKey, Organisation, OrganisationMember + +admin_roles = [OrganisationMember.OWNER, OrganisationMember.ADMIN] + + +def user_is_admin(user_id, org_id): + member = OrganisationMember.objects.get( + user_id=user_id, organisation_id=org_id, deleted_at=None) + return member.role in admin_roles + + +def user_is_org_member(user_id, org_id): + return OrganisationMember.objects.filter(user_id=user_id, organisation_id=org_id, deleted_at=None).exists() + + +def user_can_access_app(user_id, app_id): + app = App.objects.get(id=app_id) + org_member = OrganisationMember.objects.get( + user_id=user_id, organisation=app.organisation, deleted_at=None) + return org_member in app.members.all() + + +def user_can_access_environment(user_id, env_id): + env = Environment.objects.get(id=env_id) + org_member = OrganisationMember.objects.get( + organisation=env.app.organisation, user_id=user_id, deleted_at=None) + return EnvironmentKey.objects.filter(user_id=org_member, environment_id=env_id).exists() + + +def member_can_access_org(member_id, org_id): + return OrganisationMember.objects.filter(id=member_id, organisation_id=org_id, deleted_at=None).exists() diff --git a/backend/backend/schema.py b/backend/backend/schema.py index cc17a572..e7f16695 100644 --- a/backend/backend/schema.py +++ b/backend/backend/schema.py @@ -1,246 +1,295 @@ -from enum import Enum +from .graphene.mutations.environment import CreateEnvironmentKeyMutation, CreateEnvironmentMutation, CreateEnvironmentTokenMutation, CreateSecretFolderMutation, CreateSecretMutation, CreateSecretTagMutation, CreateServiceTokenMutation, CreateUserTokenMutation, DeleteSecretMutation, DeleteServiceTokenMutation, DeleteUserTokenMutation, EditSecretMutation, ReadSecretMutation, UpdateMemberEnvScopeMutation +from .graphene.utils.permissions import user_can_access_app, user_can_access_environment, user_is_admin, user_is_org_member +from .graphene.mutations.app import AddAppMemberMutation, CreateAppMutation, DeleteAppMutation, RemoveAppMemberMutation, RotateAppKeysMutation +from .graphene.mutations.organisation import CreateOrganisationMemberMutation, CreateOrganisationMutation, DeleteInviteMutation, DeleteOrganisationMemberMutation, InviteOrganisationMemberMutation, UpdateOrganisationMemberRole, UpdateUserWrappedSecretsMutation +from .graphene.types import AppType, ChartDataPointType, EnvironmentKeyType, EnvironmentTokenType, EnvironmentType, KMSLogType, LogsResponseType, OrganisationMemberInviteType, OrganisationMemberType, OrganisationType, SecretEventType, SecretTagType, SecretType, ServiceTokenType, TimeRange, UserTokenType import graphene -from django.utils import timezone -from graphene import ObjectType, relay -from graphene_django import DjangoObjectType from graphql import GraphQLError -from api.models import CustomUser, Organisation, App -from backend.api.kv import delete, purge -from ee.feature_flags import allow_new_app -from logs.dynamodb_models import KMSLog +from api.models import Environment, EnvironmentKey, EnvironmentToken, Organisation, App, OrganisationMember, OrganisationMemberInvite, Secret, SecretEvent, SecretTag, ServiceToken, UserToken from logs.queries import get_app_log_count, get_app_log_count_range, get_app_logs from datetime import datetime, timedelta from django.conf import settings from logs.models import KMSDBLog +from itertools import chain +from django.utils import timezone CLOUD_HOSTED = settings.APP_HOST == 'cloud' -class OrganisationType(DjangoObjectType): - class Meta: - model = Organisation - fields = ('id', 'name', 'identity_key', 'created_at', 'plan') - - -class AppType(DjangoObjectType): - class Meta: - model = App - fields = ('id', 'name', 'identity_key', - 'wrapped_key_share', 'created_at', 'app_token', 'app_seed', 'app_version') - - -class KMSLogType(ObjectType): - class Meta: - model = KMSLog - fields = ('id', 'app_id', 'timestamp', 'phase_node', - 'event_type', 'ip_address', 'ph_size', 'edge_location', 'country', 'city', 'latitude', 'longitude') - interfaces = (relay.Node, ) - - id = graphene.ID(required=True) - timestamp = graphene.BigInt() - app_id = graphene.String() - phase_node = graphene.String() - event_type = graphene.String() - ip_address = graphene.String() - ph_size = graphene.Int() - asn = graphene.Int() - isp = graphene.String() - edge_location = graphene.String() - country = graphene.String() - city = graphene.String() - latitude = graphene.Float() - longitude = graphene.Float() - - -class ChartDataPointType(graphene.ObjectType): - index = graphene.Int() - date = graphene.BigInt() - data = graphene.Int() - - -class TimeRange(Enum): - HOUR = 'hour' - DAY = 'day' - WEEK = 'week' - MONTH = 'month' - YEAR = 'year' - ALL_TIME = 'allTime' - - -class CreateOrganisationMutation(graphene.Mutation): - class Arguments: - id = graphene.ID(required=True) - name = graphene.String(required=True) - identity_key = graphene.String(required=True) - - organisation = graphene.Field(OrganisationType) - - @classmethod - def mutate(cls, root, info, id, name, identity_key): - if Organisation.objects.filter(name__iexact=name).exists(): - raise GraphQLError('This organisation name is not available.') - if Organisation.objects.filter(owner__userId=info.context.user.userId).exists(): - raise GraphQLError( - 'Your current plan only supports one organisation.') - - owner = CustomUser.objects.get(userId=info.context.user.userId) - org = Organisation.objects.create( - id=id, name=name, identity_key=identity_key, owner=owner) - - return CreateOrganisationMutation(organisation=org) - - -class CreateAppMutation(graphene.Mutation): - class Arguments: - id = graphene.ID(required=True) - organisation_id = graphene.ID(required=True) - name = graphene.String(required=True) - identity_key = graphene.String(required=True) - app_token = graphene.String(required=True) - app_seed = graphene.String(required=True) - wrapped_key_share = graphene.String(required=True) - app_version = graphene.Int(required=True) - - app = graphene.Field(AppType) - - @classmethod - def mutate(cls, root, info, id, organisation_id, name, identity_key, app_token, app_seed, wrapped_key_share, app_version): - owner = info.context.user - org = Organisation.objects.get(id=organisation_id) - if not Organisation.objects.filter(id=organisation_id, owner__userId=owner.userId).exists(): - raise GraphQLError("You don't have access to this organisation") - - if allow_new_app(org) == False: - raise GraphQLError( - 'You have reached the App limit for your current plan. Please upgrade your account to add more.') - if App.objects.filter(identity_key=identity_key).exists(): - raise GraphQLError("This app already exists") - - app = App.objects.create(id=id, organisation=org, name=name, identity_key=identity_key, - app_token=app_token, app_seed=app_seed, wrapped_key_share=wrapped_key_share, app_version=app_version) +class Query(graphene.ObjectType): + organisations = graphene.List(OrganisationType) + organisation_members = graphene.List(OrganisationMemberType, organisation_id=graphene.ID( + ), user_id=graphene.ID(), role=graphene.List(graphene.String)) + organisation_admins_and_self = graphene.List( + OrganisationMemberType, organisation_id=graphene.ID()) + organisation_invites = graphene.List( + OrganisationMemberInviteType, org_id=graphene.ID()) + validate_invite = graphene.Field( + OrganisationMemberInviteType, invite_id=graphene.ID()) + apps = graphene.List( + AppType, organisation_id=graphene.ID(), app_id=graphene.ID()) - return CreateAppMutation(app=app) + logs = graphene.Field(LogsResponseType, app_id=graphene.ID(), + start=graphene.BigInt(), end=graphene.BigInt()) + kms_logs_count = graphene.Int(app_id=graphene.ID(), + this_month=graphene.Boolean()) -class RotateAppKeysMutation(graphene.Mutation): - class Arguments: - id = graphene.ID(required=True) - app_token = graphene.String(required=True) - wrapped_key_share = graphene.String(required=True) + secrets_logs_count = graphene.Int(app_id=graphene.ID()) - app = graphene.Field(AppType) + app_activity_chart = graphene.List(ChartDataPointType, app_id=graphene.ID( + ), period=graphene.Argument(graphene.Enum.from_enum(TimeRange))) - @classmethod - def mutate(cls, root, info, id, app_token, wrapped_key_share): - owner = info.context.user - org = Organisation.objects.filter( - owner__userId=owner.userId).first() - app = App.objects.get(id=id) - if not app.organisation.id == org.id: - raise GraphQLError("You don't have access to this app") + app_environments = graphene.List(EnvironmentType, app_id=graphene.ID( + ), environment_id=graphene.ID(required=False), member_id=graphene.ID(required=False)) + app_users = graphene.List(OrganisationMemberType, app_id=graphene.ID()) + secrets = graphene.List(SecretType, env_id=graphene.ID()) + secret_history = graphene.List(SecretEventType, secret_id=graphene.ID()) + secret_tags = graphene.List(SecretTagType, org_id=graphene.ID()) + environment_keys = graphene.List( + EnvironmentKeyType, app_id=graphene.ID(required=False), environment_id=graphene.ID(required=False), member_id=graphene.ID(required=False)) + environment_tokens = graphene.List( + EnvironmentTokenType, environment_id=graphene.ID()) + user_tokens = graphene.List(UserTokenType, organisation_id=graphene.ID()) + service_tokens = graphene.List(ServiceTokenType, app_id=graphene.ID()) - if CLOUD_HOSTED: - # delete current keys from cloudflare KV - deleted = delete(app.app_token) + def resolve_organisations(root, info): + memberships = OrganisationMember.objects.filter( + user=info.context.user, deleted_at=None) - # purge keys from cloudflare cache - purged = purge( - f"phApp:v{app.app_version}:{app.identity_key}/{app.app_token}") + return [membership.organisation for membership in memberships] - if not deleted or not purged: - raise GraphQLError("Failed to delete app keys. Please try again.") + def resolve_organisation_members(root, info, organisation_id, role, user_id=None): + if not user_is_org_member(info.context.user.userId, organisation_id): + raise GraphQLError("You don't have access to this organisation") - app.app_token = app_token - app.wrapped_key_share = wrapped_key_share - app.save() + filter = { + "organisation_id": organisation_id, + "deleted_at": None + } - return RotateAppKeysMutation(app=app) + if role: + roles = [user_role.lower() for user_role in role] + filter["roles__in"] = roles + return OrganisationMember.objects.filter(**filter) -class DeleteAppMutation(graphene.Mutation): - class Arguments: - id = graphene.ID(required=True) + def resolve_organisation_admins_and_self(root, info, organisation_id): + if not user_is_org_member(info.context.user.userId, organisation_id): + raise GraphQLError("You don't have access to this organisation") - app = graphene.Field(AppType) + roles = ['owner', 'admin'] - @classmethod - def mutate(cls, root, info, id): - owner = info.context.user - org = Organisation.objects.filter( - owner__userId=owner.userId).first() - app = App.objects.get(id=id) - if not app.organisation.id == org.id: - raise GraphQLError("You don't have access to this app") + members = OrganisationMember.objects.filter( + organisation_id=organisation_id, role__in=roles, deleted_at=None) - if CLOUD_HOSTED: - # delete current keys from cloudflare KV - deleted = delete(app.app_token) + if not info.context.user.userId in [member.user_id for member in members]: + self_member = OrganisationMember.objects.filter( + organisation_id=organisation_id, user_id=info.context.user.userId, deleted_at=None) + members = list(chain(members, self_member)) - # purge keys from cloudflare cache - purged = purge( - f"phApp:v{app.app_version}:{app.identity_key}/{app.app_token}") + return members - if not deleted or not purged: - raise GraphQLError("Failed to delete app keys. Please try again.") + def resolve_organisation_invites(root, info, org_id): + if not user_is_org_member(info.context.user.userId, org_id): + raise GraphQLError("You don't have access to this organisation") - app.wrapped_key_share = "" - app.is_deleted = True - app.deleted_at = timezone.now() - app.save() + invites = OrganisationMemberInvite.objects.filter( + organisation_id=org_id, valid=True) - return DeleteAppMutation(app=app) + return invites + def resolve_validate_invite(root, info, invite_id): + try: + invite = OrganisationMemberInvite.objects.get( + id=invite_id, valid=True) + except: + raise GraphQLError("This invite is invalid") -class Query(graphene.ObjectType): - organisations = graphene.List(OrganisationType) - apps = graphene.List( - AppType, organisation_id=graphene.ID(), app_id=graphene.ID()) - logs = graphene.List(KMSLogType, app_id=graphene.ID(), + if invite.expires_at < timezone.now(): + raise GraphQLError("This invite has expired") - start=graphene.BigInt(), end=graphene.BigInt()) - logs_count = graphene.Int(app_id=graphene.ID(), - this_month=graphene.Boolean()) - - app_activity_chart = graphene.List(ChartDataPointType, app_id=graphene.ID( - ), period=graphene.Argument(graphene.Enum.from_enum(TimeRange))) - - def resolve_organisations(root, info): - return Organisation.objects.filter(owner__userId=info.context.user.userId) + if invite.invitee_email == info.context.user.email: + return invite + else: + raise GraphQLError("This invite is for another user") def resolve_apps(root, info, organisation_id, app_id): + org_member = OrganisationMember.objects.get( + organisation_id=organisation_id, user_id=info.context.user.userId, deleted_at=None) + filter = { 'organisation_id': organisation_id, + 'id__in': org_member.apps.all(), 'is_deleted': False } + if app_id != '': filter['id'] = app_id return App.objects.filter(**filter) - def resolve_logs(root, info, app_id, start=0, end=0): - owner = info.context.user - org = Organisation.objects.filter( - owner__userId=owner.userId).first() + def resolve_app_environments(root, info, app_id, environment_id, member_id=None): + if not user_can_access_app(info.context.user.userId, app_id): + raise GraphQLError("You don't have access to this app") + + app = App.objects.get(id=app_id) + + if member_id is not None: + org_member = OrganisationMember.objects.get(id=member_id) + else: + org_member = OrganisationMember.objects.get( + organisation=app.organisation, user_id=info.context.user.userId, deleted_at=None) + + filter = { + 'app_id': app_id + } + + if environment_id: + filter['id'] = environment_id + + app_environments = Environment.objects.filter(**filter) + return [app_env for app_env in app_environments if EnvironmentKey.objects.filter(user=org_member, environment_id=app_env.id).exists()] + + def resolve_app_users(root, info, app_id): + if not user_can_access_app(info.context.user.userId, app_id): + raise GraphQLError("You don't have access to this app") + + app = App.objects.get(id=app_id) + return app.members.filter(deleted_at=None) + + def resolve_secrets(root, info, env_id): + if not user_can_access_environment(info.context.user.userId, env_id): + raise GraphQLError("You don't have access to this environment") + + return Secret.objects.filter(environment_id=env_id, deleted_at=None).order_by('created_at') + + def resolve_secret_history(root, info, secret_id): + secret = Secret.objects.get(id=secret_id) + if not user_can_access_environment(info.context.user.userId, secret.environment.id): + raise GraphQLError("You don't have access to this secret") + return SecretEvent.objects.filter(secret_id=secret_id) + + def resolve_secret_tags(root, info, org_id): + if not user_is_org_member(info.context.user.userId, org_id): + raise GraphQLError("You don't have access to this Organisation") + + return SecretTag.objects.filter(organisation_id=org_id) + + def resolve_environment_keys(root, info, app_id=None, environment_id=None, member_id=None): + if app_id is None and environment_id is None: + return None + elif app_id is not None: + app = App.objects.get(id=app_id) + else: + app = Environment.objects.get(id=environment_id).app + + if not user_can_access_app(info.context.user.userId, app.id): + raise GraphQLError("You don't have access to this app") + + filter = { + 'environment__app': app, + 'deleted_at': None + } + + if environment_id: + filter['environment_id'] = environment_id + + if member_id is not None: + org_member = OrganisationMember.objects.get( + id=member_id, deleted_at=None) + else: + org_member = OrganisationMember.objects.get( + user=info.context.user, organisation=app.organisation, deleted_at=None) + + filter['user'] = org_member + + return EnvironmentKey.objects.filter(**filter) + + def resolve_environment_tokens(root, info, environment_id): + if not user_can_access_environment(info.context.user.userId, environment_id): + raise GraphQLError("You don't have access to this secret") + + env = Environment.objects.get(id=environment_id) + org_member = OrganisationMember.objects.get( + user=info.context.user, organisation=env.app.organisation, deleted_at=None) + return EnvironmentToken.objects.filter(environment=env, user=org_member) + + def resolve_user_tokens(root, info, organisation_id): + if not user_is_org_member(info.context.user.userId, organisation_id): + raise GraphQLError("You don't have access to this organisation") + + org_member = OrganisationMember.objects.get( + user=info.context.user, organisation_id=organisation_id, deleted_at=None) + return UserToken.objects.filter(user=org_member, deleted_at=None) + + def resolve_service_tokens(root, info, app_id): app = App.objects.get(id=app_id) - if not app.organisation.id == org.id: + if not user_is_org_member(info.context.user.userId, app.organisation.id): + raise GraphQLError("You don't have access to this organisation") + + return ServiceToken.objects.filter(app=app, deleted_at=None) + + def resolve_logs(root, info, app_id, start=0, end=0): + if not user_can_access_app(info.context.user.userId, app_id): raise GraphQLError("You don't have access to this app") + + app = App.objects.get(id=app_id) + if end == 0: end = datetime.now().timestamp() * 1000 + if CLOUD_HOSTED: - return get_app_logs(f"phApp:v{app.app_version}:{app.identity_key}", start, end, 25) - logs = KMSDBLog.objects.filter(app_id=f"phApp:v{app.app_version}:{app.identity_key}",timestamp__lte=end, timestamp__gte=start).order_by('-timestamp')[:25] - return list(logs.values()) - - def resolve_logs_count(root, info, app_id): - owner = info.context.user - org = Organisation.objects.filter( - owner__userId=owner.userId).first() - app = App.objects.get(id=app_id) - if not app.organisation.id == org.id: + kms_logs = get_app_logs( + f"phApp:v{app.app_version}:{app.identity_key}", start, end, 25) + + else: + kms_logs = list(KMSDBLog.objects.filter( + app_id=f"phApp:v{app.app_version}:{app.identity_key}", timestamp__lte=end, timestamp__gte=start).order_by('-timestamp')[:25].values()) + + org_member = OrganisationMember.objects.get( + user=info.context.user, organisation=app.organisation, deleted_at=None) + + env_keys = EnvironmentKey.objects.filter( + environment__app=app, user=org_member, deleted_at=None + ).select_related('environment') + + envs = [env_key.environment for env_key in env_keys] + + start_dt = datetime.fromtimestamp(start / 1000) + end_dt = datetime.fromtimestamp(end / 1000) + + secret_events = SecretEvent.objects.filter( + environment__in=envs, timestamp__lte=end_dt, timestamp__gte=start_dt).order_by('-timestamp')[:25] + + return LogsResponseType(kms=kms_logs, secrets=secret_events) + + def resolve_kms_logs_count(root, info, app_id): + if not user_can_access_app(info.context.user.userId, app_id): raise GraphQLError("You don't have access to this app") + + app = App.objects.get(id=app_id) + if CLOUD_HOSTED: return get_app_log_count(f"phApp:v{app.app_version}:{app.identity_key}") return KMSDBLog.objects.filter(app_id=f"phApp:v{app.app_version}:{app.identity_key}").count() + def resolve_secrets_logs_count(root, info, app_id): + if not user_can_access_app(info.context.user.userId, app_id): + raise GraphQLError("You don't have access to this app") + + app = App.objects.get(id=app_id) + + org_member = OrganisationMember.objects.get( + user=info.context.user, organisation=app.organisation, deleted_at=None) + + env_keys = EnvironmentKey.objects.filter( + environment__app=app, user=org_member, deleted_at=None + ).select_related('environment') + + envs = [env_key.environment for env_key in env_keys] + + return SecretEvent.objects.filter(environment__in=envs).count() + def resolve_app_activity_chart(root, info, app_id, period=TimeRange.DAY): """ Converts app log activity for the chosen time period into time series data that can be used to draw a chart @@ -255,11 +304,9 @@ def resolve_app_activity_chart(root, info, app_id, period=TimeRange.DAY): Returns: List[ChartDataPointType]: Time series decrypt count data """ - owner = info.context.user - org = Organisation.objects.filter( - owner__userId=owner.userId).first() + app = App.objects.get(id=app_id) - if not app.organisation.id == org.id: + if not user_can_access_app(info.context.user.userId, app_id): raise GraphQLError("You don't have access to this app") end_date = datetime.now() # current time @@ -316,7 +363,8 @@ def resolve_app_activity_chart(root, info, app_id, period=TimeRange.DAY): decrypts = get_app_log_count_range( f"phApp:v{app.app_version}:{app.identity_key}", start_unix, end_unix) else: - decrypts = KMSDBLog.objects.filter(app_id=f"phApp:v{app.app_version}:{app.identity_key}",timestamp__lte=end_unix, timestamp__gte=start_unix).count() + decrypts = KMSDBLog.objects.filter( + app_id=f"phApp:v{app.app_version}:{app.identity_key}", timestamp__lte=end_unix, timestamp__gte=start_unix).count() time_series_logs.append(ChartDataPointType( index=str(index), date=end_unix, data=decrypts)) @@ -330,9 +378,37 @@ def resolve_app_activity_chart(root, info, app_id, period=TimeRange.DAY): class Mutation(graphene.ObjectType): create_organisation = CreateOrganisationMutation.Field() + invite_organisation_member = InviteOrganisationMemberMutation.Field() + create_organisation_member = CreateOrganisationMemberMutation.Field() + delete_organisation_member = DeleteOrganisationMemberMutation.Field() + update_organisation_member_role = UpdateOrganisationMemberRole.Field() + update_member_wrapped_secrets = UpdateUserWrappedSecretsMutation.Field() + + delete_invitation = DeleteInviteMutation.Field() + create_app = CreateAppMutation.Field() rotate_app_keys = RotateAppKeysMutation.Field() delete_app = DeleteAppMutation.Field() + add_app_member = AddAppMemberMutation.Field() + remove_app_member = RemoveAppMemberMutation.Field() + update_member_environment_scope = UpdateMemberEnvScopeMutation.Field() + + create_environment = CreateEnvironmentMutation.Field() + create_environment_key = CreateEnvironmentKeyMutation.Field() + create_environment_token = CreateEnvironmentTokenMutation.Field() + + create_user_token = CreateUserTokenMutation.Field() + delete_user_token = DeleteUserTokenMutation.Field() + + create_service_token = CreateServiceTokenMutation.Field() + delete_service_token = DeleteServiceTokenMutation.Field() + + create_secret_folder = CreateSecretFolderMutation.Field() + create_secret_tag = CreateSecretTagMutation.Field() + create_secret = CreateSecretMutation.Field() + edit_secret = EditSecretMutation.Field() + delete_secret = DeleteSecretMutation.Field() + read_secret = ReadSecretMutation.Field() schema = graphene.Schema(query=Query, mutation=Mutation) diff --git a/backend/backend/settings.py b/backend/backend/settings.py index 32125793..db939fea 100644 --- a/backend/backend/settings.py +++ b/backend/backend/settings.py @@ -121,6 +121,17 @@ OAUTH_REDIRECT_URI = os.getenv('OAUTH_REDIRECT_URI') + +# Email configurations +EMAIL_BACKEND = 'django.core.mail.backends.smtp.EmailBackend' +EMAIL_HOST = os.getenv('SMTP_SERVER') +EMAIL_PORT = int(os.getenv('SMTP_PORT', 587)) +EMAIL_USE_TLS = True +EMAIL_HOST_USER = os.getenv('SMTP_USERNAME') +EMAIL_HOST_PASSWORD = os.getenv('SMTP_PASSWORD') +DEFAULT_FROM_EMAIL = os.getenv('DEFAULT_FROM_EMAIL') + + SITE_ID = 1 MIDDLEWARE = [ @@ -256,4 +267,3 @@ APP_HOST = os.getenv('APP_HOST') except: APP_HOST = 'self' - \ No newline at end of file diff --git a/backend/backend/urls.py b/backend/backend/urls.py index 872d7ed0..413acefe 100644 --- a/backend/backend/urls.py +++ b/backend/backend/urls.py @@ -3,7 +3,7 @@ from django.conf import settings from graphene_django.views import GraphQLView from django.views.decorators.csrf import csrf_exempt -from api.views import PrivateGraphQLView, logout_view, health_check, kms +from api.views import PrivateGraphQLView, logout_view, health_check, kms, SecretsView, user_token_kms, service_token_kms, secrets_tokens CLOUD_HOSTED = settings.APP_HOST == 'cloud' @@ -14,6 +14,8 @@ path('logout/', csrf_exempt(logout_view)), path('graphql/', csrf_exempt(PrivateGraphQLView.as_view(graphiql=True))), path('493c5048-99f9-4eac-ad0d-98c3740b491f/health', health_check), + path('secrets/', SecretsView.as_view()), + path('secrets/tokens/', secrets_tokens) ] if not CLOUD_HOSTED: diff --git a/backend/ee/LICENSE b/backend/ee/LICENSE index 5e43b4ed..6343eec5 100644 --- a/backend/ee/LICENSE +++ b/backend/ee/LICENSE @@ -9,13 +9,13 @@ and are in compliance with, the Phase Subscription Terms of Service, available at https://phase.dev/legal/terms, or other agreement governing the use of the Software, as agreed by you and Phase, and otherwise have a valid Phase Console Enterprise License for the -correct number of applications & user seats. Subject to the foregoing sentence, you are free to +correct number of user seats. Subject to the foregoing sentence, you are free to modify this Software and publish patches to the Software. You agree that Phase and/or its licensors (as applicable) retain all right, title and interest in and to all such modifications and/or patches, and all such modifications and/or patches may only be used, copied, modified, displayed, distributed, or otherwise exploited with a valid Phase Console Enterprise subscription for the correct number of -applications & user seats. Notwithstanding the foregoing, you may copy and modify +user seats. Notwithstanding the foregoing, you may copy and modify the Software for development and testing purposes, without requiring a subscription. You agree that Phase and/or its licensors (as applicable) retain all right, title and interest in and to all such modifications. You are not diff --git a/backend/ee/feature_flags.py b/backend/ee/feature_flags.py index df8d3728..ba54b97a 100644 --- a/backend/ee/feature_flags.py +++ b/backend/ee/feature_flags.py @@ -11,13 +11,14 @@ def allow_new_app(organisation): Returns: bool: Whether or not to allow creating an app for the given org """ - FREE_APP_LIMIT = 1 + FREE_APP_LIMIT = 3 PRO_APP_LIMIT = 10 - current_app_count = App.objects.filter(organisation=organisation, is_deleted=False).count() - + current_app_count = App.objects.filter( + organisation=organisation, is_deleted=False).count() + if organisation.plan == Organisation.FREE_PLAN and current_app_count >= FREE_APP_LIMIT: return False elif organisation.plan == Organisation.PRO_PLAN and current_app_count >= PRO_APP_LIMIT: return False - return True \ No newline at end of file + return True diff --git a/backend/requirements.txt b/backend/requirements.txt index 646126e7..c36f0b58 100644 --- a/backend/requirements.txt +++ b/backend/requirements.txt @@ -9,7 +9,7 @@ certifi==2023.7.22 cffi==1.15.1 charset-normalizer==3.0.1 constantly==15.1.0 -cryptography==41.0.2 +cryptography==41.0.4 defusedxml==0.7.1 dj-rest-auth==3.0.0 Django==4.2.3 diff --git a/dev-docker-compose.yml b/dev-docker-compose.yml index bed702ad..6161f16a 100644 --- a/dev-docker-compose.yml +++ b/dev-docker-compose.yml @@ -49,7 +49,7 @@ services: postgres: container_name: phase-postgres - image: postgres + image: postgres:15.4-alpine3.17 restart: always env_file: - .env.dev diff --git a/docker-compose.yml b/docker-compose.yml index 04865871..1d625ca4 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -29,8 +29,9 @@ services: NEXTAUTH_URL: "${HTTP_PROTOCOL}${HOST}" OAUTH_REDIRECT_URI: "${HTTP_PROTOCOL}${HOST}" BACKEND_API_BASE: "http://backend:8000" - NEXT_PUBLIC_BACKEND_API_BASE: "${HTTP_PROTOCOL}${HOST}/ph-backend" + NEXT_PUBLIC_BACKEND_API_BASE: "${HTTP_PROTOCOL}${HOST}/service" NEXT_PUBLIC_NEXTAUTH_PROVIDERS: "${SSO_PROVIDERS}" + NEXT_PUBLIC_POSTHOG_KEY: "${NEXT_PUBLIC_POSTHOG_KEY}" networks: - phase-net @@ -50,7 +51,7 @@ services: postgres: container_name: phase-postgres - image: postgres + image: postgres:15.4-alpine3.17 restart: always env_file: - .env diff --git a/frontend/.vscode/settings.json b/frontend/.vscode/settings.json new file mode 100644 index 00000000..d0679104 --- /dev/null +++ b/frontend/.vscode/settings.json @@ -0,0 +1,4 @@ +{ + "typescript.tsdk": "node_modules/typescript/lib", + "typescript.enablePromptUseWorkspaceTsdk": true +} \ No newline at end of file diff --git a/frontend/Dockerfile b/frontend/Dockerfile index 6b9e7cad..fa921d4d 100644 --- a/frontend/Dockerfile +++ b/frontend/Dockerfile @@ -3,6 +3,9 @@ FROM node:alpine AS base # set working directory WORKDIR /app +# install system dependencies +RUN apk add curl + # Add a new user "app" and change ownership of the /app directory RUN addgroup app && adduser -S -G app app && chown -R app:app /app @@ -14,9 +17,10 @@ COPY --chown=app:app package.json yarn.lock ./ # ---- Dependencies ---- FROM base AS dependencies + # install node packages RUN yarn install --frozen-lockfile --no-cache && yarn cache clean -# copy production node_modules aside (this is a neat trick to get only production modules) +# copy production node_modules aside RUN cp -R node_modules /tmp/node_modules # install ALL node_modules, including 'devDependencies' RUN yarn install --frozen-lockfile @@ -29,6 +33,7 @@ COPY --chown=app:app . . ARG NEXT_PUBLIC_BACKEND_API_BASE=BAKED_NEXT_PUBLIC_BACKEND_API_BASE ARG NEXT_PUBLIC_NEXTAUTH_PROVIDERS=BAKED_NEXT_PUBLIC_NEXTAUTH_PROVIDERS ARG NEXT_PUBLIC_APP_HOST=BAKED_NEXT_PUBLIC_APP_HOST +ARG NEXT_PUBLIC_POSTHOG_KEY=BAKED_NEXT_PUBLIC_POSTHOG_KEY RUN yarn build # ---- Release ---- diff --git a/frontend/Dockerfile.dev b/frontend/Dockerfile.dev index fec896a1..841c6b54 100644 --- a/frontend/Dockerfile.dev +++ b/frontend/Dockerfile.dev @@ -6,12 +6,12 @@ WORKDIR /app # Copy over package files COPY package.json ./ -COPY package-lock.json ./ +COPY yarn.lock ./ # Install dependencies -RUN npm install +RUN yarn install --frozen-lockfile --no-cache && yarn cache clean # Copy all files COPY . . -CMD ["npm", "run", "dev"] \ No newline at end of file +CMD ["yarn", "dev"] \ No newline at end of file diff --git a/frontend/apollo/client.ts b/frontend/apollo/client.ts index 2bbfac83..e38a0a60 100644 --- a/frontend/apollo/client.ts +++ b/frontend/apollo/client.ts @@ -5,8 +5,10 @@ import { signOut, SignOutParams } from 'next-auth/react' import { UrlUtils } from '@/utils/auth' import axios from 'axios' import { toast } from 'react-toastify' +import posthog from 'posthog-js' export const handleSignout = async (options?: SignOutParams | undefined) => { + posthog.reset() const response = await axios.post( UrlUtils.makeUrl(process.env.NEXT_PUBLIC_BACKEND_API_BASE!, 'logout'), {}, diff --git a/frontend/apollo/gql.ts b/frontend/apollo/gql.ts index b04b6771..497ecb2e 100644 --- a/frontend/apollo/gql.ts +++ b/frontend/apollo/gql.ts @@ -1,8 +1,65 @@ /* eslint-disable */ -import * as types from './graphql' -import { TypedDocumentNode as DocumentNode } from '@graphql-typed-document-node/core' +import * as types from './graphql'; +import { TypedDocumentNode as DocumentNode } from '@graphql-typed-document-node/core'; + +/** + * Map of all GraphQL operations in the project. + * + * This map has several performance disadvantages: + * 1. It is not tree-shakeable, so it will include all operations in the project. + * 2. It is not minifiable, so the string of a GraphQL query will be multiple times inside the bundle. + * 3. It does not support dead code elimination, so it will add unused operations. + * + * Therefore it is highly recommended to use the babel or swc plugin for production. + */ +const documents = { + "mutation AddMemberToApp($memberId: ID!, $appId: ID!, $envKeys: [EnvironmentKeyInput]) {\n addAppMember(memberId: $memberId, appId: $appId, envKeys: $envKeys) {\n app {\n id\n }\n }\n}": types.AddMemberToAppDocument, + "mutation RemoveMemberFromApp($memberId: ID!, $appId: ID!) {\n removeAppMember(memberId: $memberId, appId: $appId) {\n app {\n id\n }\n }\n}": types.RemoveMemberFromAppDocument, + "mutation UpdateEnvScope($memberId: ID!, $appId: ID!, $envKeys: [EnvironmentKeyInput]) {\n updateMemberEnvironmentScope(\n memberId: $memberId\n appId: $appId\n envKeys: $envKeys\n ) {\n app {\n id\n }\n }\n}": types.UpdateEnvScopeDocument, + "mutation CreateApplication($id: ID!, $organisationId: ID!, $name: String!, $identityKey: String!, $appToken: String!, $appSeed: String!, $wrappedKeyShare: String!, $appVersion: Int!) {\n createApp(\n id: $id\n organisationId: $organisationId\n name: $name\n identityKey: $identityKey\n appToken: $appToken\n appSeed: $appSeed\n wrappedKeyShare: $wrappedKeyShare\n appVersion: $appVersion\n ) {\n app {\n id\n name\n identityKey\n }\n }\n}": types.CreateApplicationDocument, + "mutation CreateOrg($id: ID!, $name: String!, $identityKey: String!, $wrappedKeyring: String!, $wrappedRecovery: String!) {\n createOrganisation(\n id: $id\n name: $name\n identityKey: $identityKey\n wrappedKeyring: $wrappedKeyring\n wrappedRecovery: $wrappedRecovery\n ) {\n organisation {\n id\n name\n createdAt\n }\n }\n}": types.CreateOrgDocument, + "mutation DeleteApplication($id: ID!) {\n deleteApp(id: $id) {\n app {\n id\n }\n }\n}": types.DeleteApplicationDocument, + "mutation CreateEnv($input: EnvironmentInput!) {\n createEnvironment(environmentData: $input) {\n environment {\n id\n name\n createdAt\n identityKey\n }\n }\n}": types.CreateEnvDocument, + "mutation CreateEnvKey($envId: ID!, $userId: ID, $wrappedSeed: String!, $wrappedSalt: String!, $identityKey: String!) {\n createEnvironmentKey(\n envId: $envId\n userId: $userId\n wrappedSeed: $wrappedSeed\n wrappedSalt: $wrappedSalt\n identityKey: $identityKey\n ) {\n environmentKey {\n id\n createdAt\n }\n }\n}": types.CreateEnvKeyDocument, + "mutation CreateEnvToken($envId: ID!, $name: String!, $identityKey: String!, $token: String!, $wrappedKeyShare: String!) {\n createEnvironmentToken(\n envId: $envId\n name: $name\n identityKey: $identityKey\n token: $token\n wrappedKeyShare: $wrappedKeyShare\n ) {\n environmentToken {\n id\n createdAt\n }\n }\n}": types.CreateEnvTokenDocument, + "mutation CreateNewSecret($newSecret: SecretInput!) {\n createSecret(secretData: $newSecret) {\n secret {\n id\n key\n value\n createdAt\n }\n }\n}": types.CreateNewSecretDocument, + "mutation CreateNewSecretTag($orgId: ID!, $name: String!, $color: String!) {\n createSecretTag(orgId: $orgId, name: $name, color: $color) {\n tag {\n id\n }\n }\n}": types.CreateNewSecretTagDocument, + "mutation CreateNewServiceToken($appId: ID!, $environmentKeys: [EnvironmentKeyInput], $identityKey: String!, $token: String!, $wrappedKeyShare: String!, $name: String!, $expiry: BigInt) {\n createServiceToken(\n appId: $appId\n environmentKeys: $environmentKeys\n identityKey: $identityKey\n token: $token\n wrappedKeyShare: $wrappedKeyShare\n name: $name\n expiry: $expiry\n ) {\n serviceToken {\n id\n createdAt\n expiresAt\n }\n }\n}": types.CreateNewServiceTokenDocument, + "mutation DeleteSecretOp($id: ID!) {\n deleteSecret(id: $id) {\n secret {\n id\n }\n }\n}": types.DeleteSecretOpDocument, + "mutation RevokeServiceToken($tokenId: ID!) {\n deleteServiceToken(tokenId: $tokenId) {\n ok\n }\n}": types.RevokeServiceTokenDocument, + "mutation UpdateSecret($id: ID!, $secretData: SecretInput!) {\n editSecret(id: $id, secretData: $secretData) {\n secret {\n id\n updatedAt\n }\n }\n}": types.UpdateSecretDocument, + "mutation InitAppEnvironments($devEnv: EnvironmentInput!, $stagingEnv: EnvironmentInput!, $prodEnv: EnvironmentInput!, $devAdminKeys: [EnvironmentKeyInput], $stagAdminKeys: [EnvironmentKeyInput], $prodAdminKeys: [EnvironmentKeyInput]) {\n devEnvironment: createEnvironment(\n environmentData: $devEnv\n adminKeys: $devAdminKeys\n ) {\n environment {\n id\n name\n createdAt\n identityKey\n }\n }\n stagingEnvironment: createEnvironment(\n environmentData: $stagingEnv\n adminKeys: $stagAdminKeys\n ) {\n environment {\n id\n name\n createdAt\n identityKey\n }\n }\n prodEnvironment: createEnvironment(\n environmentData: $prodEnv\n adminKeys: $prodAdminKeys\n ) {\n environment {\n id\n name\n createdAt\n identityKey\n }\n }\n}": types.InitAppEnvironmentsDocument, + "mutation LogSecretRead($id: ID!) {\n readSecret(id: $id) {\n ok\n }\n}": types.LogSecretReadDocument, + "mutation AcceptOrganisationInvite($orgId: ID!, $identityKey: String!, $wrappedKeyring: String!, $wrappedRecovery: String!, $inviteId: ID!) {\n createOrganisationMember(\n orgId: $orgId\n identityKey: $identityKey\n wrappedKeyring: $wrappedKeyring\n wrappedRecovery: $wrappedRecovery\n inviteId: $inviteId\n ) {\n orgMember {\n id\n email\n createdAt\n role\n }\n }\n}": types.AcceptOrganisationInviteDocument, + "mutation DeleteOrgInvite($inviteId: ID!) {\n deleteInvitation(inviteId: $inviteId) {\n ok\n }\n}": types.DeleteOrgInviteDocument, + "mutation RemoveMember($memberId: ID!) {\n deleteOrganisationMember(memberId: $memberId) {\n ok\n }\n}": types.RemoveMemberDocument, + "mutation InviteMember($orgId: ID!, $email: String!, $apps: [String], $role: String) {\n inviteOrganisationMember(orgId: $orgId, email: $email, apps: $apps, role: $role) {\n invite {\n id\n }\n }\n}": types.InviteMemberDocument, + "mutation UpdateMemberRole($memberId: ID!, $role: String!) {\n updateOrganisationMemberRole(memberId: $memberId, role: $role) {\n orgMember {\n id\n role\n }\n }\n}": types.UpdateMemberRoleDocument, + "mutation UpdateWrappedSecrets($orgId: ID!, $wrappedKeyring: String!, $wrappedRecovery: String!) {\n updateMemberWrappedSecrets(\n orgId: $orgId\n wrappedKeyring: $wrappedKeyring\n wrappedRecovery: $wrappedRecovery\n ) {\n orgMember {\n id\n }\n }\n}": types.UpdateWrappedSecretsDocument, + "mutation RotateAppKey($id: ID!, $appToken: String!, $wrappedKeyShare: String!) {\n rotateAppKeys(id: $id, appToken: $appToken, wrappedKeyShare: $wrappedKeyShare) {\n app {\n id\n }\n }\n}": types.RotateAppKeyDocument, + "mutation CreateNewUserToken($orgId: ID!, $name: String!, $identityKey: String!, $token: String!, $wrappedKeyShare: String!, $expiry: BigInt) {\n createUserToken(\n orgId: $orgId\n name: $name\n identityKey: $identityKey\n token: $token\n wrappedKeyShare: $wrappedKeyShare\n expiry: $expiry\n ) {\n ok\n }\n}": types.CreateNewUserTokenDocument, + "mutation RevokeUserToken($tokenId: ID!) {\n deleteUserToken(tokenId: $tokenId) {\n ok\n }\n}": types.RevokeUserTokenDocument, + "query GetAppMembers($appId: ID!) {\n appUsers(appId: $appId) {\n id\n identityKey\n email\n fullName\n avatarUrl\n createdAt\n role\n }\n}": types.GetAppMembersDocument, + "query GetAppActivityChart($appId: ID!, $period: TimeRange) {\n appActivityChart(appId: $appId, period: $period) {\n index\n date\n data\n }\n}": types.GetAppActivityChartDocument, + "query GetAppDetail($organisationId: ID!, $appId: ID!) {\n apps(organisationId: $organisationId, appId: $appId) {\n id\n name\n identityKey\n createdAt\n appToken\n appSeed\n appVersion\n }\n}": types.GetAppDetailDocument, + "query GetAppKmsLogs($appId: ID!, $start: BigInt, $end: BigInt) {\n logs(appId: $appId, start: $start, end: $end) {\n kms {\n id\n timestamp\n phaseNode\n eventType\n ipAddress\n country\n city\n phSize\n }\n }\n kmsLogsCount(appId: $appId)\n}": types.GetAppKmsLogsDocument, + "query GetApps($organisationId: ID!, $appId: ID!) {\n apps(organisationId: $organisationId, appId: $appId) {\n id\n name\n identityKey\n createdAt\n }\n}": types.GetAppsDocument, + "query GetOrganisations {\n organisations {\n id\n name\n identityKey\n createdAt\n plan\n role\n memberId\n keyring\n recovery\n }\n}": types.GetOrganisationsDocument, + "query GetInvites($orgId: ID!) {\n organisationInvites(orgId: $orgId) {\n id\n createdAt\n expiresAt\n invitedBy {\n email\n fullName\n self\n }\n inviteeEmail\n }\n}": types.GetInvitesDocument, + "query GetOrganisationAdminsAndSelf($organisationId: ID!) {\n organisationAdminsAndSelf(organisationId: $organisationId) {\n id\n role\n identityKey\n self\n }\n}": types.GetOrganisationAdminsAndSelfDocument, + "query GetOrganisationMembers($organisationId: ID!, $role: [String]) {\n organisationMembers(organisationId: $organisationId, role: $role) {\n id\n role\n identityKey\n email\n fullName\n avatarUrl\n createdAt\n self\n }\n}": types.GetOrganisationMembersDocument, + "query VerifyInvite($inviteId: ID!) {\n validateInvite(inviteId: $inviteId) {\n id\n organisation {\n id\n name\n }\n inviteeEmail\n invitedBy {\n email\n }\n apps {\n id\n name\n }\n }\n}": types.VerifyInviteDocument, + "query GetAppEnvironments($appId: ID!, $memberId: ID) {\n appEnvironments(appId: $appId, environmentId: null, memberId: $memberId) {\n id\n name\n envType\n identityKey\n wrappedSeed\n wrappedSalt\n createdAt\n }\n}": types.GetAppEnvironmentsDocument, + "query GetAppSecretsLogs($appId: ID!, $start: BigInt, $end: BigInt) {\n logs(appId: $appId, start: $start, end: $end) {\n secrets {\n id\n key\n value\n tags {\n id\n name\n color\n }\n version\n comment\n timestamp\n ipAddress\n userAgent\n user {\n email\n username\n fullName\n avatarUrl\n }\n eventType\n environment {\n id\n envType\n name\n }\n secret {\n id\n }\n }\n }\n secretsLogsCount(appId: $appId)\n environmentKeys(appId: $appId) {\n id\n identityKey\n wrappedSeed\n wrappedSalt\n environment {\n id\n }\n }\n}": types.GetAppSecretsLogsDocument, + "query GetEnvironmentKey($envId: ID!, $appId: ID!) {\n environmentKeys(environmentId: $envId, appId: $appId) {\n id\n identityKey\n wrappedSeed\n wrappedSalt\n }\n}": types.GetEnvironmentKeyDocument, + "query GetEnvironmentTokens($envId: ID!) {\n environmentTokens(environmentId: $envId) {\n id\n name\n wrappedKeyShare\n createdAt\n }\n}": types.GetEnvironmentTokensDocument, + "query GetEnvSecretsKV($envId: ID!) {\n secrets(envId: $envId) {\n id\n key\n value\n }\n environmentKeys(environmentId: $envId) {\n id\n identityKey\n wrappedSeed\n wrappedSalt\n }\n}": types.GetEnvSecretsKvDocument, + "query GetSecretTags($orgId: ID!) {\n secretTags(orgId: $orgId) {\n id\n name\n color\n }\n}": types.GetSecretTagsDocument, + "query GetSecrets($appId: ID!, $envId: ID!) {\n secrets(envId: $envId) {\n id\n key\n value\n tags {\n id\n name\n color\n }\n comment\n createdAt\n history {\n id\n key\n value\n tags {\n id\n name\n color\n }\n version\n comment\n timestamp\n ipAddress\n userAgent\n user {\n email\n username\n fullName\n avatarUrl\n }\n eventType\n }\n }\n appEnvironments(appId: $appId, environmentId: $envId) {\n id\n name\n envType\n identityKey\n }\n environmentKeys(appId: $appId, environmentId: $envId) {\n id\n identityKey\n wrappedSeed\n wrappedSalt\n }\n}": types.GetSecretsDocument, + "query GetServiceTokens($appId: ID!) {\n serviceTokens(appId: $appId) {\n id\n name\n createdAt\n createdBy {\n fullName\n avatarUrl\n self\n }\n expiresAt\n keys {\n id\n }\n }\n}": types.GetServiceTokensDocument, + "query GetUserTokens($organisationId: ID!) {\n userTokens(organisationId: $organisationId) {\n id\n name\n wrappedKeyShare\n createdAt\n expiresAt\n }\n}": types.GetUserTokensDocument, +}; -const documents: never[] = [] /** * The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients. * @@ -15,11 +72,191 @@ const documents: never[] = [] * The query argument is unknown! * Please regenerate the types. */ -export function graphql(source: string): unknown +export function graphql(source: string): unknown; + +/** + * The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients. + */ +export function graphql(source: "mutation AddMemberToApp($memberId: ID!, $appId: ID!, $envKeys: [EnvironmentKeyInput]) {\n addAppMember(memberId: $memberId, appId: $appId, envKeys: $envKeys) {\n app {\n id\n }\n }\n}"): (typeof documents)["mutation AddMemberToApp($memberId: ID!, $appId: ID!, $envKeys: [EnvironmentKeyInput]) {\n addAppMember(memberId: $memberId, appId: $appId, envKeys: $envKeys) {\n app {\n id\n }\n }\n}"]; +/** + * The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients. + */ +export function graphql(source: "mutation RemoveMemberFromApp($memberId: ID!, $appId: ID!) {\n removeAppMember(memberId: $memberId, appId: $appId) {\n app {\n id\n }\n }\n}"): (typeof documents)["mutation RemoveMemberFromApp($memberId: ID!, $appId: ID!) {\n removeAppMember(memberId: $memberId, appId: $appId) {\n app {\n id\n }\n }\n}"]; +/** + * The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients. + */ +export function graphql(source: "mutation UpdateEnvScope($memberId: ID!, $appId: ID!, $envKeys: [EnvironmentKeyInput]) {\n updateMemberEnvironmentScope(\n memberId: $memberId\n appId: $appId\n envKeys: $envKeys\n ) {\n app {\n id\n }\n }\n}"): (typeof documents)["mutation UpdateEnvScope($memberId: ID!, $appId: ID!, $envKeys: [EnvironmentKeyInput]) {\n updateMemberEnvironmentScope(\n memberId: $memberId\n appId: $appId\n envKeys: $envKeys\n ) {\n app {\n id\n }\n }\n}"]; +/** + * The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients. + */ +export function graphql(source: "mutation CreateApplication($id: ID!, $organisationId: ID!, $name: String!, $identityKey: String!, $appToken: String!, $appSeed: String!, $wrappedKeyShare: String!, $appVersion: Int!) {\n createApp(\n id: $id\n organisationId: $organisationId\n name: $name\n identityKey: $identityKey\n appToken: $appToken\n appSeed: $appSeed\n wrappedKeyShare: $wrappedKeyShare\n appVersion: $appVersion\n ) {\n app {\n id\n name\n identityKey\n }\n }\n}"): (typeof documents)["mutation CreateApplication($id: ID!, $organisationId: ID!, $name: String!, $identityKey: String!, $appToken: String!, $appSeed: String!, $wrappedKeyShare: String!, $appVersion: Int!) {\n createApp(\n id: $id\n organisationId: $organisationId\n name: $name\n identityKey: $identityKey\n appToken: $appToken\n appSeed: $appSeed\n wrappedKeyShare: $wrappedKeyShare\n appVersion: $appVersion\n ) {\n app {\n id\n name\n identityKey\n }\n }\n}"]; +/** + * The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients. + */ +export function graphql(source: "mutation CreateOrg($id: ID!, $name: String!, $identityKey: String!, $wrappedKeyring: String!, $wrappedRecovery: String!) {\n createOrganisation(\n id: $id\n name: $name\n identityKey: $identityKey\n wrappedKeyring: $wrappedKeyring\n wrappedRecovery: $wrappedRecovery\n ) {\n organisation {\n id\n name\n createdAt\n }\n }\n}"): (typeof documents)["mutation CreateOrg($id: ID!, $name: String!, $identityKey: String!, $wrappedKeyring: String!, $wrappedRecovery: String!) {\n createOrganisation(\n id: $id\n name: $name\n identityKey: $identityKey\n wrappedKeyring: $wrappedKeyring\n wrappedRecovery: $wrappedRecovery\n ) {\n organisation {\n id\n name\n createdAt\n }\n }\n}"]; +/** + * The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients. + */ +export function graphql(source: "mutation DeleteApplication($id: ID!) {\n deleteApp(id: $id) {\n app {\n id\n }\n }\n}"): (typeof documents)["mutation DeleteApplication($id: ID!) {\n deleteApp(id: $id) {\n app {\n id\n }\n }\n}"]; +/** + * The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients. + */ +export function graphql(source: "mutation CreateEnv($input: EnvironmentInput!) {\n createEnvironment(environmentData: $input) {\n environment {\n id\n name\n createdAt\n identityKey\n }\n }\n}"): (typeof documents)["mutation CreateEnv($input: EnvironmentInput!) {\n createEnvironment(environmentData: $input) {\n environment {\n id\n name\n createdAt\n identityKey\n }\n }\n}"]; +/** + * The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients. + */ +export function graphql(source: "mutation CreateEnvKey($envId: ID!, $userId: ID, $wrappedSeed: String!, $wrappedSalt: String!, $identityKey: String!) {\n createEnvironmentKey(\n envId: $envId\n userId: $userId\n wrappedSeed: $wrappedSeed\n wrappedSalt: $wrappedSalt\n identityKey: $identityKey\n ) {\n environmentKey {\n id\n createdAt\n }\n }\n}"): (typeof documents)["mutation CreateEnvKey($envId: ID!, $userId: ID, $wrappedSeed: String!, $wrappedSalt: String!, $identityKey: String!) {\n createEnvironmentKey(\n envId: $envId\n userId: $userId\n wrappedSeed: $wrappedSeed\n wrappedSalt: $wrappedSalt\n identityKey: $identityKey\n ) {\n environmentKey {\n id\n createdAt\n }\n }\n}"]; +/** + * The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients. + */ +export function graphql(source: "mutation CreateEnvToken($envId: ID!, $name: String!, $identityKey: String!, $token: String!, $wrappedKeyShare: String!) {\n createEnvironmentToken(\n envId: $envId\n name: $name\n identityKey: $identityKey\n token: $token\n wrappedKeyShare: $wrappedKeyShare\n ) {\n environmentToken {\n id\n createdAt\n }\n }\n}"): (typeof documents)["mutation CreateEnvToken($envId: ID!, $name: String!, $identityKey: String!, $token: String!, $wrappedKeyShare: String!) {\n createEnvironmentToken(\n envId: $envId\n name: $name\n identityKey: $identityKey\n token: $token\n wrappedKeyShare: $wrappedKeyShare\n ) {\n environmentToken {\n id\n createdAt\n }\n }\n}"]; +/** + * The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients. + */ +export function graphql(source: "mutation CreateNewSecret($newSecret: SecretInput!) {\n createSecret(secretData: $newSecret) {\n secret {\n id\n key\n value\n createdAt\n }\n }\n}"): (typeof documents)["mutation CreateNewSecret($newSecret: SecretInput!) {\n createSecret(secretData: $newSecret) {\n secret {\n id\n key\n value\n createdAt\n }\n }\n}"]; +/** + * The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients. + */ +export function graphql(source: "mutation CreateNewSecretTag($orgId: ID!, $name: String!, $color: String!) {\n createSecretTag(orgId: $orgId, name: $name, color: $color) {\n tag {\n id\n }\n }\n}"): (typeof documents)["mutation CreateNewSecretTag($orgId: ID!, $name: String!, $color: String!) {\n createSecretTag(orgId: $orgId, name: $name, color: $color) {\n tag {\n id\n }\n }\n}"]; +/** + * The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients. + */ +export function graphql(source: "mutation CreateNewServiceToken($appId: ID!, $environmentKeys: [EnvironmentKeyInput], $identityKey: String!, $token: String!, $wrappedKeyShare: String!, $name: String!, $expiry: BigInt) {\n createServiceToken(\n appId: $appId\n environmentKeys: $environmentKeys\n identityKey: $identityKey\n token: $token\n wrappedKeyShare: $wrappedKeyShare\n name: $name\n expiry: $expiry\n ) {\n serviceToken {\n id\n createdAt\n expiresAt\n }\n }\n}"): (typeof documents)["mutation CreateNewServiceToken($appId: ID!, $environmentKeys: [EnvironmentKeyInput], $identityKey: String!, $token: String!, $wrappedKeyShare: String!, $name: String!, $expiry: BigInt) {\n createServiceToken(\n appId: $appId\n environmentKeys: $environmentKeys\n identityKey: $identityKey\n token: $token\n wrappedKeyShare: $wrappedKeyShare\n name: $name\n expiry: $expiry\n ) {\n serviceToken {\n id\n createdAt\n expiresAt\n }\n }\n}"]; +/** + * The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients. + */ +export function graphql(source: "mutation DeleteSecretOp($id: ID!) {\n deleteSecret(id: $id) {\n secret {\n id\n }\n }\n}"): (typeof documents)["mutation DeleteSecretOp($id: ID!) {\n deleteSecret(id: $id) {\n secret {\n id\n }\n }\n}"]; +/** + * The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients. + */ +export function graphql(source: "mutation RevokeServiceToken($tokenId: ID!) {\n deleteServiceToken(tokenId: $tokenId) {\n ok\n }\n}"): (typeof documents)["mutation RevokeServiceToken($tokenId: ID!) {\n deleteServiceToken(tokenId: $tokenId) {\n ok\n }\n}"]; +/** + * The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients. + */ +export function graphql(source: "mutation UpdateSecret($id: ID!, $secretData: SecretInput!) {\n editSecret(id: $id, secretData: $secretData) {\n secret {\n id\n updatedAt\n }\n }\n}"): (typeof documents)["mutation UpdateSecret($id: ID!, $secretData: SecretInput!) {\n editSecret(id: $id, secretData: $secretData) {\n secret {\n id\n updatedAt\n }\n }\n}"]; +/** + * The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients. + */ +export function graphql(source: "mutation InitAppEnvironments($devEnv: EnvironmentInput!, $stagingEnv: EnvironmentInput!, $prodEnv: EnvironmentInput!, $devAdminKeys: [EnvironmentKeyInput], $stagAdminKeys: [EnvironmentKeyInput], $prodAdminKeys: [EnvironmentKeyInput]) {\n devEnvironment: createEnvironment(\n environmentData: $devEnv\n adminKeys: $devAdminKeys\n ) {\n environment {\n id\n name\n createdAt\n identityKey\n }\n }\n stagingEnvironment: createEnvironment(\n environmentData: $stagingEnv\n adminKeys: $stagAdminKeys\n ) {\n environment {\n id\n name\n createdAt\n identityKey\n }\n }\n prodEnvironment: createEnvironment(\n environmentData: $prodEnv\n adminKeys: $prodAdminKeys\n ) {\n environment {\n id\n name\n createdAt\n identityKey\n }\n }\n}"): (typeof documents)["mutation InitAppEnvironments($devEnv: EnvironmentInput!, $stagingEnv: EnvironmentInput!, $prodEnv: EnvironmentInput!, $devAdminKeys: [EnvironmentKeyInput], $stagAdminKeys: [EnvironmentKeyInput], $prodAdminKeys: [EnvironmentKeyInput]) {\n devEnvironment: createEnvironment(\n environmentData: $devEnv\n adminKeys: $devAdminKeys\n ) {\n environment {\n id\n name\n createdAt\n identityKey\n }\n }\n stagingEnvironment: createEnvironment(\n environmentData: $stagingEnv\n adminKeys: $stagAdminKeys\n ) {\n environment {\n id\n name\n createdAt\n identityKey\n }\n }\n prodEnvironment: createEnvironment(\n environmentData: $prodEnv\n adminKeys: $prodAdminKeys\n ) {\n environment {\n id\n name\n createdAt\n identityKey\n }\n }\n}"]; +/** + * The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients. + */ +export function graphql(source: "mutation LogSecretRead($id: ID!) {\n readSecret(id: $id) {\n ok\n }\n}"): (typeof documents)["mutation LogSecretRead($id: ID!) {\n readSecret(id: $id) {\n ok\n }\n}"]; +/** + * The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients. + */ +export function graphql(source: "mutation AcceptOrganisationInvite($orgId: ID!, $identityKey: String!, $wrappedKeyring: String!, $wrappedRecovery: String!, $inviteId: ID!) {\n createOrganisationMember(\n orgId: $orgId\n identityKey: $identityKey\n wrappedKeyring: $wrappedKeyring\n wrappedRecovery: $wrappedRecovery\n inviteId: $inviteId\n ) {\n orgMember {\n id\n email\n createdAt\n role\n }\n }\n}"): (typeof documents)["mutation AcceptOrganisationInvite($orgId: ID!, $identityKey: String!, $wrappedKeyring: String!, $wrappedRecovery: String!, $inviteId: ID!) {\n createOrganisationMember(\n orgId: $orgId\n identityKey: $identityKey\n wrappedKeyring: $wrappedKeyring\n wrappedRecovery: $wrappedRecovery\n inviteId: $inviteId\n ) {\n orgMember {\n id\n email\n createdAt\n role\n }\n }\n}"]; +/** + * The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients. + */ +export function graphql(source: "mutation DeleteOrgInvite($inviteId: ID!) {\n deleteInvitation(inviteId: $inviteId) {\n ok\n }\n}"): (typeof documents)["mutation DeleteOrgInvite($inviteId: ID!) {\n deleteInvitation(inviteId: $inviteId) {\n ok\n }\n}"]; +/** + * The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients. + */ +export function graphql(source: "mutation RemoveMember($memberId: ID!) {\n deleteOrganisationMember(memberId: $memberId) {\n ok\n }\n}"): (typeof documents)["mutation RemoveMember($memberId: ID!) {\n deleteOrganisationMember(memberId: $memberId) {\n ok\n }\n}"]; +/** + * The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients. + */ +export function graphql(source: "mutation InviteMember($orgId: ID!, $email: String!, $apps: [String], $role: String) {\n inviteOrganisationMember(orgId: $orgId, email: $email, apps: $apps, role: $role) {\n invite {\n id\n }\n }\n}"): (typeof documents)["mutation InviteMember($orgId: ID!, $email: String!, $apps: [String], $role: String) {\n inviteOrganisationMember(orgId: $orgId, email: $email, apps: $apps, role: $role) {\n invite {\n id\n }\n }\n}"]; +/** + * The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients. + */ +export function graphql(source: "mutation UpdateMemberRole($memberId: ID!, $role: String!) {\n updateOrganisationMemberRole(memberId: $memberId, role: $role) {\n orgMember {\n id\n role\n }\n }\n}"): (typeof documents)["mutation UpdateMemberRole($memberId: ID!, $role: String!) {\n updateOrganisationMemberRole(memberId: $memberId, role: $role) {\n orgMember {\n id\n role\n }\n }\n}"]; +/** + * The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients. + */ +export function graphql(source: "mutation UpdateWrappedSecrets($orgId: ID!, $wrappedKeyring: String!, $wrappedRecovery: String!) {\n updateMemberWrappedSecrets(\n orgId: $orgId\n wrappedKeyring: $wrappedKeyring\n wrappedRecovery: $wrappedRecovery\n ) {\n orgMember {\n id\n }\n }\n}"): (typeof documents)["mutation UpdateWrappedSecrets($orgId: ID!, $wrappedKeyring: String!, $wrappedRecovery: String!) {\n updateMemberWrappedSecrets(\n orgId: $orgId\n wrappedKeyring: $wrappedKeyring\n wrappedRecovery: $wrappedRecovery\n ) {\n orgMember {\n id\n }\n }\n}"]; +/** + * The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients. + */ +export function graphql(source: "mutation RotateAppKey($id: ID!, $appToken: String!, $wrappedKeyShare: String!) {\n rotateAppKeys(id: $id, appToken: $appToken, wrappedKeyShare: $wrappedKeyShare) {\n app {\n id\n }\n }\n}"): (typeof documents)["mutation RotateAppKey($id: ID!, $appToken: String!, $wrappedKeyShare: String!) {\n rotateAppKeys(id: $id, appToken: $appToken, wrappedKeyShare: $wrappedKeyShare) {\n app {\n id\n }\n }\n}"]; +/** + * The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients. + */ +export function graphql(source: "mutation CreateNewUserToken($orgId: ID!, $name: String!, $identityKey: String!, $token: String!, $wrappedKeyShare: String!, $expiry: BigInt) {\n createUserToken(\n orgId: $orgId\n name: $name\n identityKey: $identityKey\n token: $token\n wrappedKeyShare: $wrappedKeyShare\n expiry: $expiry\n ) {\n ok\n }\n}"): (typeof documents)["mutation CreateNewUserToken($orgId: ID!, $name: String!, $identityKey: String!, $token: String!, $wrappedKeyShare: String!, $expiry: BigInt) {\n createUserToken(\n orgId: $orgId\n name: $name\n identityKey: $identityKey\n token: $token\n wrappedKeyShare: $wrappedKeyShare\n expiry: $expiry\n ) {\n ok\n }\n}"]; +/** + * The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients. + */ +export function graphql(source: "mutation RevokeUserToken($tokenId: ID!) {\n deleteUserToken(tokenId: $tokenId) {\n ok\n }\n}"): (typeof documents)["mutation RevokeUserToken($tokenId: ID!) {\n deleteUserToken(tokenId: $tokenId) {\n ok\n }\n}"]; +/** + * The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients. + */ +export function graphql(source: "query GetAppMembers($appId: ID!) {\n appUsers(appId: $appId) {\n id\n identityKey\n email\n fullName\n avatarUrl\n createdAt\n role\n }\n}"): (typeof documents)["query GetAppMembers($appId: ID!) {\n appUsers(appId: $appId) {\n id\n identityKey\n email\n fullName\n avatarUrl\n createdAt\n role\n }\n}"]; +/** + * The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients. + */ +export function graphql(source: "query GetAppActivityChart($appId: ID!, $period: TimeRange) {\n appActivityChart(appId: $appId, period: $period) {\n index\n date\n data\n }\n}"): (typeof documents)["query GetAppActivityChart($appId: ID!, $period: TimeRange) {\n appActivityChart(appId: $appId, period: $period) {\n index\n date\n data\n }\n}"]; +/** + * The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients. + */ +export function graphql(source: "query GetAppDetail($organisationId: ID!, $appId: ID!) {\n apps(organisationId: $organisationId, appId: $appId) {\n id\n name\n identityKey\n createdAt\n appToken\n appSeed\n appVersion\n }\n}"): (typeof documents)["query GetAppDetail($organisationId: ID!, $appId: ID!) {\n apps(organisationId: $organisationId, appId: $appId) {\n id\n name\n identityKey\n createdAt\n appToken\n appSeed\n appVersion\n }\n}"]; +/** + * The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients. + */ +export function graphql(source: "query GetAppKmsLogs($appId: ID!, $start: BigInt, $end: BigInt) {\n logs(appId: $appId, start: $start, end: $end) {\n kms {\n id\n timestamp\n phaseNode\n eventType\n ipAddress\n country\n city\n phSize\n }\n }\n kmsLogsCount(appId: $appId)\n}"): (typeof documents)["query GetAppKmsLogs($appId: ID!, $start: BigInt, $end: BigInt) {\n logs(appId: $appId, start: $start, end: $end) {\n kms {\n id\n timestamp\n phaseNode\n eventType\n ipAddress\n country\n city\n phSize\n }\n }\n kmsLogsCount(appId: $appId)\n}"]; +/** + * The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients. + */ +export function graphql(source: "query GetApps($organisationId: ID!, $appId: ID!) {\n apps(organisationId: $organisationId, appId: $appId) {\n id\n name\n identityKey\n createdAt\n }\n}"): (typeof documents)["query GetApps($organisationId: ID!, $appId: ID!) {\n apps(organisationId: $organisationId, appId: $appId) {\n id\n name\n identityKey\n createdAt\n }\n}"]; +/** + * The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients. + */ +export function graphql(source: "query GetOrganisations {\n organisations {\n id\n name\n identityKey\n createdAt\n plan\n role\n memberId\n keyring\n recovery\n }\n}"): (typeof documents)["query GetOrganisations {\n organisations {\n id\n name\n identityKey\n createdAt\n plan\n role\n memberId\n keyring\n recovery\n }\n}"]; +/** + * The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients. + */ +export function graphql(source: "query GetInvites($orgId: ID!) {\n organisationInvites(orgId: $orgId) {\n id\n createdAt\n expiresAt\n invitedBy {\n email\n fullName\n self\n }\n inviteeEmail\n }\n}"): (typeof documents)["query GetInvites($orgId: ID!) {\n organisationInvites(orgId: $orgId) {\n id\n createdAt\n expiresAt\n invitedBy {\n email\n fullName\n self\n }\n inviteeEmail\n }\n}"]; +/** + * The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients. + */ +export function graphql(source: "query GetOrganisationAdminsAndSelf($organisationId: ID!) {\n organisationAdminsAndSelf(organisationId: $organisationId) {\n id\n role\n identityKey\n self\n }\n}"): (typeof documents)["query GetOrganisationAdminsAndSelf($organisationId: ID!) {\n organisationAdminsAndSelf(organisationId: $organisationId) {\n id\n role\n identityKey\n self\n }\n}"]; +/** + * The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients. + */ +export function graphql(source: "query GetOrganisationMembers($organisationId: ID!, $role: [String]) {\n organisationMembers(organisationId: $organisationId, role: $role) {\n id\n role\n identityKey\n email\n fullName\n avatarUrl\n createdAt\n self\n }\n}"): (typeof documents)["query GetOrganisationMembers($organisationId: ID!, $role: [String]) {\n organisationMembers(organisationId: $organisationId, role: $role) {\n id\n role\n identityKey\n email\n fullName\n avatarUrl\n createdAt\n self\n }\n}"]; +/** + * The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients. + */ +export function graphql(source: "query VerifyInvite($inviteId: ID!) {\n validateInvite(inviteId: $inviteId) {\n id\n organisation {\n id\n name\n }\n inviteeEmail\n invitedBy {\n email\n }\n apps {\n id\n name\n }\n }\n}"): (typeof documents)["query VerifyInvite($inviteId: ID!) {\n validateInvite(inviteId: $inviteId) {\n id\n organisation {\n id\n name\n }\n inviteeEmail\n invitedBy {\n email\n }\n apps {\n id\n name\n }\n }\n}"]; +/** + * The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients. + */ +export function graphql(source: "query GetAppEnvironments($appId: ID!, $memberId: ID) {\n appEnvironments(appId: $appId, environmentId: null, memberId: $memberId) {\n id\n name\n envType\n identityKey\n wrappedSeed\n wrappedSalt\n createdAt\n }\n}"): (typeof documents)["query GetAppEnvironments($appId: ID!, $memberId: ID) {\n appEnvironments(appId: $appId, environmentId: null, memberId: $memberId) {\n id\n name\n envType\n identityKey\n wrappedSeed\n wrappedSalt\n createdAt\n }\n}"]; +/** + * The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients. + */ +export function graphql(source: "query GetAppSecretsLogs($appId: ID!, $start: BigInt, $end: BigInt) {\n logs(appId: $appId, start: $start, end: $end) {\n secrets {\n id\n key\n value\n tags {\n id\n name\n color\n }\n version\n comment\n timestamp\n ipAddress\n userAgent\n user {\n email\n username\n fullName\n avatarUrl\n }\n eventType\n environment {\n id\n envType\n name\n }\n secret {\n id\n }\n }\n }\n secretsLogsCount(appId: $appId)\n environmentKeys(appId: $appId) {\n id\n identityKey\n wrappedSeed\n wrappedSalt\n environment {\n id\n }\n }\n}"): (typeof documents)["query GetAppSecretsLogs($appId: ID!, $start: BigInt, $end: BigInt) {\n logs(appId: $appId, start: $start, end: $end) {\n secrets {\n id\n key\n value\n tags {\n id\n name\n color\n }\n version\n comment\n timestamp\n ipAddress\n userAgent\n user {\n email\n username\n fullName\n avatarUrl\n }\n eventType\n environment {\n id\n envType\n name\n }\n secret {\n id\n }\n }\n }\n secretsLogsCount(appId: $appId)\n environmentKeys(appId: $appId) {\n id\n identityKey\n wrappedSeed\n wrappedSalt\n environment {\n id\n }\n }\n}"]; +/** + * The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients. + */ +export function graphql(source: "query GetEnvironmentKey($envId: ID!, $appId: ID!) {\n environmentKeys(environmentId: $envId, appId: $appId) {\n id\n identityKey\n wrappedSeed\n wrappedSalt\n }\n}"): (typeof documents)["query GetEnvironmentKey($envId: ID!, $appId: ID!) {\n environmentKeys(environmentId: $envId, appId: $appId) {\n id\n identityKey\n wrappedSeed\n wrappedSalt\n }\n}"]; +/** + * The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients. + */ +export function graphql(source: "query GetEnvironmentTokens($envId: ID!) {\n environmentTokens(environmentId: $envId) {\n id\n name\n wrappedKeyShare\n createdAt\n }\n}"): (typeof documents)["query GetEnvironmentTokens($envId: ID!) {\n environmentTokens(environmentId: $envId) {\n id\n name\n wrappedKeyShare\n createdAt\n }\n}"]; +/** + * The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients. + */ +export function graphql(source: "query GetEnvSecretsKV($envId: ID!) {\n secrets(envId: $envId) {\n id\n key\n value\n }\n environmentKeys(environmentId: $envId) {\n id\n identityKey\n wrappedSeed\n wrappedSalt\n }\n}"): (typeof documents)["query GetEnvSecretsKV($envId: ID!) {\n secrets(envId: $envId) {\n id\n key\n value\n }\n environmentKeys(environmentId: $envId) {\n id\n identityKey\n wrappedSeed\n wrappedSalt\n }\n}"]; +/** + * The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients. + */ +export function graphql(source: "query GetSecretTags($orgId: ID!) {\n secretTags(orgId: $orgId) {\n id\n name\n color\n }\n}"): (typeof documents)["query GetSecretTags($orgId: ID!) {\n secretTags(orgId: $orgId) {\n id\n name\n color\n }\n}"]; +/** + * The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients. + */ +export function graphql(source: "query GetSecrets($appId: ID!, $envId: ID!) {\n secrets(envId: $envId) {\n id\n key\n value\n tags {\n id\n name\n color\n }\n comment\n createdAt\n history {\n id\n key\n value\n tags {\n id\n name\n color\n }\n version\n comment\n timestamp\n ipAddress\n userAgent\n user {\n email\n username\n fullName\n avatarUrl\n }\n eventType\n }\n }\n appEnvironments(appId: $appId, environmentId: $envId) {\n id\n name\n envType\n identityKey\n }\n environmentKeys(appId: $appId, environmentId: $envId) {\n id\n identityKey\n wrappedSeed\n wrappedSalt\n }\n}"): (typeof documents)["query GetSecrets($appId: ID!, $envId: ID!) {\n secrets(envId: $envId) {\n id\n key\n value\n tags {\n id\n name\n color\n }\n comment\n createdAt\n history {\n id\n key\n value\n tags {\n id\n name\n color\n }\n version\n comment\n timestamp\n ipAddress\n userAgent\n user {\n email\n username\n fullName\n avatarUrl\n }\n eventType\n }\n }\n appEnvironments(appId: $appId, environmentId: $envId) {\n id\n name\n envType\n identityKey\n }\n environmentKeys(appId: $appId, environmentId: $envId) {\n id\n identityKey\n wrappedSeed\n wrappedSalt\n }\n}"]; +/** + * The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients. + */ +export function graphql(source: "query GetServiceTokens($appId: ID!) {\n serviceTokens(appId: $appId) {\n id\n name\n createdAt\n createdBy {\n fullName\n avatarUrl\n self\n }\n expiresAt\n keys {\n id\n }\n }\n}"): (typeof documents)["query GetServiceTokens($appId: ID!) {\n serviceTokens(appId: $appId) {\n id\n name\n createdAt\n createdBy {\n fullName\n avatarUrl\n self\n }\n expiresAt\n keys {\n id\n }\n }\n}"]; +/** + * The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients. + */ +export function graphql(source: "query GetUserTokens($organisationId: ID!) {\n userTokens(organisationId: $organisationId) {\n id\n name\n wrappedKeyShare\n createdAt\n expiresAt\n }\n}"): (typeof documents)["query GetUserTokens($organisationId: ID!) {\n userTokens(organisationId: $organisationId) {\n id\n name\n wrappedKeyShare\n createdAt\n expiresAt\n }\n}"]; export function graphql(source: string) { - return (documents as any)[source] ?? {} + return (documents as any)[source] ?? {}; } -export type DocumentType> = - TDocumentNode extends DocumentNode ? TType : never +export type DocumentType> = TDocumentNode extends DocumentNode< infer TType, any> ? TType : never; \ No newline at end of file diff --git a/frontend/apollo/graphql.ts b/frontend/apollo/graphql.ts index ac182286..a6b000c9 100644 --- a/frontend/apollo/graphql.ts +++ b/frontend/apollo/graphql.ts @@ -1,28 +1,64 @@ /* eslint-disable */ -export type Maybe = T | null -export type InputMaybe = Maybe -export type Exact = { [K in keyof T]: T[K] } -export type MakeOptional = Omit & { [SubKey in K]?: Maybe } -export type MakeMaybe = Omit & { [SubKey in K]: Maybe } +import { TypedDocumentNode as DocumentNode } from '@graphql-typed-document-node/core'; +export type Maybe = T | null; +export type InputMaybe = Maybe; +export type Exact = { [K in keyof T]: T[K] }; +export type MakeOptional = Omit & { [SubKey in K]?: Maybe }; +export type MakeMaybe = Omit & { [SubKey in K]: Maybe }; /** All built-in and custom scalars, mapped to their actual values */ export type Scalars = { - ID: string - String: string - Boolean: boolean - Int: number - Float: number + ID: string; + String: string; + Boolean: boolean; + Int: number; + Float: number; /** * The `BigInt` scalar type represents non-fractional whole numeric values. * `BigInt` is not constrained to 32-bit like the `Int` type and thus is a less * compatible type. */ - BigInt: any + BigInt: any; /** * The `DateTime` scalar type represents a DateTime * value as specified by * [iso8601](https://en.wikipedia.org/wiki/ISO_8601). */ - DateTime: any + DateTime: any; +}; + +export type AddAppMemberMutation = { + __typename?: 'AddAppMemberMutation'; + app?: Maybe; +}; + +/** An enumeration. */ +export enum ApiEnvironmentEnvTypeChoices { + /** Development */ + Dev = 'DEV', + /** Production */ + Prod = 'PROD', + /** Staging */ + Staging = 'STAGING' +} + +/** An enumeration. */ +export enum ApiOrganisationMemberInviteRoleChoices { + /** Admin */ + Admin = 'ADMIN', + /** Developer */ + Dev = 'DEV', + /** Owner */ + Owner = 'OWNER' +} + +/** An enumeration. */ +export enum ApiOrganisationMemberRoleChoices { + /** Admin */ + Admin = 'ADMIN', + /** Developer */ + Dev = 'DEV', + /** Owner */ + Owner = 'OWNER' } /** An enumeration. */ @@ -32,145 +68,681 @@ export enum ApiOrganisationPlanChoices { /** Free */ Fr = 'FR', /** Pro */ - Pr = 'PR', + Pr = 'PR' } -export type AppType = { - __typename?: 'AppType' - appSeed: Scalars['String'] - appToken: Scalars['String'] - appVersion: Scalars['Int'] - createdAt?: Maybe - id: Scalars['String'] - identityKey: Scalars['String'] - name: Scalars['String'] - wrappedKeyShare: Scalars['String'] +/** An enumeration. */ +export enum ApiSecretEventEventTypeChoices { + /** Create */ + C = 'C', + /** Delete */ + D = 'D', + /** Read */ + R = 'R', + /** Update */ + U = 'U' } +export type AppType = { + __typename?: 'AppType'; + appSeed: Scalars['String']; + appToken: Scalars['String']; + appVersion: Scalars['Int']; + createdAt?: Maybe; + id: Scalars['String']; + identityKey: Scalars['String']; + name: Scalars['String']; + wrappedKeyShare: Scalars['String']; +}; + export type ChartDataPointType = { - __typename?: 'ChartDataPointType' - data?: Maybe - date?: Maybe - index?: Maybe -} + __typename?: 'ChartDataPointType'; + data?: Maybe; + date?: Maybe; + index?: Maybe; +}; export type CreateAppMutation = { - __typename?: 'CreateAppMutation' - app?: Maybe -} + __typename?: 'CreateAppMutation'; + app?: Maybe; +}; + +export type CreateEnvironmentKeyMutation = { + __typename?: 'CreateEnvironmentKeyMutation'; + environmentKey?: Maybe; +}; + +export type CreateEnvironmentMutation = { + __typename?: 'CreateEnvironmentMutation'; + environment?: Maybe; +}; + +export type CreateEnvironmentTokenMutation = { + __typename?: 'CreateEnvironmentTokenMutation'; + environmentToken?: Maybe; +}; + +export type CreateOrganisationMemberMutation = { + __typename?: 'CreateOrganisationMemberMutation'; + orgMember?: Maybe; +}; export type CreateOrganisationMutation = { - __typename?: 'CreateOrganisationMutation' - organisation?: Maybe -} + __typename?: 'CreateOrganisationMutation'; + organisation?: Maybe; +}; + +export type CreateSecretFolderMutation = { + __typename?: 'CreateSecretFolderMutation'; + folder?: Maybe; +}; + +export type CreateSecretMutation = { + __typename?: 'CreateSecretMutation'; + secret?: Maybe; +}; + +export type CreateSecretTagMutation = { + __typename?: 'CreateSecretTagMutation'; + tag?: Maybe; +}; + +export type CreateServiceTokenMutation = { + __typename?: 'CreateServiceTokenMutation'; + serviceToken?: Maybe; +}; + +export type CreateUserTokenMutation = { + __typename?: 'CreateUserTokenMutation'; + ok?: Maybe; + userToken?: Maybe; +}; export type DeleteAppMutation = { - __typename?: 'DeleteAppMutation' - app?: Maybe -} + __typename?: 'DeleteAppMutation'; + app?: Maybe; +}; + +export type DeleteInviteMutation = { + __typename?: 'DeleteInviteMutation'; + ok?: Maybe; +}; + +export type DeleteOrganisationMemberMutation = { + __typename?: 'DeleteOrganisationMemberMutation'; + ok?: Maybe; +}; + +export type DeleteSecretMutation = { + __typename?: 'DeleteSecretMutation'; + secret?: Maybe; +}; + +export type DeleteServiceTokenMutation = { + __typename?: 'DeleteServiceTokenMutation'; + ok?: Maybe; +}; + +export type DeleteUserTokenMutation = { + __typename?: 'DeleteUserTokenMutation'; + ok?: Maybe; +}; + +export type EditSecretMutation = { + __typename?: 'EditSecretMutation'; + secret?: Maybe; +}; + +export type EnvironmentInput = { + appId: Scalars['ID']; + envType: Scalars['String']; + identityKey: Scalars['String']; + name: Scalars['String']; + wrappedSalt: Scalars['String']; + wrappedSeed: Scalars['String']; +}; + +export type EnvironmentKeyInput = { + envId: Scalars['ID']; + identityKey: Scalars['String']; + userId?: InputMaybe; + wrappedSalt: Scalars['String']; + wrappedSeed: Scalars['String']; +}; + +export type EnvironmentKeyType = { + __typename?: 'EnvironmentKeyType'; + createdAt?: Maybe; + environment: EnvironmentType; + id: Scalars['String']; + identityKey: Scalars['String']; + updatedAt: Scalars['DateTime']; + wrappedSalt: Scalars['String']; + wrappedSeed: Scalars['String']; +}; + +export type EnvironmentTokenType = { + __typename?: 'EnvironmentTokenType'; + createdAt?: Maybe; + id: Scalars['String']; + identityKey: Scalars['String']; + name: Scalars['String']; + token: Scalars['String']; + updatedAt: Scalars['DateTime']; + wrappedKeyShare: Scalars['String']; +}; + +export type EnvironmentType = { + __typename?: 'EnvironmentType'; + createdAt?: Maybe; + envType: ApiEnvironmentEnvTypeChoices; + id: Scalars['String']; + identityKey: Scalars['String']; + name: Scalars['String']; + updatedAt: Scalars['DateTime']; + wrappedSalt: Scalars['String']; + wrappedSeed: Scalars['String']; +}; + +export type InviteOrganisationMemberMutation = { + __typename?: 'InviteOrganisationMemberMutation'; + invite?: Maybe; +}; export type KmsLogType = Node & { - __typename?: 'KMSLogType' - appId?: Maybe - asn?: Maybe - city?: Maybe - country?: Maybe - edgeLocation?: Maybe - eventType?: Maybe - id: Scalars['ID'] - ipAddress?: Maybe - isp?: Maybe - latitude?: Maybe - longitude?: Maybe - phSize?: Maybe - phaseNode?: Maybe - timestamp?: Maybe -} + __typename?: 'KMSLogType'; + appId?: Maybe; + asn?: Maybe; + city?: Maybe; + country?: Maybe; + edgeLocation?: Maybe; + eventType?: Maybe; + id: Scalars['ID']; + ipAddress?: Maybe; + isp?: Maybe; + latitude?: Maybe; + longitude?: Maybe; + phSize?: Maybe; + phaseNode?: Maybe; + timestamp?: Maybe; +}; + +export type LogsResponseType = { + __typename?: 'LogsResponseType'; + kms?: Maybe>>; + secrets?: Maybe>>; +}; export type Mutation = { - __typename?: 'Mutation' - createApp?: Maybe - createOrganisation?: Maybe - deleteApp?: Maybe - rotateAppKeys?: Maybe -} + __typename?: 'Mutation'; + addAppMember?: Maybe; + createApp?: Maybe; + createEnvironment?: Maybe; + createEnvironmentKey?: Maybe; + createEnvironmentToken?: Maybe; + createOrganisation?: Maybe; + createOrganisationMember?: Maybe; + createSecret?: Maybe; + createSecretFolder?: Maybe; + createSecretTag?: Maybe; + createServiceToken?: Maybe; + createUserToken?: Maybe; + deleteApp?: Maybe; + deleteInvitation?: Maybe; + deleteOrganisationMember?: Maybe; + deleteSecret?: Maybe; + deleteServiceToken?: Maybe; + deleteUserToken?: Maybe; + editSecret?: Maybe; + inviteOrganisationMember?: Maybe; + readSecret?: Maybe; + removeAppMember?: Maybe; + rotateAppKeys?: Maybe; + updateMemberEnvironmentScope?: Maybe; + updateMemberWrappedSecrets?: Maybe; + updateOrganisationMemberRole?: Maybe; +}; + + +export type MutationAddAppMemberArgs = { + appId?: InputMaybe; + envKeys?: InputMaybe>>; + memberId?: InputMaybe; +}; + export type MutationCreateAppArgs = { - appSeed: Scalars['String'] - appToken: Scalars['String'] - appVersion: Scalars['Int'] - id: Scalars['ID'] - identityKey: Scalars['String'] - name: Scalars['String'] - organisationId: Scalars['ID'] - wrappedKeyShare: Scalars['String'] -} + appSeed: Scalars['String']; + appToken: Scalars['String']; + appVersion: Scalars['Int']; + id: Scalars['ID']; + identityKey: Scalars['String']; + name: Scalars['String']; + organisationId: Scalars['ID']; + wrappedKeyShare: Scalars['String']; +}; + + +export type MutationCreateEnvironmentArgs = { + adminKeys?: InputMaybe>>; + environmentData: EnvironmentInput; +}; + + +export type MutationCreateEnvironmentKeyArgs = { + envId: Scalars['ID']; + identityKey: Scalars['String']; + userId?: InputMaybe; + wrappedSalt: Scalars['String']; + wrappedSeed: Scalars['String']; +}; + + +export type MutationCreateEnvironmentTokenArgs = { + envId: Scalars['ID']; + identityKey: Scalars['String']; + name: Scalars['String']; + token: Scalars['String']; + wrappedKeyShare: Scalars['String']; +}; + export type MutationCreateOrganisationArgs = { - id: Scalars['ID'] - identityKey: Scalars['String'] - name: Scalars['String'] -} + id: Scalars['ID']; + identityKey: Scalars['String']; + name: Scalars['String']; + wrappedKeyring: Scalars['String']; + wrappedRecovery: Scalars['String']; +}; + + +export type MutationCreateOrganisationMemberArgs = { + identityKey: Scalars['String']; + inviteId: Scalars['ID']; + orgId: Scalars['ID']; + wrappedKeyring?: InputMaybe; + wrappedRecovery?: InputMaybe; +}; + + +export type MutationCreateSecretArgs = { + secretData?: InputMaybe; +}; + + +export type MutationCreateSecretFolderArgs = { + envId: Scalars['ID']; + id: Scalars['ID']; + name: Scalars['String']; + parentFolderId?: InputMaybe; +}; + + +export type MutationCreateSecretTagArgs = { + color: Scalars['String']; + name: Scalars['String']; + orgId: Scalars['ID']; +}; + + +export type MutationCreateServiceTokenArgs = { + appId: Scalars['ID']; + environmentKeys?: InputMaybe>>; + expiry?: InputMaybe; + identityKey: Scalars['String']; + name: Scalars['String']; + token: Scalars['String']; + wrappedKeyShare: Scalars['String']; +}; + + +export type MutationCreateUserTokenArgs = { + expiry?: InputMaybe; + identityKey: Scalars['String']; + name: Scalars['String']; + orgId: Scalars['ID']; + token: Scalars['String']; + wrappedKeyShare: Scalars['String']; +}; + export type MutationDeleteAppArgs = { - id: Scalars['ID'] -} + id: Scalars['ID']; +}; + + +export type MutationDeleteInvitationArgs = { + inviteId: Scalars['ID']; +}; + + +export type MutationDeleteOrganisationMemberArgs = { + memberId: Scalars['ID']; +}; + + +export type MutationDeleteSecretArgs = { + id: Scalars['ID']; +}; + + +export type MutationDeleteServiceTokenArgs = { + tokenId: Scalars['ID']; +}; + + +export type MutationDeleteUserTokenArgs = { + tokenId: Scalars['ID']; +}; + + +export type MutationEditSecretArgs = { + id: Scalars['ID']; + secretData?: InputMaybe; +}; + + +export type MutationInviteOrganisationMemberArgs = { + apps?: InputMaybe>>; + email: Scalars['String']; + orgId: Scalars['ID']; + role?: InputMaybe; +}; + + +export type MutationReadSecretArgs = { + id: Scalars['ID']; +}; + + +export type MutationRemoveAppMemberArgs = { + appId?: InputMaybe; + memberId?: InputMaybe; +}; + export type MutationRotateAppKeysArgs = { - appToken: Scalars['String'] - id: Scalars['ID'] - wrappedKeyShare: Scalars['String'] -} + appToken: Scalars['String']; + id: Scalars['ID']; + wrappedKeyShare: Scalars['String']; +}; + + +export type MutationUpdateMemberEnvironmentScopeArgs = { + appId?: InputMaybe; + envKeys?: InputMaybe>>; + memberId?: InputMaybe; +}; + + +export type MutationUpdateMemberWrappedSecretsArgs = { + orgId: Scalars['ID']; + wrappedKeyring: Scalars['String']; + wrappedRecovery: Scalars['String']; +}; + + +export type MutationUpdateOrganisationMemberRoleArgs = { + memberId: Scalars['ID']; + role: Scalars['String']; +}; /** An object with an ID */ export type Node = { /** The ID of the object */ - id: Scalars['ID'] -} + id: Scalars['ID']; +}; + +export type OrganisationMemberInviteType = { + __typename?: 'OrganisationMemberInviteType'; + apps: Array; + createdAt?: Maybe; + expiresAt: Scalars['DateTime']; + id: Scalars['String']; + invitedBy: OrganisationMemberType; + inviteeEmail: Scalars['String']; + organisation: OrganisationType; + role: ApiOrganisationMemberInviteRoleChoices; + updatedAt: Scalars['DateTime']; + valid: Scalars['Boolean']; +}; + +export type OrganisationMemberType = { + __typename?: 'OrganisationMemberType'; + avatarUrl?: Maybe; + createdAt?: Maybe; + email?: Maybe; + fullName?: Maybe; + id: Scalars['String']; + identityKey?: Maybe; + role: ApiOrganisationMemberRoleChoices; + self?: Maybe; + updatedAt: Scalars['DateTime']; + username?: Maybe; + wrappedKeyring: Scalars['String']; +}; export type OrganisationType = { - __typename?: 'OrganisationType' - createdAt?: Maybe - id: Scalars['String'] - identityKey: Scalars['String'] - name: Scalars['String'] - plan: ApiOrganisationPlanChoices -} + __typename?: 'OrganisationType'; + createdAt?: Maybe; + id: Scalars['String']; + identityKey: Scalars['String']; + keyring?: Maybe; + memberId?: Maybe; + name: Scalars['String']; + plan: ApiOrganisationPlanChoices; + recovery?: Maybe; + role?: Maybe; +}; export type Query = { - __typename?: 'Query' - appActivityChart?: Maybe>> - apps?: Maybe>> - logs?: Maybe>> - logsCount?: Maybe - organisations?: Maybe>> -} + __typename?: 'Query'; + appActivityChart?: Maybe>>; + appEnvironments?: Maybe>>; + appUsers?: Maybe>>; + apps?: Maybe>>; + environmentKeys?: Maybe>>; + environmentTokens?: Maybe>>; + kmsLogsCount?: Maybe; + logs?: Maybe; + organisationAdminsAndSelf?: Maybe>>; + organisationInvites?: Maybe>>; + organisationMembers?: Maybe>>; + organisations?: Maybe>>; + secretHistory?: Maybe>>; + secretTags?: Maybe>>; + secrets?: Maybe>>; + secretsLogsCount?: Maybe; + serviceTokens?: Maybe>>; + userTokens?: Maybe>>; + validateInvite?: Maybe; +}; + export type QueryAppActivityChartArgs = { - appId?: InputMaybe - period?: InputMaybe -} + appId?: InputMaybe; + period?: InputMaybe; +}; + + +export type QueryAppEnvironmentsArgs = { + appId?: InputMaybe; + environmentId?: InputMaybe; + memberId?: InputMaybe; +}; + + +export type QueryAppUsersArgs = { + appId?: InputMaybe; +}; + export type QueryAppsArgs = { - appId?: InputMaybe - organisationId?: InputMaybe -} + appId?: InputMaybe; + organisationId?: InputMaybe; +}; + + +export type QueryEnvironmentKeysArgs = { + appId?: InputMaybe; + environmentId?: InputMaybe; + memberId?: InputMaybe; +}; + + +export type QueryEnvironmentTokensArgs = { + environmentId?: InputMaybe; +}; + + +export type QueryKmsLogsCountArgs = { + appId?: InputMaybe; + thisMonth?: InputMaybe; +}; + export type QueryLogsArgs = { - appId?: InputMaybe - end?: InputMaybe - start?: InputMaybe -} + appId?: InputMaybe; + end?: InputMaybe; + start?: InputMaybe; +}; + + +export type QueryOrganisationAdminsAndSelfArgs = { + organisationId?: InputMaybe; +}; + + +export type QueryOrganisationInvitesArgs = { + orgId?: InputMaybe; +}; + + +export type QueryOrganisationMembersArgs = { + organisationId?: InputMaybe; + role?: InputMaybe>>; + userId?: InputMaybe; +}; + + +export type QuerySecretHistoryArgs = { + secretId?: InputMaybe; +}; + + +export type QuerySecretTagsArgs = { + orgId?: InputMaybe; +}; -export type QueryLogsCountArgs = { - appId?: InputMaybe - thisMonth?: InputMaybe -} + +export type QuerySecretsArgs = { + envId?: InputMaybe; +}; + + +export type QuerySecretsLogsCountArgs = { + appId?: InputMaybe; +}; + + +export type QueryServiceTokensArgs = { + appId?: InputMaybe; +}; + + +export type QueryUserTokensArgs = { + organisationId?: InputMaybe; +}; + + +export type QueryValidateInviteArgs = { + inviteId?: InputMaybe; +}; + +export type ReadSecretMutation = { + __typename?: 'ReadSecretMutation'; + ok?: Maybe; +}; + +export type RemoveAppMemberMutation = { + __typename?: 'RemoveAppMemberMutation'; + app?: Maybe; +}; export type RotateAppKeysMutation = { - __typename?: 'RotateAppKeysMutation' - app?: Maybe -} + __typename?: 'RotateAppKeysMutation'; + app?: Maybe; +}; + +export type SecretEventType = { + __typename?: 'SecretEventType'; + comment: Scalars['String']; + environment: EnvironmentType; + eventType: ApiSecretEventEventTypeChoices; + id: Scalars['String']; + ipAddress?: Maybe; + key: Scalars['String']; + secret: SecretType; + tags: Array; + timestamp: Scalars['DateTime']; + user?: Maybe; + userAgent?: Maybe; + value: Scalars['String']; + version: Scalars['Int']; +}; + +export type SecretFolderType = { + __typename?: 'SecretFolderType'; + createdAt?: Maybe; + id: Scalars['String']; + name: Scalars['String']; + updatedAt: Scalars['DateTime']; +}; + +export type SecretInput = { + comment?: InputMaybe; + envId?: InputMaybe; + folderId?: InputMaybe; + key: Scalars['String']; + keyDigest: Scalars['String']; + tags?: InputMaybe>>; + value: Scalars['String']; +}; + +export type SecretTagType = { + __typename?: 'SecretTagType'; + color: Scalars['String']; + id: Scalars['String']; + name: Scalars['String']; +}; + +export type SecretType = { + __typename?: 'SecretType'; + comment: Scalars['String']; + createdAt?: Maybe; + folder?: Maybe; + history?: Maybe>>; + id: Scalars['String']; + key: Scalars['String']; + tags: Array; + updatedAt: Scalars['DateTime']; + value: Scalars['String']; + version: Scalars['Int']; +}; + +export type ServiceTokenType = { + __typename?: 'ServiceTokenType'; + createdAt?: Maybe; + createdBy?: Maybe; + expiresAt?: Maybe; + id: Scalars['String']; + identityKey: Scalars['String']; + keys: Array; + name: Scalars['String']; + token: Scalars['String']; + updatedAt: Scalars['DateTime']; + wrappedKeyShare: Scalars['String']; +}; /** An enumeration. */ export enum TimeRange { @@ -179,5 +751,458 @@ export enum TimeRange { Hour = 'HOUR', Month = 'MONTH', Week = 'WEEK', - Year = 'YEAR', + Year = 'YEAR' } + +export type UpdateMemberEnvScopeMutation = { + __typename?: 'UpdateMemberEnvScopeMutation'; + app?: Maybe; +}; + +export type UpdateOrganisationMemberRole = { + __typename?: 'UpdateOrganisationMemberRole'; + orgMember?: Maybe; +}; + +export type UpdateUserWrappedSecretsMutation = { + __typename?: 'UpdateUserWrappedSecretsMutation'; + orgMember?: Maybe; +}; + +export type UserTokenType = { + __typename?: 'UserTokenType'; + createdAt?: Maybe; + expiresAt?: Maybe; + id: Scalars['String']; + identityKey: Scalars['String']; + name: Scalars['String']; + token: Scalars['String']; + updatedAt: Scalars['DateTime']; + wrappedKeyShare: Scalars['String']; +}; + +export type AddMemberToAppMutationVariables = Exact<{ + memberId: Scalars['ID']; + appId: Scalars['ID']; + envKeys?: InputMaybe> | InputMaybe>; +}>; + + +export type AddMemberToAppMutation = { __typename?: 'Mutation', addAppMember?: { __typename?: 'AddAppMemberMutation', app?: { __typename?: 'AppType', id: string } | null } | null }; + +export type RemoveMemberFromAppMutationVariables = Exact<{ + memberId: Scalars['ID']; + appId: Scalars['ID']; +}>; + + +export type RemoveMemberFromAppMutation = { __typename?: 'Mutation', removeAppMember?: { __typename?: 'RemoveAppMemberMutation', app?: { __typename?: 'AppType', id: string } | null } | null }; + +export type UpdateEnvScopeMutationVariables = Exact<{ + memberId: Scalars['ID']; + appId: Scalars['ID']; + envKeys?: InputMaybe> | InputMaybe>; +}>; + + +export type UpdateEnvScopeMutation = { __typename?: 'Mutation', updateMemberEnvironmentScope?: { __typename?: 'UpdateMemberEnvScopeMutation', app?: { __typename?: 'AppType', id: string } | null } | null }; + +export type CreateApplicationMutationVariables = Exact<{ + id: Scalars['ID']; + organisationId: Scalars['ID']; + name: Scalars['String']; + identityKey: Scalars['String']; + appToken: Scalars['String']; + appSeed: Scalars['String']; + wrappedKeyShare: Scalars['String']; + appVersion: Scalars['Int']; +}>; + + +export type CreateApplicationMutation = { __typename?: 'Mutation', createApp?: { __typename?: 'CreateAppMutation', app?: { __typename?: 'AppType', id: string, name: string, identityKey: string } | null } | null }; + +export type CreateOrgMutationVariables = Exact<{ + id: Scalars['ID']; + name: Scalars['String']; + identityKey: Scalars['String']; + wrappedKeyring: Scalars['String']; + wrappedRecovery: Scalars['String']; +}>; + + +export type CreateOrgMutation = { __typename?: 'Mutation', createOrganisation?: { __typename?: 'CreateOrganisationMutation', organisation?: { __typename?: 'OrganisationType', id: string, name: string, createdAt?: any | null } | null } | null }; + +export type DeleteApplicationMutationVariables = Exact<{ + id: Scalars['ID']; +}>; + + +export type DeleteApplicationMutation = { __typename?: 'Mutation', deleteApp?: { __typename?: 'DeleteAppMutation', app?: { __typename?: 'AppType', id: string } | null } | null }; + +export type CreateEnvMutationVariables = Exact<{ + input: EnvironmentInput; +}>; + + +export type CreateEnvMutation = { __typename?: 'Mutation', createEnvironment?: { __typename?: 'CreateEnvironmentMutation', environment?: { __typename?: 'EnvironmentType', id: string, name: string, createdAt?: any | null, identityKey: string } | null } | null }; + +export type CreateEnvKeyMutationVariables = Exact<{ + envId: Scalars['ID']; + userId?: InputMaybe; + wrappedSeed: Scalars['String']; + wrappedSalt: Scalars['String']; + identityKey: Scalars['String']; +}>; + + +export type CreateEnvKeyMutation = { __typename?: 'Mutation', createEnvironmentKey?: { __typename?: 'CreateEnvironmentKeyMutation', environmentKey?: { __typename?: 'EnvironmentKeyType', id: string, createdAt?: any | null } | null } | null }; + +export type CreateEnvTokenMutationVariables = Exact<{ + envId: Scalars['ID']; + name: Scalars['String']; + identityKey: Scalars['String']; + token: Scalars['String']; + wrappedKeyShare: Scalars['String']; +}>; + + +export type CreateEnvTokenMutation = { __typename?: 'Mutation', createEnvironmentToken?: { __typename?: 'CreateEnvironmentTokenMutation', environmentToken?: { __typename?: 'EnvironmentTokenType', id: string, createdAt?: any | null } | null } | null }; + +export type CreateNewSecretMutationVariables = Exact<{ + newSecret: SecretInput; +}>; + + +export type CreateNewSecretMutation = { __typename?: 'Mutation', createSecret?: { __typename?: 'CreateSecretMutation', secret?: { __typename?: 'SecretType', id: string, key: string, value: string, createdAt?: any | null } | null } | null }; + +export type CreateNewSecretTagMutationVariables = Exact<{ + orgId: Scalars['ID']; + name: Scalars['String']; + color: Scalars['String']; +}>; + + +export type CreateNewSecretTagMutation = { __typename?: 'Mutation', createSecretTag?: { __typename?: 'CreateSecretTagMutation', tag?: { __typename?: 'SecretTagType', id: string } | null } | null }; + +export type CreateNewServiceTokenMutationVariables = Exact<{ + appId: Scalars['ID']; + environmentKeys?: InputMaybe> | InputMaybe>; + identityKey: Scalars['String']; + token: Scalars['String']; + wrappedKeyShare: Scalars['String']; + name: Scalars['String']; + expiry?: InputMaybe; +}>; + + +export type CreateNewServiceTokenMutation = { __typename?: 'Mutation', createServiceToken?: { __typename?: 'CreateServiceTokenMutation', serviceToken?: { __typename?: 'ServiceTokenType', id: string, createdAt?: any | null, expiresAt?: any | null } | null } | null }; + +export type DeleteSecretOpMutationVariables = Exact<{ + id: Scalars['ID']; +}>; + + +export type DeleteSecretOpMutation = { __typename?: 'Mutation', deleteSecret?: { __typename?: 'DeleteSecretMutation', secret?: { __typename?: 'SecretType', id: string } | null } | null }; + +export type RevokeServiceTokenMutationVariables = Exact<{ + tokenId: Scalars['ID']; +}>; + + +export type RevokeServiceTokenMutation = { __typename?: 'Mutation', deleteServiceToken?: { __typename?: 'DeleteServiceTokenMutation', ok?: boolean | null } | null }; + +export type UpdateSecretMutationVariables = Exact<{ + id: Scalars['ID']; + secretData: SecretInput; +}>; + + +export type UpdateSecretMutation = { __typename?: 'Mutation', editSecret?: { __typename?: 'EditSecretMutation', secret?: { __typename?: 'SecretType', id: string, updatedAt: any } | null } | null }; + +export type InitAppEnvironmentsMutationVariables = Exact<{ + devEnv: EnvironmentInput; + stagingEnv: EnvironmentInput; + prodEnv: EnvironmentInput; + devAdminKeys?: InputMaybe> | InputMaybe>; + stagAdminKeys?: InputMaybe> | InputMaybe>; + prodAdminKeys?: InputMaybe> | InputMaybe>; +}>; + + +export type InitAppEnvironmentsMutation = { __typename?: 'Mutation', devEnvironment?: { __typename?: 'CreateEnvironmentMutation', environment?: { __typename?: 'EnvironmentType', id: string, name: string, createdAt?: any | null, identityKey: string } | null } | null, stagingEnvironment?: { __typename?: 'CreateEnvironmentMutation', environment?: { __typename?: 'EnvironmentType', id: string, name: string, createdAt?: any | null, identityKey: string } | null } | null, prodEnvironment?: { __typename?: 'CreateEnvironmentMutation', environment?: { __typename?: 'EnvironmentType', id: string, name: string, createdAt?: any | null, identityKey: string } | null } | null }; + +export type LogSecretReadMutationVariables = Exact<{ + id: Scalars['ID']; +}>; + + +export type LogSecretReadMutation = { __typename?: 'Mutation', readSecret?: { __typename?: 'ReadSecretMutation', ok?: boolean | null } | null }; + +export type AcceptOrganisationInviteMutationVariables = Exact<{ + orgId: Scalars['ID']; + identityKey: Scalars['String']; + wrappedKeyring: Scalars['String']; + wrappedRecovery: Scalars['String']; + inviteId: Scalars['ID']; +}>; + + +export type AcceptOrganisationInviteMutation = { __typename?: 'Mutation', createOrganisationMember?: { __typename?: 'CreateOrganisationMemberMutation', orgMember?: { __typename?: 'OrganisationMemberType', id: string, email?: string | null, createdAt?: any | null, role: ApiOrganisationMemberRoleChoices } | null } | null }; + +export type DeleteOrgInviteMutationVariables = Exact<{ + inviteId: Scalars['ID']; +}>; + + +export type DeleteOrgInviteMutation = { __typename?: 'Mutation', deleteInvitation?: { __typename?: 'DeleteInviteMutation', ok?: boolean | null } | null }; + +export type RemoveMemberMutationVariables = Exact<{ + memberId: Scalars['ID']; +}>; + + +export type RemoveMemberMutation = { __typename?: 'Mutation', deleteOrganisationMember?: { __typename?: 'DeleteOrganisationMemberMutation', ok?: boolean | null } | null }; + +export type InviteMemberMutationVariables = Exact<{ + orgId: Scalars['ID']; + email: Scalars['String']; + apps?: InputMaybe> | InputMaybe>; + role?: InputMaybe; +}>; + + +export type InviteMemberMutation = { __typename?: 'Mutation', inviteOrganisationMember?: { __typename?: 'InviteOrganisationMemberMutation', invite?: { __typename?: 'OrganisationMemberInviteType', id: string } | null } | null }; + +export type UpdateMemberRoleMutationVariables = Exact<{ + memberId: Scalars['ID']; + role: Scalars['String']; +}>; + + +export type UpdateMemberRoleMutation = { __typename?: 'Mutation', updateOrganisationMemberRole?: { __typename?: 'UpdateOrganisationMemberRole', orgMember?: { __typename?: 'OrganisationMemberType', id: string, role: ApiOrganisationMemberRoleChoices } | null } | null }; + +export type UpdateWrappedSecretsMutationVariables = Exact<{ + orgId: Scalars['ID']; + wrappedKeyring: Scalars['String']; + wrappedRecovery: Scalars['String']; +}>; + + +export type UpdateWrappedSecretsMutation = { __typename?: 'Mutation', updateMemberWrappedSecrets?: { __typename?: 'UpdateUserWrappedSecretsMutation', orgMember?: { __typename?: 'OrganisationMemberType', id: string } | null } | null }; + +export type RotateAppKeyMutationVariables = Exact<{ + id: Scalars['ID']; + appToken: Scalars['String']; + wrappedKeyShare: Scalars['String']; +}>; + + +export type RotateAppKeyMutation = { __typename?: 'Mutation', rotateAppKeys?: { __typename?: 'RotateAppKeysMutation', app?: { __typename?: 'AppType', id: string } | null } | null }; + +export type CreateNewUserTokenMutationVariables = Exact<{ + orgId: Scalars['ID']; + name: Scalars['String']; + identityKey: Scalars['String']; + token: Scalars['String']; + wrappedKeyShare: Scalars['String']; + expiry?: InputMaybe; +}>; + + +export type CreateNewUserTokenMutation = { __typename?: 'Mutation', createUserToken?: { __typename?: 'CreateUserTokenMutation', ok?: boolean | null } | null }; + +export type RevokeUserTokenMutationVariables = Exact<{ + tokenId: Scalars['ID']; +}>; + + +export type RevokeUserTokenMutation = { __typename?: 'Mutation', deleteUserToken?: { __typename?: 'DeleteUserTokenMutation', ok?: boolean | null } | null }; + +export type GetAppMembersQueryVariables = Exact<{ + appId: Scalars['ID']; +}>; + + +export type GetAppMembersQuery = { __typename?: 'Query', appUsers?: Array<{ __typename?: 'OrganisationMemberType', id: string, identityKey?: string | null, email?: string | null, fullName?: string | null, avatarUrl?: string | null, createdAt?: any | null, role: ApiOrganisationMemberRoleChoices } | null> | null }; + +export type GetAppActivityChartQueryVariables = Exact<{ + appId: Scalars['ID']; + period?: InputMaybe; +}>; + + +export type GetAppActivityChartQuery = { __typename?: 'Query', appActivityChart?: Array<{ __typename?: 'ChartDataPointType', index?: number | null, date?: any | null, data?: number | null } | null> | null }; + +export type GetAppDetailQueryVariables = Exact<{ + organisationId: Scalars['ID']; + appId: Scalars['ID']; +}>; + + +export type GetAppDetailQuery = { __typename?: 'Query', apps?: Array<{ __typename?: 'AppType', id: string, name: string, identityKey: string, createdAt?: any | null, appToken: string, appSeed: string, appVersion: number } | null> | null }; + +export type GetAppKmsLogsQueryVariables = Exact<{ + appId: Scalars['ID']; + start?: InputMaybe; + end?: InputMaybe; +}>; + + +export type GetAppKmsLogsQuery = { __typename?: 'Query', kmsLogsCount?: number | null, logs?: { __typename?: 'LogsResponseType', kms?: Array<{ __typename?: 'KMSLogType', id: string, timestamp?: any | null, phaseNode?: string | null, eventType?: string | null, ipAddress?: string | null, country?: string | null, city?: string | null, phSize?: number | null } | null> | null } | null }; + +export type GetAppsQueryVariables = Exact<{ + organisationId: Scalars['ID']; + appId: Scalars['ID']; +}>; + + +export type GetAppsQuery = { __typename?: 'Query', apps?: Array<{ __typename?: 'AppType', id: string, name: string, identityKey: string, createdAt?: any | null } | null> | null }; + +export type GetOrganisationsQueryVariables = Exact<{ [key: string]: never; }>; + + +export type GetOrganisationsQuery = { __typename?: 'Query', organisations?: Array<{ __typename?: 'OrganisationType', id: string, name: string, identityKey: string, createdAt?: any | null, plan: ApiOrganisationPlanChoices, role?: string | null, memberId?: string | null, keyring?: string | null, recovery?: string | null } | null> | null }; + +export type GetInvitesQueryVariables = Exact<{ + orgId: Scalars['ID']; +}>; + + +export type GetInvitesQuery = { __typename?: 'Query', organisationInvites?: Array<{ __typename?: 'OrganisationMemberInviteType', id: string, createdAt?: any | null, expiresAt: any, inviteeEmail: string, invitedBy: { __typename?: 'OrganisationMemberType', email?: string | null, fullName?: string | null, self?: boolean | null } } | null> | null }; + +export type GetOrganisationAdminsAndSelfQueryVariables = Exact<{ + organisationId: Scalars['ID']; +}>; + + +export type GetOrganisationAdminsAndSelfQuery = { __typename?: 'Query', organisationAdminsAndSelf?: Array<{ __typename?: 'OrganisationMemberType', id: string, role: ApiOrganisationMemberRoleChoices, identityKey?: string | null, self?: boolean | null } | null> | null }; + +export type GetOrganisationMembersQueryVariables = Exact<{ + organisationId: Scalars['ID']; + role?: InputMaybe> | InputMaybe>; +}>; + + +export type GetOrganisationMembersQuery = { __typename?: 'Query', organisationMembers?: Array<{ __typename?: 'OrganisationMemberType', id: string, role: ApiOrganisationMemberRoleChoices, identityKey?: string | null, email?: string | null, fullName?: string | null, avatarUrl?: string | null, createdAt?: any | null, self?: boolean | null } | null> | null }; + +export type VerifyInviteQueryVariables = Exact<{ + inviteId: Scalars['ID']; +}>; + + +export type VerifyInviteQuery = { __typename?: 'Query', validateInvite?: { __typename?: 'OrganisationMemberInviteType', id: string, inviteeEmail: string, organisation: { __typename?: 'OrganisationType', id: string, name: string }, invitedBy: { __typename?: 'OrganisationMemberType', email?: string | null }, apps: Array<{ __typename?: 'AppType', id: string, name: string }> } | null }; + +export type GetAppEnvironmentsQueryVariables = Exact<{ + appId: Scalars['ID']; + memberId?: InputMaybe; +}>; + + +export type GetAppEnvironmentsQuery = { __typename?: 'Query', appEnvironments?: Array<{ __typename?: 'EnvironmentType', id: string, name: string, envType: ApiEnvironmentEnvTypeChoices, identityKey: string, wrappedSeed: string, wrappedSalt: string, createdAt?: any | null } | null> | null }; + +export type GetAppSecretsLogsQueryVariables = Exact<{ + appId: Scalars['ID']; + start?: InputMaybe; + end?: InputMaybe; +}>; + + +export type GetAppSecretsLogsQuery = { __typename?: 'Query', secretsLogsCount?: number | null, logs?: { __typename?: 'LogsResponseType', secrets?: Array<{ __typename?: 'SecretEventType', id: string, key: string, value: string, version: number, comment: string, timestamp: any, ipAddress?: string | null, userAgent?: string | null, eventType: ApiSecretEventEventTypeChoices, tags: Array<{ __typename?: 'SecretTagType', id: string, name: string, color: string }>, user?: { __typename?: 'OrganisationMemberType', email?: string | null, username?: string | null, fullName?: string | null, avatarUrl?: string | null } | null, environment: { __typename?: 'EnvironmentType', id: string, envType: ApiEnvironmentEnvTypeChoices, name: string }, secret: { __typename?: 'SecretType', id: string } } | null> | null } | null, environmentKeys?: Array<{ __typename?: 'EnvironmentKeyType', id: string, identityKey: string, wrappedSeed: string, wrappedSalt: string, environment: { __typename?: 'EnvironmentType', id: string } } | null> | null }; + +export type GetEnvironmentKeyQueryVariables = Exact<{ + envId: Scalars['ID']; + appId: Scalars['ID']; +}>; + + +export type GetEnvironmentKeyQuery = { __typename?: 'Query', environmentKeys?: Array<{ __typename?: 'EnvironmentKeyType', id: string, identityKey: string, wrappedSeed: string, wrappedSalt: string } | null> | null }; + +export type GetEnvironmentTokensQueryVariables = Exact<{ + envId: Scalars['ID']; +}>; + + +export type GetEnvironmentTokensQuery = { __typename?: 'Query', environmentTokens?: Array<{ __typename?: 'EnvironmentTokenType', id: string, name: string, wrappedKeyShare: string, createdAt?: any | null } | null> | null }; + +export type GetEnvSecretsKvQueryVariables = Exact<{ + envId: Scalars['ID']; +}>; + + +export type GetEnvSecretsKvQuery = { __typename?: 'Query', secrets?: Array<{ __typename?: 'SecretType', id: string, key: string, value: string } | null> | null, environmentKeys?: Array<{ __typename?: 'EnvironmentKeyType', id: string, identityKey: string, wrappedSeed: string, wrappedSalt: string } | null> | null }; + +export type GetSecretTagsQueryVariables = Exact<{ + orgId: Scalars['ID']; +}>; + + +export type GetSecretTagsQuery = { __typename?: 'Query', secretTags?: Array<{ __typename?: 'SecretTagType', id: string, name: string, color: string } | null> | null }; + +export type GetSecretsQueryVariables = Exact<{ + appId: Scalars['ID']; + envId: Scalars['ID']; +}>; + + +export type GetSecretsQuery = { __typename?: 'Query', secrets?: Array<{ __typename?: 'SecretType', id: string, key: string, value: string, comment: string, createdAt?: any | null, tags: Array<{ __typename?: 'SecretTagType', id: string, name: string, color: string }>, history?: Array<{ __typename?: 'SecretEventType', id: string, key: string, value: string, version: number, comment: string, timestamp: any, ipAddress?: string | null, userAgent?: string | null, eventType: ApiSecretEventEventTypeChoices, tags: Array<{ __typename?: 'SecretTagType', id: string, name: string, color: string }>, user?: { __typename?: 'OrganisationMemberType', email?: string | null, username?: string | null, fullName?: string | null, avatarUrl?: string | null } | null } | null> | null } | null> | null, appEnvironments?: Array<{ __typename?: 'EnvironmentType', id: string, name: string, envType: ApiEnvironmentEnvTypeChoices, identityKey: string } | null> | null, environmentKeys?: Array<{ __typename?: 'EnvironmentKeyType', id: string, identityKey: string, wrappedSeed: string, wrappedSalt: string } | null> | null }; + +export type GetServiceTokensQueryVariables = Exact<{ + appId: Scalars['ID']; +}>; + + +export type GetServiceTokensQuery = { __typename?: 'Query', serviceTokens?: Array<{ __typename?: 'ServiceTokenType', id: string, name: string, createdAt?: any | null, expiresAt?: any | null, createdBy?: { __typename?: 'OrganisationMemberType', fullName?: string | null, avatarUrl?: string | null, self?: boolean | null } | null, keys: Array<{ __typename?: 'EnvironmentKeyType', id: string }> } | null> | null }; + +export type GetUserTokensQueryVariables = Exact<{ + organisationId: Scalars['ID']; +}>; + + +export type GetUserTokensQuery = { __typename?: 'Query', userTokens?: Array<{ __typename?: 'UserTokenType', id: string, name: string, wrappedKeyShare: string, createdAt?: any | null, expiresAt?: any | null } | null> | null }; + + +export const AddMemberToAppDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"mutation","name":{"kind":"Name","value":"AddMemberToApp"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"memberId"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"ID"}}}},{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"appId"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"ID"}}}},{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"envKeys"}},"type":{"kind":"ListType","type":{"kind":"NamedType","name":{"kind":"Name","value":"EnvironmentKeyInput"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"addAppMember"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"memberId"},"value":{"kind":"Variable","name":{"kind":"Name","value":"memberId"}}},{"kind":"Argument","name":{"kind":"Name","value":"appId"},"value":{"kind":"Variable","name":{"kind":"Name","value":"appId"}}},{"kind":"Argument","name":{"kind":"Name","value":"envKeys"},"value":{"kind":"Variable","name":{"kind":"Name","value":"envKeys"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"app"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}}]}}]}}]}}]} as unknown as DocumentNode; +export const RemoveMemberFromAppDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"mutation","name":{"kind":"Name","value":"RemoveMemberFromApp"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"memberId"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"ID"}}}},{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"appId"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"ID"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"removeAppMember"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"memberId"},"value":{"kind":"Variable","name":{"kind":"Name","value":"memberId"}}},{"kind":"Argument","name":{"kind":"Name","value":"appId"},"value":{"kind":"Variable","name":{"kind":"Name","value":"appId"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"app"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}}]}}]}}]}}]} as unknown as DocumentNode; +export const UpdateEnvScopeDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"mutation","name":{"kind":"Name","value":"UpdateEnvScope"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"memberId"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"ID"}}}},{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"appId"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"ID"}}}},{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"envKeys"}},"type":{"kind":"ListType","type":{"kind":"NamedType","name":{"kind":"Name","value":"EnvironmentKeyInput"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"updateMemberEnvironmentScope"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"memberId"},"value":{"kind":"Variable","name":{"kind":"Name","value":"memberId"}}},{"kind":"Argument","name":{"kind":"Name","value":"appId"},"value":{"kind":"Variable","name":{"kind":"Name","value":"appId"}}},{"kind":"Argument","name":{"kind":"Name","value":"envKeys"},"value":{"kind":"Variable","name":{"kind":"Name","value":"envKeys"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"app"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}}]}}]}}]}}]} as unknown as DocumentNode; +export const CreateApplicationDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"mutation","name":{"kind":"Name","value":"CreateApplication"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"id"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"ID"}}}},{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"organisationId"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"ID"}}}},{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"name"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"String"}}}},{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"identityKey"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"String"}}}},{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"appToken"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"String"}}}},{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"appSeed"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"String"}}}},{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"wrappedKeyShare"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"String"}}}},{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"appVersion"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"Int"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"createApp"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"id"},"value":{"kind":"Variable","name":{"kind":"Name","value":"id"}}},{"kind":"Argument","name":{"kind":"Name","value":"organisationId"},"value":{"kind":"Variable","name":{"kind":"Name","value":"organisationId"}}},{"kind":"Argument","name":{"kind":"Name","value":"name"},"value":{"kind":"Variable","name":{"kind":"Name","value":"name"}}},{"kind":"Argument","name":{"kind":"Name","value":"identityKey"},"value":{"kind":"Variable","name":{"kind":"Name","value":"identityKey"}}},{"kind":"Argument","name":{"kind":"Name","value":"appToken"},"value":{"kind":"Variable","name":{"kind":"Name","value":"appToken"}}},{"kind":"Argument","name":{"kind":"Name","value":"appSeed"},"value":{"kind":"Variable","name":{"kind":"Name","value":"appSeed"}}},{"kind":"Argument","name":{"kind":"Name","value":"wrappedKeyShare"},"value":{"kind":"Variable","name":{"kind":"Name","value":"wrappedKeyShare"}}},{"kind":"Argument","name":{"kind":"Name","value":"appVersion"},"value":{"kind":"Variable","name":{"kind":"Name","value":"appVersion"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"app"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"name"}},{"kind":"Field","name":{"kind":"Name","value":"identityKey"}}]}}]}}]}}]} as unknown as DocumentNode; +export const CreateOrgDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"mutation","name":{"kind":"Name","value":"CreateOrg"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"id"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"ID"}}}},{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"name"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"String"}}}},{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"identityKey"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"String"}}}},{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"wrappedKeyring"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"String"}}}},{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"wrappedRecovery"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"String"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"createOrganisation"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"id"},"value":{"kind":"Variable","name":{"kind":"Name","value":"id"}}},{"kind":"Argument","name":{"kind":"Name","value":"name"},"value":{"kind":"Variable","name":{"kind":"Name","value":"name"}}},{"kind":"Argument","name":{"kind":"Name","value":"identityKey"},"value":{"kind":"Variable","name":{"kind":"Name","value":"identityKey"}}},{"kind":"Argument","name":{"kind":"Name","value":"wrappedKeyring"},"value":{"kind":"Variable","name":{"kind":"Name","value":"wrappedKeyring"}}},{"kind":"Argument","name":{"kind":"Name","value":"wrappedRecovery"},"value":{"kind":"Variable","name":{"kind":"Name","value":"wrappedRecovery"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"organisation"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"name"}},{"kind":"Field","name":{"kind":"Name","value":"createdAt"}}]}}]}}]}}]} as unknown as DocumentNode; +export const DeleteApplicationDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"mutation","name":{"kind":"Name","value":"DeleteApplication"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"id"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"ID"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"deleteApp"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"id"},"value":{"kind":"Variable","name":{"kind":"Name","value":"id"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"app"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}}]}}]}}]}}]} as unknown as DocumentNode; +export const CreateEnvDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"mutation","name":{"kind":"Name","value":"CreateEnv"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"input"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"EnvironmentInput"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"createEnvironment"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"environmentData"},"value":{"kind":"Variable","name":{"kind":"Name","value":"input"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"environment"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"name"}},{"kind":"Field","name":{"kind":"Name","value":"createdAt"}},{"kind":"Field","name":{"kind":"Name","value":"identityKey"}}]}}]}}]}}]} as unknown as DocumentNode; +export const CreateEnvKeyDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"mutation","name":{"kind":"Name","value":"CreateEnvKey"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"envId"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"ID"}}}},{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"userId"}},"type":{"kind":"NamedType","name":{"kind":"Name","value":"ID"}}},{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"wrappedSeed"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"String"}}}},{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"wrappedSalt"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"String"}}}},{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"identityKey"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"String"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"createEnvironmentKey"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"envId"},"value":{"kind":"Variable","name":{"kind":"Name","value":"envId"}}},{"kind":"Argument","name":{"kind":"Name","value":"userId"},"value":{"kind":"Variable","name":{"kind":"Name","value":"userId"}}},{"kind":"Argument","name":{"kind":"Name","value":"wrappedSeed"},"value":{"kind":"Variable","name":{"kind":"Name","value":"wrappedSeed"}}},{"kind":"Argument","name":{"kind":"Name","value":"wrappedSalt"},"value":{"kind":"Variable","name":{"kind":"Name","value":"wrappedSalt"}}},{"kind":"Argument","name":{"kind":"Name","value":"identityKey"},"value":{"kind":"Variable","name":{"kind":"Name","value":"identityKey"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"environmentKey"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"createdAt"}}]}}]}}]}}]} as unknown as DocumentNode; +export const CreateEnvTokenDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"mutation","name":{"kind":"Name","value":"CreateEnvToken"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"envId"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"ID"}}}},{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"name"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"String"}}}},{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"identityKey"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"String"}}}},{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"token"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"String"}}}},{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"wrappedKeyShare"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"String"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"createEnvironmentToken"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"envId"},"value":{"kind":"Variable","name":{"kind":"Name","value":"envId"}}},{"kind":"Argument","name":{"kind":"Name","value":"name"},"value":{"kind":"Variable","name":{"kind":"Name","value":"name"}}},{"kind":"Argument","name":{"kind":"Name","value":"identityKey"},"value":{"kind":"Variable","name":{"kind":"Name","value":"identityKey"}}},{"kind":"Argument","name":{"kind":"Name","value":"token"},"value":{"kind":"Variable","name":{"kind":"Name","value":"token"}}},{"kind":"Argument","name":{"kind":"Name","value":"wrappedKeyShare"},"value":{"kind":"Variable","name":{"kind":"Name","value":"wrappedKeyShare"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"environmentToken"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"createdAt"}}]}}]}}]}}]} as unknown as DocumentNode; +export const CreateNewSecretDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"mutation","name":{"kind":"Name","value":"CreateNewSecret"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"newSecret"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"SecretInput"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"createSecret"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"secretData"},"value":{"kind":"Variable","name":{"kind":"Name","value":"newSecret"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"secret"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"key"}},{"kind":"Field","name":{"kind":"Name","value":"value"}},{"kind":"Field","name":{"kind":"Name","value":"createdAt"}}]}}]}}]}}]} as unknown as DocumentNode; +export const CreateNewSecretTagDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"mutation","name":{"kind":"Name","value":"CreateNewSecretTag"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"orgId"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"ID"}}}},{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"name"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"String"}}}},{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"color"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"String"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"createSecretTag"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"orgId"},"value":{"kind":"Variable","name":{"kind":"Name","value":"orgId"}}},{"kind":"Argument","name":{"kind":"Name","value":"name"},"value":{"kind":"Variable","name":{"kind":"Name","value":"name"}}},{"kind":"Argument","name":{"kind":"Name","value":"color"},"value":{"kind":"Variable","name":{"kind":"Name","value":"color"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"tag"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}}]}}]}}]}}]} as unknown as DocumentNode; +export const CreateNewServiceTokenDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"mutation","name":{"kind":"Name","value":"CreateNewServiceToken"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"appId"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"ID"}}}},{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"environmentKeys"}},"type":{"kind":"ListType","type":{"kind":"NamedType","name":{"kind":"Name","value":"EnvironmentKeyInput"}}}},{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"identityKey"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"String"}}}},{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"token"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"String"}}}},{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"wrappedKeyShare"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"String"}}}},{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"name"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"String"}}}},{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"expiry"}},"type":{"kind":"NamedType","name":{"kind":"Name","value":"BigInt"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"createServiceToken"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"appId"},"value":{"kind":"Variable","name":{"kind":"Name","value":"appId"}}},{"kind":"Argument","name":{"kind":"Name","value":"environmentKeys"},"value":{"kind":"Variable","name":{"kind":"Name","value":"environmentKeys"}}},{"kind":"Argument","name":{"kind":"Name","value":"identityKey"},"value":{"kind":"Variable","name":{"kind":"Name","value":"identityKey"}}},{"kind":"Argument","name":{"kind":"Name","value":"token"},"value":{"kind":"Variable","name":{"kind":"Name","value":"token"}}},{"kind":"Argument","name":{"kind":"Name","value":"wrappedKeyShare"},"value":{"kind":"Variable","name":{"kind":"Name","value":"wrappedKeyShare"}}},{"kind":"Argument","name":{"kind":"Name","value":"name"},"value":{"kind":"Variable","name":{"kind":"Name","value":"name"}}},{"kind":"Argument","name":{"kind":"Name","value":"expiry"},"value":{"kind":"Variable","name":{"kind":"Name","value":"expiry"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"serviceToken"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"createdAt"}},{"kind":"Field","name":{"kind":"Name","value":"expiresAt"}}]}}]}}]}}]} as unknown as DocumentNode; +export const DeleteSecretOpDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"mutation","name":{"kind":"Name","value":"DeleteSecretOp"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"id"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"ID"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"deleteSecret"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"id"},"value":{"kind":"Variable","name":{"kind":"Name","value":"id"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"secret"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}}]}}]}}]}}]} as unknown as DocumentNode; +export const RevokeServiceTokenDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"mutation","name":{"kind":"Name","value":"RevokeServiceToken"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"tokenId"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"ID"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"deleteServiceToken"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"tokenId"},"value":{"kind":"Variable","name":{"kind":"Name","value":"tokenId"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"ok"}}]}}]}}]} as unknown as DocumentNode; +export const UpdateSecretDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"mutation","name":{"kind":"Name","value":"UpdateSecret"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"id"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"ID"}}}},{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"secretData"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"SecretInput"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"editSecret"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"id"},"value":{"kind":"Variable","name":{"kind":"Name","value":"id"}}},{"kind":"Argument","name":{"kind":"Name","value":"secretData"},"value":{"kind":"Variable","name":{"kind":"Name","value":"secretData"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"secret"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"updatedAt"}}]}}]}}]}}]} as unknown as DocumentNode; +export const InitAppEnvironmentsDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"mutation","name":{"kind":"Name","value":"InitAppEnvironments"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"devEnv"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"EnvironmentInput"}}}},{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"stagingEnv"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"EnvironmentInput"}}}},{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"prodEnv"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"EnvironmentInput"}}}},{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"devAdminKeys"}},"type":{"kind":"ListType","type":{"kind":"NamedType","name":{"kind":"Name","value":"EnvironmentKeyInput"}}}},{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"stagAdminKeys"}},"type":{"kind":"ListType","type":{"kind":"NamedType","name":{"kind":"Name","value":"EnvironmentKeyInput"}}}},{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"prodAdminKeys"}},"type":{"kind":"ListType","type":{"kind":"NamedType","name":{"kind":"Name","value":"EnvironmentKeyInput"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","alias":{"kind":"Name","value":"devEnvironment"},"name":{"kind":"Name","value":"createEnvironment"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"environmentData"},"value":{"kind":"Variable","name":{"kind":"Name","value":"devEnv"}}},{"kind":"Argument","name":{"kind":"Name","value":"adminKeys"},"value":{"kind":"Variable","name":{"kind":"Name","value":"devAdminKeys"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"environment"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"name"}},{"kind":"Field","name":{"kind":"Name","value":"createdAt"}},{"kind":"Field","name":{"kind":"Name","value":"identityKey"}}]}}]}},{"kind":"Field","alias":{"kind":"Name","value":"stagingEnvironment"},"name":{"kind":"Name","value":"createEnvironment"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"environmentData"},"value":{"kind":"Variable","name":{"kind":"Name","value":"stagingEnv"}}},{"kind":"Argument","name":{"kind":"Name","value":"adminKeys"},"value":{"kind":"Variable","name":{"kind":"Name","value":"stagAdminKeys"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"environment"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"name"}},{"kind":"Field","name":{"kind":"Name","value":"createdAt"}},{"kind":"Field","name":{"kind":"Name","value":"identityKey"}}]}}]}},{"kind":"Field","alias":{"kind":"Name","value":"prodEnvironment"},"name":{"kind":"Name","value":"createEnvironment"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"environmentData"},"value":{"kind":"Variable","name":{"kind":"Name","value":"prodEnv"}}},{"kind":"Argument","name":{"kind":"Name","value":"adminKeys"},"value":{"kind":"Variable","name":{"kind":"Name","value":"prodAdminKeys"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"environment"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"name"}},{"kind":"Field","name":{"kind":"Name","value":"createdAt"}},{"kind":"Field","name":{"kind":"Name","value":"identityKey"}}]}}]}}]}}]} as unknown as DocumentNode; +export const LogSecretReadDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"mutation","name":{"kind":"Name","value":"LogSecretRead"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"id"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"ID"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"readSecret"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"id"},"value":{"kind":"Variable","name":{"kind":"Name","value":"id"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"ok"}}]}}]}}]} as unknown as DocumentNode; +export const AcceptOrganisationInviteDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"mutation","name":{"kind":"Name","value":"AcceptOrganisationInvite"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"orgId"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"ID"}}}},{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"identityKey"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"String"}}}},{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"wrappedKeyring"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"String"}}}},{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"wrappedRecovery"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"String"}}}},{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"inviteId"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"ID"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"createOrganisationMember"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"orgId"},"value":{"kind":"Variable","name":{"kind":"Name","value":"orgId"}}},{"kind":"Argument","name":{"kind":"Name","value":"identityKey"},"value":{"kind":"Variable","name":{"kind":"Name","value":"identityKey"}}},{"kind":"Argument","name":{"kind":"Name","value":"wrappedKeyring"},"value":{"kind":"Variable","name":{"kind":"Name","value":"wrappedKeyring"}}},{"kind":"Argument","name":{"kind":"Name","value":"wrappedRecovery"},"value":{"kind":"Variable","name":{"kind":"Name","value":"wrappedRecovery"}}},{"kind":"Argument","name":{"kind":"Name","value":"inviteId"},"value":{"kind":"Variable","name":{"kind":"Name","value":"inviteId"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"orgMember"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"email"}},{"kind":"Field","name":{"kind":"Name","value":"createdAt"}},{"kind":"Field","name":{"kind":"Name","value":"role"}}]}}]}}]}}]} as unknown as DocumentNode; +export const DeleteOrgInviteDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"mutation","name":{"kind":"Name","value":"DeleteOrgInvite"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"inviteId"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"ID"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"deleteInvitation"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"inviteId"},"value":{"kind":"Variable","name":{"kind":"Name","value":"inviteId"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"ok"}}]}}]}}]} as unknown as DocumentNode; +export const RemoveMemberDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"mutation","name":{"kind":"Name","value":"RemoveMember"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"memberId"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"ID"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"deleteOrganisationMember"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"memberId"},"value":{"kind":"Variable","name":{"kind":"Name","value":"memberId"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"ok"}}]}}]}}]} as unknown as DocumentNode; +export const InviteMemberDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"mutation","name":{"kind":"Name","value":"InviteMember"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"orgId"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"ID"}}}},{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"email"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"String"}}}},{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"apps"}},"type":{"kind":"ListType","type":{"kind":"NamedType","name":{"kind":"Name","value":"String"}}}},{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"role"}},"type":{"kind":"NamedType","name":{"kind":"Name","value":"String"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"inviteOrganisationMember"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"orgId"},"value":{"kind":"Variable","name":{"kind":"Name","value":"orgId"}}},{"kind":"Argument","name":{"kind":"Name","value":"email"},"value":{"kind":"Variable","name":{"kind":"Name","value":"email"}}},{"kind":"Argument","name":{"kind":"Name","value":"apps"},"value":{"kind":"Variable","name":{"kind":"Name","value":"apps"}}},{"kind":"Argument","name":{"kind":"Name","value":"role"},"value":{"kind":"Variable","name":{"kind":"Name","value":"role"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"invite"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}}]}}]}}]}}]} as unknown as DocumentNode; +export const UpdateMemberRoleDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"mutation","name":{"kind":"Name","value":"UpdateMemberRole"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"memberId"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"ID"}}}},{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"role"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"String"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"updateOrganisationMemberRole"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"memberId"},"value":{"kind":"Variable","name":{"kind":"Name","value":"memberId"}}},{"kind":"Argument","name":{"kind":"Name","value":"role"},"value":{"kind":"Variable","name":{"kind":"Name","value":"role"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"orgMember"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"role"}}]}}]}}]}}]} as unknown as DocumentNode; +export const UpdateWrappedSecretsDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"mutation","name":{"kind":"Name","value":"UpdateWrappedSecrets"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"orgId"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"ID"}}}},{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"wrappedKeyring"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"String"}}}},{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"wrappedRecovery"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"String"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"updateMemberWrappedSecrets"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"orgId"},"value":{"kind":"Variable","name":{"kind":"Name","value":"orgId"}}},{"kind":"Argument","name":{"kind":"Name","value":"wrappedKeyring"},"value":{"kind":"Variable","name":{"kind":"Name","value":"wrappedKeyring"}}},{"kind":"Argument","name":{"kind":"Name","value":"wrappedRecovery"},"value":{"kind":"Variable","name":{"kind":"Name","value":"wrappedRecovery"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"orgMember"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}}]}}]}}]}}]} as unknown as DocumentNode; +export const RotateAppKeyDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"mutation","name":{"kind":"Name","value":"RotateAppKey"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"id"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"ID"}}}},{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"appToken"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"String"}}}},{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"wrappedKeyShare"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"String"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"rotateAppKeys"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"id"},"value":{"kind":"Variable","name":{"kind":"Name","value":"id"}}},{"kind":"Argument","name":{"kind":"Name","value":"appToken"},"value":{"kind":"Variable","name":{"kind":"Name","value":"appToken"}}},{"kind":"Argument","name":{"kind":"Name","value":"wrappedKeyShare"},"value":{"kind":"Variable","name":{"kind":"Name","value":"wrappedKeyShare"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"app"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}}]}}]}}]}}]} as unknown as DocumentNode; +export const CreateNewUserTokenDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"mutation","name":{"kind":"Name","value":"CreateNewUserToken"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"orgId"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"ID"}}}},{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"name"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"String"}}}},{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"identityKey"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"String"}}}},{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"token"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"String"}}}},{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"wrappedKeyShare"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"String"}}}},{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"expiry"}},"type":{"kind":"NamedType","name":{"kind":"Name","value":"BigInt"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"createUserToken"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"orgId"},"value":{"kind":"Variable","name":{"kind":"Name","value":"orgId"}}},{"kind":"Argument","name":{"kind":"Name","value":"name"},"value":{"kind":"Variable","name":{"kind":"Name","value":"name"}}},{"kind":"Argument","name":{"kind":"Name","value":"identityKey"},"value":{"kind":"Variable","name":{"kind":"Name","value":"identityKey"}}},{"kind":"Argument","name":{"kind":"Name","value":"token"},"value":{"kind":"Variable","name":{"kind":"Name","value":"token"}}},{"kind":"Argument","name":{"kind":"Name","value":"wrappedKeyShare"},"value":{"kind":"Variable","name":{"kind":"Name","value":"wrappedKeyShare"}}},{"kind":"Argument","name":{"kind":"Name","value":"expiry"},"value":{"kind":"Variable","name":{"kind":"Name","value":"expiry"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"ok"}}]}}]}}]} as unknown as DocumentNode; +export const RevokeUserTokenDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"mutation","name":{"kind":"Name","value":"RevokeUserToken"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"tokenId"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"ID"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"deleteUserToken"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"tokenId"},"value":{"kind":"Variable","name":{"kind":"Name","value":"tokenId"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"ok"}}]}}]}}]} as unknown as DocumentNode; +export const GetAppMembersDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"query","name":{"kind":"Name","value":"GetAppMembers"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"appId"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"ID"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"appUsers"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"appId"},"value":{"kind":"Variable","name":{"kind":"Name","value":"appId"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"identityKey"}},{"kind":"Field","name":{"kind":"Name","value":"email"}},{"kind":"Field","name":{"kind":"Name","value":"fullName"}},{"kind":"Field","name":{"kind":"Name","value":"avatarUrl"}},{"kind":"Field","name":{"kind":"Name","value":"createdAt"}},{"kind":"Field","name":{"kind":"Name","value":"role"}}]}}]}}]} as unknown as DocumentNode; +export const GetAppActivityChartDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"query","name":{"kind":"Name","value":"GetAppActivityChart"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"appId"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"ID"}}}},{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"period"}},"type":{"kind":"NamedType","name":{"kind":"Name","value":"TimeRange"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"appActivityChart"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"appId"},"value":{"kind":"Variable","name":{"kind":"Name","value":"appId"}}},{"kind":"Argument","name":{"kind":"Name","value":"period"},"value":{"kind":"Variable","name":{"kind":"Name","value":"period"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"index"}},{"kind":"Field","name":{"kind":"Name","value":"date"}},{"kind":"Field","name":{"kind":"Name","value":"data"}}]}}]}}]} as unknown as DocumentNode; +export const GetAppDetailDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"query","name":{"kind":"Name","value":"GetAppDetail"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"organisationId"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"ID"}}}},{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"appId"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"ID"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"apps"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"organisationId"},"value":{"kind":"Variable","name":{"kind":"Name","value":"organisationId"}}},{"kind":"Argument","name":{"kind":"Name","value":"appId"},"value":{"kind":"Variable","name":{"kind":"Name","value":"appId"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"name"}},{"kind":"Field","name":{"kind":"Name","value":"identityKey"}},{"kind":"Field","name":{"kind":"Name","value":"createdAt"}},{"kind":"Field","name":{"kind":"Name","value":"appToken"}},{"kind":"Field","name":{"kind":"Name","value":"appSeed"}},{"kind":"Field","name":{"kind":"Name","value":"appVersion"}}]}}]}}]} as unknown as DocumentNode; +export const GetAppKmsLogsDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"query","name":{"kind":"Name","value":"GetAppKmsLogs"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"appId"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"ID"}}}},{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"start"}},"type":{"kind":"NamedType","name":{"kind":"Name","value":"BigInt"}}},{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"end"}},"type":{"kind":"NamedType","name":{"kind":"Name","value":"BigInt"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"logs"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"appId"},"value":{"kind":"Variable","name":{"kind":"Name","value":"appId"}}},{"kind":"Argument","name":{"kind":"Name","value":"start"},"value":{"kind":"Variable","name":{"kind":"Name","value":"start"}}},{"kind":"Argument","name":{"kind":"Name","value":"end"},"value":{"kind":"Variable","name":{"kind":"Name","value":"end"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"kms"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"timestamp"}},{"kind":"Field","name":{"kind":"Name","value":"phaseNode"}},{"kind":"Field","name":{"kind":"Name","value":"eventType"}},{"kind":"Field","name":{"kind":"Name","value":"ipAddress"}},{"kind":"Field","name":{"kind":"Name","value":"country"}},{"kind":"Field","name":{"kind":"Name","value":"city"}},{"kind":"Field","name":{"kind":"Name","value":"phSize"}}]}}]}},{"kind":"Field","name":{"kind":"Name","value":"kmsLogsCount"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"appId"},"value":{"kind":"Variable","name":{"kind":"Name","value":"appId"}}}]}]}}]} as unknown as DocumentNode; +export const GetAppsDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"query","name":{"kind":"Name","value":"GetApps"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"organisationId"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"ID"}}}},{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"appId"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"ID"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"apps"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"organisationId"},"value":{"kind":"Variable","name":{"kind":"Name","value":"organisationId"}}},{"kind":"Argument","name":{"kind":"Name","value":"appId"},"value":{"kind":"Variable","name":{"kind":"Name","value":"appId"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"name"}},{"kind":"Field","name":{"kind":"Name","value":"identityKey"}},{"kind":"Field","name":{"kind":"Name","value":"createdAt"}}]}}]}}]} as unknown as DocumentNode; +export const GetOrganisationsDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"query","name":{"kind":"Name","value":"GetOrganisations"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"organisations"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"name"}},{"kind":"Field","name":{"kind":"Name","value":"identityKey"}},{"kind":"Field","name":{"kind":"Name","value":"createdAt"}},{"kind":"Field","name":{"kind":"Name","value":"plan"}},{"kind":"Field","name":{"kind":"Name","value":"role"}},{"kind":"Field","name":{"kind":"Name","value":"memberId"}},{"kind":"Field","name":{"kind":"Name","value":"keyring"}},{"kind":"Field","name":{"kind":"Name","value":"recovery"}}]}}]}}]} as unknown as DocumentNode; +export const GetInvitesDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"query","name":{"kind":"Name","value":"GetInvites"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"orgId"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"ID"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"organisationInvites"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"orgId"},"value":{"kind":"Variable","name":{"kind":"Name","value":"orgId"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"createdAt"}},{"kind":"Field","name":{"kind":"Name","value":"expiresAt"}},{"kind":"Field","name":{"kind":"Name","value":"invitedBy"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"email"}},{"kind":"Field","name":{"kind":"Name","value":"fullName"}},{"kind":"Field","name":{"kind":"Name","value":"self"}}]}},{"kind":"Field","name":{"kind":"Name","value":"inviteeEmail"}}]}}]}}]} as unknown as DocumentNode; +export const GetOrganisationAdminsAndSelfDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"query","name":{"kind":"Name","value":"GetOrganisationAdminsAndSelf"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"organisationId"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"ID"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"organisationAdminsAndSelf"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"organisationId"},"value":{"kind":"Variable","name":{"kind":"Name","value":"organisationId"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"role"}},{"kind":"Field","name":{"kind":"Name","value":"identityKey"}},{"kind":"Field","name":{"kind":"Name","value":"self"}}]}}]}}]} as unknown as DocumentNode; +export const GetOrganisationMembersDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"query","name":{"kind":"Name","value":"GetOrganisationMembers"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"organisationId"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"ID"}}}},{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"role"}},"type":{"kind":"ListType","type":{"kind":"NamedType","name":{"kind":"Name","value":"String"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"organisationMembers"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"organisationId"},"value":{"kind":"Variable","name":{"kind":"Name","value":"organisationId"}}},{"kind":"Argument","name":{"kind":"Name","value":"role"},"value":{"kind":"Variable","name":{"kind":"Name","value":"role"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"role"}},{"kind":"Field","name":{"kind":"Name","value":"identityKey"}},{"kind":"Field","name":{"kind":"Name","value":"email"}},{"kind":"Field","name":{"kind":"Name","value":"fullName"}},{"kind":"Field","name":{"kind":"Name","value":"avatarUrl"}},{"kind":"Field","name":{"kind":"Name","value":"createdAt"}},{"kind":"Field","name":{"kind":"Name","value":"self"}}]}}]}}]} as unknown as DocumentNode; +export const VerifyInviteDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"query","name":{"kind":"Name","value":"VerifyInvite"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"inviteId"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"ID"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"validateInvite"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"inviteId"},"value":{"kind":"Variable","name":{"kind":"Name","value":"inviteId"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"organisation"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"name"}}]}},{"kind":"Field","name":{"kind":"Name","value":"inviteeEmail"}},{"kind":"Field","name":{"kind":"Name","value":"invitedBy"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"email"}}]}},{"kind":"Field","name":{"kind":"Name","value":"apps"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"name"}}]}}]}}]}}]} as unknown as DocumentNode; +export const GetAppEnvironmentsDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"query","name":{"kind":"Name","value":"GetAppEnvironments"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"appId"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"ID"}}}},{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"memberId"}},"type":{"kind":"NamedType","name":{"kind":"Name","value":"ID"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"appEnvironments"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"appId"},"value":{"kind":"Variable","name":{"kind":"Name","value":"appId"}}},{"kind":"Argument","name":{"kind":"Name","value":"environmentId"},"value":{"kind":"NullValue"}},{"kind":"Argument","name":{"kind":"Name","value":"memberId"},"value":{"kind":"Variable","name":{"kind":"Name","value":"memberId"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"name"}},{"kind":"Field","name":{"kind":"Name","value":"envType"}},{"kind":"Field","name":{"kind":"Name","value":"identityKey"}},{"kind":"Field","name":{"kind":"Name","value":"wrappedSeed"}},{"kind":"Field","name":{"kind":"Name","value":"wrappedSalt"}},{"kind":"Field","name":{"kind":"Name","value":"createdAt"}}]}}]}}]} as unknown as DocumentNode; +export const GetAppSecretsLogsDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"query","name":{"kind":"Name","value":"GetAppSecretsLogs"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"appId"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"ID"}}}},{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"start"}},"type":{"kind":"NamedType","name":{"kind":"Name","value":"BigInt"}}},{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"end"}},"type":{"kind":"NamedType","name":{"kind":"Name","value":"BigInt"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"logs"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"appId"},"value":{"kind":"Variable","name":{"kind":"Name","value":"appId"}}},{"kind":"Argument","name":{"kind":"Name","value":"start"},"value":{"kind":"Variable","name":{"kind":"Name","value":"start"}}},{"kind":"Argument","name":{"kind":"Name","value":"end"},"value":{"kind":"Variable","name":{"kind":"Name","value":"end"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"secrets"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"key"}},{"kind":"Field","name":{"kind":"Name","value":"value"}},{"kind":"Field","name":{"kind":"Name","value":"tags"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"name"}},{"kind":"Field","name":{"kind":"Name","value":"color"}}]}},{"kind":"Field","name":{"kind":"Name","value":"version"}},{"kind":"Field","name":{"kind":"Name","value":"comment"}},{"kind":"Field","name":{"kind":"Name","value":"timestamp"}},{"kind":"Field","name":{"kind":"Name","value":"ipAddress"}},{"kind":"Field","name":{"kind":"Name","value":"userAgent"}},{"kind":"Field","name":{"kind":"Name","value":"user"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"email"}},{"kind":"Field","name":{"kind":"Name","value":"username"}},{"kind":"Field","name":{"kind":"Name","value":"fullName"}},{"kind":"Field","name":{"kind":"Name","value":"avatarUrl"}}]}},{"kind":"Field","name":{"kind":"Name","value":"eventType"}},{"kind":"Field","name":{"kind":"Name","value":"environment"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"envType"}},{"kind":"Field","name":{"kind":"Name","value":"name"}}]}},{"kind":"Field","name":{"kind":"Name","value":"secret"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}}]}}]}}]}},{"kind":"Field","name":{"kind":"Name","value":"secretsLogsCount"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"appId"},"value":{"kind":"Variable","name":{"kind":"Name","value":"appId"}}}]},{"kind":"Field","name":{"kind":"Name","value":"environmentKeys"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"appId"},"value":{"kind":"Variable","name":{"kind":"Name","value":"appId"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"identityKey"}},{"kind":"Field","name":{"kind":"Name","value":"wrappedSeed"}},{"kind":"Field","name":{"kind":"Name","value":"wrappedSalt"}},{"kind":"Field","name":{"kind":"Name","value":"environment"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}}]}}]}}]}}]} as unknown as DocumentNode; +export const GetEnvironmentKeyDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"query","name":{"kind":"Name","value":"GetEnvironmentKey"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"envId"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"ID"}}}},{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"appId"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"ID"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"environmentKeys"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"environmentId"},"value":{"kind":"Variable","name":{"kind":"Name","value":"envId"}}},{"kind":"Argument","name":{"kind":"Name","value":"appId"},"value":{"kind":"Variable","name":{"kind":"Name","value":"appId"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"identityKey"}},{"kind":"Field","name":{"kind":"Name","value":"wrappedSeed"}},{"kind":"Field","name":{"kind":"Name","value":"wrappedSalt"}}]}}]}}]} as unknown as DocumentNode; +export const GetEnvironmentTokensDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"query","name":{"kind":"Name","value":"GetEnvironmentTokens"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"envId"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"ID"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"environmentTokens"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"environmentId"},"value":{"kind":"Variable","name":{"kind":"Name","value":"envId"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"name"}},{"kind":"Field","name":{"kind":"Name","value":"wrappedKeyShare"}},{"kind":"Field","name":{"kind":"Name","value":"createdAt"}}]}}]}}]} as unknown as DocumentNode; +export const GetEnvSecretsKvDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"query","name":{"kind":"Name","value":"GetEnvSecretsKV"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"envId"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"ID"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"secrets"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"envId"},"value":{"kind":"Variable","name":{"kind":"Name","value":"envId"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"key"}},{"kind":"Field","name":{"kind":"Name","value":"value"}}]}},{"kind":"Field","name":{"kind":"Name","value":"environmentKeys"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"environmentId"},"value":{"kind":"Variable","name":{"kind":"Name","value":"envId"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"identityKey"}},{"kind":"Field","name":{"kind":"Name","value":"wrappedSeed"}},{"kind":"Field","name":{"kind":"Name","value":"wrappedSalt"}}]}}]}}]} as unknown as DocumentNode; +export const GetSecretTagsDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"query","name":{"kind":"Name","value":"GetSecretTags"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"orgId"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"ID"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"secretTags"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"orgId"},"value":{"kind":"Variable","name":{"kind":"Name","value":"orgId"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"name"}},{"kind":"Field","name":{"kind":"Name","value":"color"}}]}}]}}]} as unknown as DocumentNode; +export const GetSecretsDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"query","name":{"kind":"Name","value":"GetSecrets"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"appId"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"ID"}}}},{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"envId"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"ID"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"secrets"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"envId"},"value":{"kind":"Variable","name":{"kind":"Name","value":"envId"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"key"}},{"kind":"Field","name":{"kind":"Name","value":"value"}},{"kind":"Field","name":{"kind":"Name","value":"tags"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"name"}},{"kind":"Field","name":{"kind":"Name","value":"color"}}]}},{"kind":"Field","name":{"kind":"Name","value":"comment"}},{"kind":"Field","name":{"kind":"Name","value":"createdAt"}},{"kind":"Field","name":{"kind":"Name","value":"history"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"key"}},{"kind":"Field","name":{"kind":"Name","value":"value"}},{"kind":"Field","name":{"kind":"Name","value":"tags"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"name"}},{"kind":"Field","name":{"kind":"Name","value":"color"}}]}},{"kind":"Field","name":{"kind":"Name","value":"version"}},{"kind":"Field","name":{"kind":"Name","value":"comment"}},{"kind":"Field","name":{"kind":"Name","value":"timestamp"}},{"kind":"Field","name":{"kind":"Name","value":"ipAddress"}},{"kind":"Field","name":{"kind":"Name","value":"userAgent"}},{"kind":"Field","name":{"kind":"Name","value":"user"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"email"}},{"kind":"Field","name":{"kind":"Name","value":"username"}},{"kind":"Field","name":{"kind":"Name","value":"fullName"}},{"kind":"Field","name":{"kind":"Name","value":"avatarUrl"}}]}},{"kind":"Field","name":{"kind":"Name","value":"eventType"}}]}}]}},{"kind":"Field","name":{"kind":"Name","value":"appEnvironments"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"appId"},"value":{"kind":"Variable","name":{"kind":"Name","value":"appId"}}},{"kind":"Argument","name":{"kind":"Name","value":"environmentId"},"value":{"kind":"Variable","name":{"kind":"Name","value":"envId"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"name"}},{"kind":"Field","name":{"kind":"Name","value":"envType"}},{"kind":"Field","name":{"kind":"Name","value":"identityKey"}}]}},{"kind":"Field","name":{"kind":"Name","value":"environmentKeys"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"appId"},"value":{"kind":"Variable","name":{"kind":"Name","value":"appId"}}},{"kind":"Argument","name":{"kind":"Name","value":"environmentId"},"value":{"kind":"Variable","name":{"kind":"Name","value":"envId"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"identityKey"}},{"kind":"Field","name":{"kind":"Name","value":"wrappedSeed"}},{"kind":"Field","name":{"kind":"Name","value":"wrappedSalt"}}]}}]}}]} as unknown as DocumentNode; +export const GetServiceTokensDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"query","name":{"kind":"Name","value":"GetServiceTokens"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"appId"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"ID"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"serviceTokens"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"appId"},"value":{"kind":"Variable","name":{"kind":"Name","value":"appId"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"name"}},{"kind":"Field","name":{"kind":"Name","value":"createdAt"}},{"kind":"Field","name":{"kind":"Name","value":"createdBy"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"fullName"}},{"kind":"Field","name":{"kind":"Name","value":"avatarUrl"}},{"kind":"Field","name":{"kind":"Name","value":"self"}}]}},{"kind":"Field","name":{"kind":"Name","value":"expiresAt"}},{"kind":"Field","name":{"kind":"Name","value":"keys"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}}]}}]}}]}}]} as unknown as DocumentNode; +export const GetUserTokensDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"query","name":{"kind":"Name","value":"GetUserTokens"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"organisationId"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"ID"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"userTokens"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"organisationId"},"value":{"kind":"Variable","name":{"kind":"Name","value":"organisationId"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"name"}},{"kind":"Field","name":{"kind":"Name","value":"wrappedKeyShare"}},{"kind":"Field","name":{"kind":"Name","value":"createdAt"}},{"kind":"Field","name":{"kind":"Name","value":"expiresAt"}}]}}]}}]} as unknown as DocumentNode; \ No newline at end of file diff --git a/frontend/apollo/mutations/createOrganisation.gql b/frontend/apollo/mutations/createOrganisation.gql deleted file mode 100644 index ab5b27b4..00000000 --- a/frontend/apollo/mutations/createOrganisation.gql +++ /dev/null @@ -1,9 +0,0 @@ -mutation CreateOrganisation($id: ID!, $name: String!, $identityKey: String!) { - createOrganisation(id: $id, name: $name, identityKey: $identityKey) { - organisation { - id - name - createdAt - } - } -} diff --git a/frontend/apollo/queries/getAppLogCount.gql b/frontend/apollo/queries/getAppLogCount.gql deleted file mode 100644 index 43023509..00000000 --- a/frontend/apollo/queries/getAppLogCount.gql +++ /dev/null @@ -1,3 +0,0 @@ -query GetAppLogCount($appId: ID!, $thisMonth: Boolean) { - logsCount(appId: $appId, thisMonth: $thisMonth) -} diff --git a/frontend/apollo/queries/getAppLogs.gql b/frontend/apollo/queries/getAppLogs.gql deleted file mode 100644 index c551a6ea..00000000 --- a/frontend/apollo/queries/getAppLogs.gql +++ /dev/null @@ -1,13 +0,0 @@ -query GetAppLogs($appId: ID!, $start: BigInt, $end: BigInt) { - logs(appId: $appId, start: $start, end: $end) { - id - timestamp - phaseNode - eventType - ipAddress - country - city - phSize - } - logsCount(appId: $appId) -} diff --git a/frontend/apollo/schema.graphql b/frontend/apollo/schema.graphql index 7aaf4a12..620916db 100644 --- a/frontend/apollo/schema.graphql +++ b/frontend/apollo/schema.graphql @@ -1,9 +1,23 @@ type Query { organisations: [OrganisationType] + organisationMembers(organisationId: ID, userId: ID, role: [String]): [OrganisationMemberType] + organisationAdminsAndSelf(organisationId: ID): [OrganisationMemberType] + organisationInvites(orgId: ID): [OrganisationMemberInviteType] + validateInvite(inviteId: ID): OrganisationMemberInviteType apps(organisationId: ID, appId: ID): [AppType] - logs(appId: ID, start: BigInt, end: BigInt): [KMSLogType] - logsCount(appId: ID, thisMonth: Boolean): Int + logs(appId: ID, start: BigInt, end: BigInt): LogsResponseType + kmsLogsCount(appId: ID, thisMonth: Boolean): Int + secretsLogsCount(appId: ID): Int appActivityChart(appId: ID, period: TimeRange): [ChartDataPointType] + appEnvironments(appId: ID, environmentId: ID, memberId: ID): [EnvironmentType] + appUsers(appId: ID): [OrganisationMemberType] + secrets(envId: ID): [SecretType] + secretHistory(secretId: ID): [SecretEventType] + secretTags(orgId: ID): [SecretTagType] + environmentKeys(appId: ID, environmentId: ID, memberId: ID): [EnvironmentKeyType] + environmentTokens(environmentId: ID): [EnvironmentTokenType] + userTokens(organisationId: ID): [UserTokenType] + serviceTokens(appId: ID): [ServiceTokenType] } type OrganisationType { @@ -12,6 +26,10 @@ type OrganisationType { identityKey: String! createdAt: DateTime plan: ApiOrganisationPlanChoices! + role: String + memberId: ID + keyring: String + recovery: String } """ @@ -33,6 +51,45 @@ enum ApiOrganisationPlanChoices { EN } +type OrganisationMemberType { + id: String! + role: ApiOrganisationMemberRoleChoices! + identityKey: String + wrappedKeyring: String! + createdAt: DateTime + updatedAt: DateTime! + email: String + username: String + fullName: String + avatarUrl: String + self: Boolean +} + +"""An enumeration.""" +enum ApiOrganisationMemberRoleChoices { + """Owner""" + OWNER + + """Admin""" + ADMIN + + """Developer""" + DEV +} + +type OrganisationMemberInviteType { + id: String! + organisation: OrganisationType! + apps: [AppType!]! + role: ApiOrganisationMemberInviteRoleChoices! + invitedBy: OrganisationMemberType! + inviteeEmail: String! + valid: Boolean! + createdAt: DateTime + updatedAt: DateTime! + expiresAt: DateTime! +} + type AppType { id: String! name: String! @@ -44,6 +101,23 @@ type AppType { createdAt: DateTime } +"""An enumeration.""" +enum ApiOrganisationMemberInviteRoleChoices { + """Owner""" + OWNER + + """Admin""" + ADMIN + + """Developer""" + DEV +} + +type LogsResponseType { + kms: [KMSLogType] + secrets: [SecretEventType] +} + type KMSLogType implements Node { id: ID! timestamp: BigInt @@ -74,6 +148,86 @@ compatible type. """ scalar BigInt +type SecretEventType { + id: String! + secret: SecretType! + environment: EnvironmentType! + user: OrganisationMemberType + key: String! + value: String! + version: Int! + tags: [SecretTagType!]! + comment: String! + eventType: ApiSecretEventEventTypeChoices! + timestamp: DateTime! + ipAddress: String + userAgent: String +} + +type SecretType { + id: String! + folder: SecretFolderType + key: String! + value: String! + version: Int! + tags: [SecretTagType!]! + comment: String! + createdAt: DateTime + updatedAt: DateTime! + history: [SecretEventType] +} + +type SecretFolderType { + id: String! + name: String! + createdAt: DateTime + updatedAt: DateTime! +} + +type SecretTagType { + id: String! + name: String! + color: String! +} + +type EnvironmentType { + id: String! + name: String! + envType: ApiEnvironmentEnvTypeChoices! + identityKey: String! + wrappedSeed: String! + wrappedSalt: String! + createdAt: DateTime + updatedAt: DateTime! +} + +"""An enumeration.""" +enum ApiEnvironmentEnvTypeChoices { + """Development""" + DEV + + """Staging""" + STAGING + + """Production""" + PROD +} + +"""An enumeration.""" +enum ApiSecretEventEventTypeChoices { + """Create""" + C + + """Read""" + R + + """Update""" + U + + """Delete""" + D +} + type ChartDataPointType { index: Int date: BigInt @@ -90,17 +244,107 @@ enum TimeRange { ALL_TIME } +type EnvironmentKeyType { + id: String! + environment: EnvironmentType! + identityKey: String! + wrappedSeed: String! + wrappedSalt: String! + createdAt: DateTime + updatedAt: DateTime! +} + +type EnvironmentTokenType { + id: String! + name: String! + identityKey: String! + token: String! + wrappedKeyShare: String! + createdAt: DateTime + updatedAt: DateTime! +} + +type UserTokenType { + id: String! + name: String! + identityKey: String! + token: String! + wrappedKeyShare: String! + createdAt: DateTime + updatedAt: DateTime! + expiresAt: DateTime +} + +type ServiceTokenType { + id: String! + keys: [EnvironmentKeyType!]! + identityKey: String! + token: String! + wrappedKeyShare: String! + name: String! + createdBy: OrganisationMemberType + createdAt: DateTime + updatedAt: DateTime! + expiresAt: DateTime +} + type Mutation { - createOrganisation(id: ID!, identityKey: String!, name: String!): CreateOrganisationMutation + createOrganisation(id: ID!, identityKey: String!, name: String!, wrappedKeyring: String!, wrappedRecovery: String!): CreateOrganisationMutation + inviteOrganisationMember(apps: [String], email: String!, orgId: ID!, role: String): InviteOrganisationMemberMutation + createOrganisationMember(identityKey: String!, inviteId: ID!, orgId: ID!, wrappedKeyring: String, wrappedRecovery: String): CreateOrganisationMemberMutation + deleteOrganisationMember(memberId: ID!): DeleteOrganisationMemberMutation + updateOrganisationMemberRole(memberId: ID!, role: String!): UpdateOrganisationMemberRole + updateMemberWrappedSecrets(orgId: ID!, wrappedKeyring: String!, wrappedRecovery: String!): UpdateUserWrappedSecretsMutation + deleteInvitation(inviteId: ID!): DeleteInviteMutation createApp(appSeed: String!, appToken: String!, appVersion: Int!, id: ID!, identityKey: String!, name: String!, organisationId: ID!, wrappedKeyShare: String!): CreateAppMutation rotateAppKeys(appToken: String!, id: ID!, wrappedKeyShare: String!): RotateAppKeysMutation deleteApp(id: ID!): DeleteAppMutation + addAppMember(appId: ID, envKeys: [EnvironmentKeyInput], memberId: ID): AddAppMemberMutation + removeAppMember(appId: ID, memberId: ID): RemoveAppMemberMutation + updateMemberEnvironmentScope(appId: ID, envKeys: [EnvironmentKeyInput], memberId: ID): UpdateMemberEnvScopeMutation + createEnvironment(adminKeys: [EnvironmentKeyInput], environmentData: EnvironmentInput!): CreateEnvironmentMutation + createEnvironmentKey(envId: ID!, identityKey: String!, userId: ID, wrappedSalt: String!, wrappedSeed: String!): CreateEnvironmentKeyMutation + createEnvironmentToken(envId: ID!, identityKey: String!, name: String!, token: String!, wrappedKeyShare: String!): CreateEnvironmentTokenMutation + createUserToken(expiry: BigInt, identityKey: String!, name: String!, orgId: ID!, token: String!, wrappedKeyShare: String!): CreateUserTokenMutation + deleteUserToken(tokenId: ID!): DeleteUserTokenMutation + createServiceToken(appId: ID!, environmentKeys: [EnvironmentKeyInput], expiry: BigInt, identityKey: String!, name: String!, token: String!, wrappedKeyShare: String!): CreateServiceTokenMutation + deleteServiceToken(tokenId: ID!): DeleteServiceTokenMutation + createSecretFolder(envId: ID!, id: ID!, name: String!, parentFolderId: ID): CreateSecretFolderMutation + createSecretTag(color: String!, name: String!, orgId: ID!): CreateSecretTagMutation + createSecret(secretData: SecretInput): CreateSecretMutation + editSecret(id: ID!, secretData: SecretInput): EditSecretMutation + deleteSecret(id: ID!): DeleteSecretMutation + readSecret(id: ID!): ReadSecretMutation } type CreateOrganisationMutation { organisation: OrganisationType } +type InviteOrganisationMemberMutation { + invite: OrganisationMemberInviteType +} + +type CreateOrganisationMemberMutation { + orgMember: OrganisationMemberType +} + +type DeleteOrganisationMemberMutation { + ok: Boolean +} + +type UpdateOrganisationMemberRole { + orgMember: OrganisationMemberType +} + +type UpdateUserWrappedSecretsMutation { + orgMember: OrganisationMemberType +} + +type DeleteInviteMutation { + ok: Boolean +} + type CreateAppMutation { app: AppType } @@ -111,4 +355,96 @@ type RotateAppKeysMutation { type DeleteAppMutation { app: AppType +} + +type AddAppMemberMutation { + app: AppType +} + +input EnvironmentKeyInput { + envId: ID! + userId: ID + identityKey: String! + wrappedSeed: String! + wrappedSalt: String! +} + +type RemoveAppMemberMutation { + app: AppType +} + +type UpdateMemberEnvScopeMutation { + app: AppType +} + +type CreateEnvironmentMutation { + environment: EnvironmentType +} + +input EnvironmentInput { + appId: ID! + name: String! + envType: String! + wrappedSeed: String! + wrappedSalt: String! + identityKey: String! +} + +type CreateEnvironmentKeyMutation { + environmentKey: EnvironmentKeyType +} + +type CreateEnvironmentTokenMutation { + environmentToken: EnvironmentTokenType +} + +type CreateUserTokenMutation { + ok: Boolean + userToken: UserTokenType +} + +type DeleteUserTokenMutation { + ok: Boolean +} + +type CreateServiceTokenMutation { + serviceToken: ServiceTokenType +} + +type DeleteServiceTokenMutation { + ok: Boolean +} + +type CreateSecretFolderMutation { + folder: SecretFolderType +} + +type CreateSecretTagMutation { + tag: SecretTagType +} + +type CreateSecretMutation { + secret: SecretType +} + +input SecretInput { + envId: ID + folderId: ID + key: String! + keyDigest: String! + value: String! + tags: [String] + comment: String +} + +type EditSecretMutation { + secret: SecretType +} + +type DeleteSecretMutation { + secret: SecretType +} + +type ReadSecretMutation { + ok: Boolean } \ No newline at end of file diff --git a/frontend/app/[team]/apps/[app]/environments/[environment]/page.tsx b/frontend/app/[team]/apps/[app]/environments/[environment]/page.tsx new file mode 100644 index 00000000..e9947101 --- /dev/null +++ b/frontend/app/[team]/apps/[app]/environments/[environment]/page.tsx @@ -0,0 +1,618 @@ +'use client' + +import { EnvironmentType, SecretInput, SecretType } from '@/apollo/graphql' +import UnlockKeyringDialog from '@/components/auth/UnlockKeyringDialog' +import { KeyringContext } from '@/contexts/keyringContext' +import { GetSecrets } from '@/graphql/queries/secrets/getSecrets.gql' +import { CreateNewSecret } from '@/graphql/mutations/environments/createSecret.gql' +import { UpdateSecret } from '@/graphql/mutations/environments/editSecret.gql' +import { DeleteSecretOp } from '@/graphql/mutations/environments/deleteSecret.gql' +import { GetAppEnvironments } from '@/graphql/queries/secrets/getAppEnvironments.gql' +import { + getUserKxPublicKey, + getUserKxPrivateKey, + decryptAsymmetric, + digest, + encryptAsymmetric, +} from '@/utils/crypto' +import { arraysEqual, envKeyring } from '@/utils/environments' +import { useMutation, useQuery } from '@apollo/client' +import { Fragment, useContext, useEffect, useRef, useState } from 'react' +import { Button } from '@/components/common/Button' +import { + FaChevronDown, + FaDownload, + FaExchangeAlt, + FaPlus, + FaSearch, + FaTimesCircle, + FaUndo, +} from 'react-icons/fa' +import SecretRow from '@/components/environments/SecretRow' +import clsx from 'clsx' +import { toast } from 'react-toastify' +import { organisationContext } from '@/contexts/organisationContext' +import { Menu, Transition } from '@headlessui/react' +import { usePathname, useSearchParams } from 'next/navigation' +import Link from 'next/link' +import { Alert } from '@/components/common/Alert' + +type EnvKeyring = { + privateKey: string + publicKey: string + salt: string +} + +export default function Environment({ + params, +}: { + params: { team: string; app: string; environment: string } +}) { + const { keyring } = useContext(KeyringContext) + const pathname = usePathname() + const searchParams = useSearchParams() + + const secretToHighlight = searchParams.get('secret') + const highlightedRef = useRef(null) + + const [envKeys, setEnvKeys] = useState(null) + const [secrets, setSecrets] = useState([]) + const [updatedSecrets, updateSecrets] = useState([]) + const [searchQuery, setSearchQuery] = useState('') + const [isLoading, setIsloading] = useState(false) + + const { activeOrganisation: organisation } = useContext(organisationContext) + + useEffect(() => { + // 2. Scroll into view when secretToHighlight changes + if (highlightedRef.current && secrets.length > 0) { + highlightedRef.current.scrollIntoView({ + behavior: 'smooth', + block: 'nearest', + inline: 'nearest', + }) + } + }, [secretToHighlight, secrets]) + + const unsavedChanges = + secrets.length !== updatedSecrets.length || + secrets.some((secret, index) => { + const updatedSecret = updatedSecrets[index] + + // Compare secret properties (comment, key, tags, value) + return ( + secret.comment !== updatedSecret.comment || + secret.key !== updatedSecret.key || + !arraysEqual(secret.tags, updatedSecret.tags) || + secret.value !== updatedSecret.value + ) + }) + + const { data: appEnvsData } = useQuery(GetAppEnvironments, { + variables: { + appId: params.app, + }, + }) + + const { data, loading } = useQuery(GetSecrets, { + variables: { + appId: params.app, + envId: params.environment, + }, + pollInterval: unsavedChanges ? 0 : 5000, + }) + + const savingAndFetching = isLoading || loading + + const [createSecret] = useMutation(CreateNewSecret) + const [updateSecret] = useMutation(UpdateSecret) + const [deleteSecret] = useMutation(DeleteSecretOp) + + const envPath = (env: EnvironmentType) => { + const pathSegments = pathname!.split('/') + pathSegments[pathSegments.length - 1] = env.id + return pathSegments?.join('/') + } + + const environment = data?.appEnvironments[0] as EnvironmentType + + const envLinks = + appEnvsData?.appEnvironments + .filter((env: EnvironmentType) => env.name !== environment?.name) + .map((env: EnvironmentType) => { + return { + label: env.name, + href: envPath(env), + } + }) ?? [] + + const handleAddSecret = (start: boolean = true) => { + const newSecret = { + id: `new-${crypto.randomUUID()}`, + updatedAt: null, + version: 1, + key: '', + value: '', + tags: [], + comment: '', + } as SecretType + start + ? updateSecrets([newSecret, ...updatedSecrets]) + : updateSecrets([...updatedSecrets, newSecret]) + } + + const handleUpdateSecret = async (secret: SecretType) => { + const { id, key, value, comment, tags } = secret + + const encryptedKey = await encryptAsymmetric(key, environment.identityKey) + const encryptedValue = await encryptAsymmetric(value, environment.identityKey) + const keyDigest = await digest(key, envKeys!.salt) + const encryptedComment = await encryptAsymmetric(comment, environment.identityKey) + const tagIds = tags.map((tag) => tag.id) + + if (id.split('-')[0] === 'new') { + await createSecret({ + variables: { + newSecret: { + envId: params.environment, + key: encryptedKey, + keyDigest, + value: encryptedValue, + folderId: null, + comment: encryptedComment, + tags: tagIds, + } as SecretInput, + }, + refetchQueries: [ + { + query: GetSecrets, + variables: { + appId: params.app, + envId: params.environment, + }, + }, + ], + }) + } else { + await updateSecret({ + variables: { + id, + secretData: { + key: encryptedKey, + keyDigest, + value: encryptedValue, + folderId: null, + comment: encryptedComment, + tags: tagIds, + } as SecretInput, + }, + refetchQueries: [ + { + query: GetSecrets, + variables: { + appId: params.app, + envId: params.environment, + }, + }, + ], + }) + } + } + + const handleDeleteSecret = async (id: string) => { + if (id.split('-')[0] === 'new') + updateSecrets(updatedSecrets.filter((secret) => secret.id !== id)) + else { + await deleteSecret({ + variables: { + id, + }, + refetchQueries: [ + { + query: GetSecrets, + variables: { + appId: params.app, + envId: params.environment, + }, + }, + ], + }) + } + toast.success('Secret deleted.') + } + + useEffect(() => { + const initEnvKeys = async () => { + const wrappedSeed = data.environmentKeys[0].wrappedSeed + + const userKxKeys = { + publicKey: await getUserKxPublicKey(keyring!.publicKey), + privateKey: await getUserKxPrivateKey(keyring!.privateKey), + } + const seed = await decryptAsymmetric(wrappedSeed, userKxKeys.privateKey, userKxKeys.publicKey) + + const salt = await decryptAsymmetric( + data.environmentKeys[0].wrappedSalt, + userKxKeys.privateKey, + userKxKeys.publicKey + ) + const { publicKey, privateKey } = await envKeyring(seed) + + setEnvKeys({ + publicKey, + privateKey, + salt, + }) + } + + if (data && keyring) initEnvKeys() + }, [data, keyring]) + + useEffect(() => { + if (data && envKeys) { + const decryptSecrets = async () => { + const decryptedSecrets = await Promise.all( + data.secrets.map(async (secret: SecretType) => { + const decryptedSecret = structuredClone(secret) + + decryptedSecret.key = await decryptAsymmetric( + secret.key, + envKeys?.privateKey, + envKeys?.publicKey + ) + + decryptedSecret.value = await decryptAsymmetric( + secret.value, + envKeys.privateKey, + envKeys.publicKey + ) + + if (decryptedSecret.comment !== '') + decryptedSecret.comment = await decryptAsymmetric( + secret.comment, + envKeys.privateKey, + envKeys.publicKey + ) + + // Decrypt history for each secret + if (secret.history && secret.history.length > 0) { + const decryptedHistory = await Promise.all( + secret.history.map(async (event) => { + const decryptedEvent = structuredClone(event) + + // Decrypt event fields + decryptedEvent!.key = await decryptAsymmetric( + event!.key, + envKeys.privateKey, + envKeys.publicKey + ) + + decryptedEvent!.value = await decryptAsymmetric( + event!.value, + envKeys.privateKey, + envKeys.publicKey + ) + + if (decryptedEvent!.comment !== '') { + decryptedEvent!.comment = await decryptAsymmetric( + event!.comment, + envKeys.privateKey, + envKeys.publicKey + ) + } + + return decryptedEvent + }) + ) + + decryptedSecret.history = decryptedHistory + } + + return decryptedSecret + }) + ) + return decryptedSecrets + } + + decryptSecrets().then((decryptedSecrets) => { + setSecrets(decryptedSecrets) + updateSecrets(decryptedSecrets) + }) + } + }, [envKeys, data]) + + const handleUpdateSecretProperty = (id: string, property: string, value: any) => { + const updatedSecretList = updatedSecrets.map((secret) => { + if (secret.id === id) { + return { ...secret, [property]: value } + } + return secret + }) + + updateSecrets(updatedSecretList) + } + + const getUpdatedSecrets = () => { + const changedElements = [] + + for (let i = 0; i < updatedSecrets.length; i++) { + const updatedSecret = updatedSecrets[i] + const originalSecret = secrets.find((secret) => secret.id === updatedSecret.id) + + // this is a newly created secret that doesn't exist on the server yet + if (!originalSecret) { + changedElements.push(updatedSecret) + } else if ( + originalSecret.comment !== updatedSecret.comment || + originalSecret.key !== updatedSecret.key || + !arraysEqual(originalSecret.tags, updatedSecret.tags) || + originalSecret.value !== updatedSecret.value + ) { + changedElements.push(updatedSecret) + } + } + + return changedElements + } + + const duplicateKeysExist = () => { + const keySet = new Set() + + for (const secret of updatedSecrets) { + if (keySet.has(secret.key)) { + return true // Duplicate key found + } + keySet.add(secret.key) + } + + return false // No duplicate keys found + } + + const handleSaveChanges = async () => { + setIsloading(true) + const changedSecrets = getUpdatedSecrets() + if (changedSecrets.some((secret) => secret.key.length === 0)) { + toast.error('Secret keys cannot be empty!') + setIsloading(false) + return false + } + + if (duplicateKeysExist()) { + toast.error('Secret keys cannot be repeated!') + setIsloading(false) + return false + } + + const updates = changedSecrets.map((secret) => handleUpdateSecret(secret)) + + await Promise.all(updates) + + setTimeout(() => setIsloading(false), 500) + + toast.success('Changes successfully deployed.') + } + + const handleDiscardChanges = () => { + updateSecrets(secrets) + } + + const secretNames = secrets.map((secret) => { + const { id, key } = secret + return { + id, + key, + } + }) + + const filteredSecrets = + searchQuery === '' + ? updatedSecrets + : updatedSecrets.filter((secret) => { + const searchRegex = new RegExp(searchQuery, 'i') + return searchRegex.test(secret.key) + }) + + const cannonicalSecret = (id: string) => secrets.find((secret) => secret.id === id) + + const downloadEnvFile = () => { + const envContent = secrets + .map((secret) => { + const comment = secret.comment ? `#${secret.comment}\n` : '' + return `${comment}${secret.key}=${secret.value}` + }) + .join('\n') + + const blob = new Blob([envContent], { type: 'text/plain' }) + const url = URL.createObjectURL(blob) + + const a = document.createElement('a') + a.href = url + a.download = `${environment.name}.env` + + document.body.appendChild(a) + a.click() + + document.body.removeChild(a) + URL.revokeObjectURL(url) + } + + // useEffect(() => { + // const warningText = 'You have unsaved changes - are you sure you wish to leave this page?' + // const handleWindowClose = (e: BeforeUnloadEvent) => { + // if (!unsavedChanges) return + // e.preventDefault() + // return (e.returnValue = warningText) + // } + // const handleBrowseAway = () => { + // if (!unsavedChanges) return + // if (window.confirm(warningText)) return + // router.events.emit('routeChangeError') + // throw 'routeChange aborted.' + // } + // window.addEventListener('beforeunload', handleWindowClose) + // router.events.on('routeChangeStart', handleBrowseAway) + // return () => { + // window.removeEventListener('beforeunload', handleWindowClose) + // router.events.off('routeChangeStart', handleBrowseAway) + // } + // }, [unsavedChanges]) + + return ( +
+ {organisation && } + {keyring !== null && !loading && ( +
+
+ {envLinks.length > 1 ? ( + + {({ open }) => ( + <> + +
+

{environment.name}

+ +
+
+ + +
+ {envLinks.map((link: { label: string; href: string }) => ( + + {({ active }) => ( + +
{link.label}
+ + + )} +
+ ))} +
+
+
+ + )} +
+ ) : ( +

{environment.name}

+ )} + {unsavedChanges && ( + + You have undeployed changes to this environment. + + )} +
+ +
+
+
+ +
+ setSearchQuery(e.target.value)} + /> + setSearchQuery('')} + /> +
+
+ + {unsavedChanges && ( + + )} + +
+
+
+
+
+ key +
+
+ value + +
+
+ {organisation && + filteredSecrets.map((secret, index: number) => ( +
+ {index + 1} + +
+ ))} + +
+ +
+
+
+ )} +
+ ) +} diff --git a/frontend/app/[team]/apps/[app]/keys/page.tsx b/frontend/app/[team]/apps/[app]/keys/page.tsx deleted file mode 100644 index 586ed2a5..00000000 --- a/frontend/app/[team]/apps/[app]/keys/page.tsx +++ /dev/null @@ -1,287 +0,0 @@ -'use client' - -import { GetOrganisations } from '@/apollo/queries/getOrganisations.gql' -import { GetAppDetail } from '@/apollo/queries/getAppDetail.gql' -import { RotateAppKeys } from '@/apollo/mutations/rotateAppKeys.gql' -import { useLazyQuery, useQuery, useMutation } from '@apollo/client' -import { AppType, ChartDataPointType, TimeRange } from '@/apollo/graphql' -import { Fragment, useEffect, useState } from 'react' -import { Button } from '@/components/common/Button' -import { copyToClipBoard } from '@/utils/clipboard' -import { - FaCopy, - FaExclamationTriangle, - FaEye, - FaEyeSlash, - FaInfo, - FaMagic, - FaTimes, -} from 'react-icons/fa' -import { MdContentCopy, MdOutlineRotateLeft } from 'react-icons/md' -import { toast } from 'react-toastify' -import { Dialog, Transition } from '@headlessui/react' -import { cryptoUtils } from '@/utils/auth' -import { useSession } from 'next-auth/react' -import { getLocalKeyring } from '@/utils/localStorage' -import { splitSecret } from '@/utils/keyshares' -import { Alert } from '@/components/common/Alert' - -export default function App({ params }: { params: { team: string; app: string } }) { - const { data: orgsData } = useQuery(GetOrganisations) - const [getApp, { data }] = useLazyQuery(GetAppDetail) - - const app = data?.apps[0] as AppType - - const appId = `phApp:v${app?.appVersion}:${app?.identityKey}` - - const [appSecret, setAppSecret] = useState('') - - const { data: session } = useSession() - - const appSecretPlaceholder = '*'.repeat(295) - - useEffect(() => { - if (orgsData) { - const organisationId = orgsData.organisations[0].id - getApp({ - variables: { - organisationId, - appId: params.app, - }, - }) - } - }, [getApp, orgsData, params.app]) - - const handleCopy = (val: string) => { - copyToClipBoard(val) - toast.info('Copied') - } - - const RotateAppDialog = () => { - const [pw, setPw] = useState('') - const [showPw, setShowPw] = useState(false) - const [loading, setLoading] = useState(false) - const [isOpen, setIsOpen] = useState(false) - const [rotateAppKeys] = useMutation(RotateAppKeys) - - const closeModal = () => { - setPw('') - setIsOpen(false) - } - - const handleGenerateNewAppKey = async () => { - const APP_VERSION = 1 - - return new Promise(async (resolve, reject) => { - setTimeout(async () => { - setLoading(true) - try { - const wrapKey = await cryptoUtils.newAppWrapKey() - const newAppToken = await cryptoUtils.newAppToken() - const deviceKey = await cryptoUtils.deviceVaultKey(pw, session?.user?.email!) - const encryptedKeyring = getLocalKeyring(orgsData.organisations[0].id) - if (!encryptedKeyring) throw 'Error fetching local encrypted keys from browser' - const decryptedKeyring = await cryptoUtils.decryptAccountKeyring( - encryptedKeyring!, - deviceKey - ) - if (!decryptedKeyring) throw 'Failed to decrypt keys' - - const appSeed = await cryptoUtils.decryptedAppSeed( - app.appSeed, - decryptedKeyring.symmetricKey - ) - - const appKeys = await cryptoUtils.appKeyring(appSeed) - const appKeyShares = await splitSecret(appKeys.privateKey) - const wrappedShare = await cryptoUtils.wrappedKeyShare(appKeyShares[1], wrapKey) - await rotateAppKeys({ - variables: { - id: app.id, - appToken: newAppToken, - wrappedKeyShare: wrappedShare, - }, - }) - - setAppSecret(`pss:v${APP_VERSION}:${newAppToken}:${appKeyShares[0]}:${wrapKey}`) - - setLoading(false) - resolve(true) - } catch (error) { - console.log(error) - setLoading(false) - reject() - } - }, 500) - }) - } - - const handleSubmit = async (event: { preventDefault: () => void }) => { - event.preventDefault() - toast - .promise(handleGenerateNewAppKey, { - pending: 'Generating app keys', - success: 'Success!', - error: 'Something went wrong! Please check your password and try again.', - }) - .then(() => closeModal()) - } - - return ( - <> - - - {}}> - -
- - -
-
- - - -

- Genereate new app secret -

- -
- - - Generate a new app secret for {app.name} - - -
-
-
- -
- -
- Warning: This will revoke your current app keys. Your application - won't be able to decrypt data using the current keys. -
-
-
- - -
- -
- Your new keys will be available to use immediately. You will be able - to decrypt any existing data with your new keys. Please allow up to - 60 seconds for your old keys to be revoked. -
-
-
-
- -
- -
- setPw(e.target.value)} - type={showPw ? 'text' : 'password'} - minLength={16} - required - className="w-full " - /> - -
-
-
- - -
-
-
-
-
-
-
-
-
- - ) - } - - return ( -
-
- {/*

keys

*/} - {app && ( -
-
-
- app id - -
- {appId} -
- -
-
- app secret -
- {appSecret && ( -
- -
{"Copy this value. You won't see it again!"}
-
- )} - {appSecret && ( - - )} -
- {!appSecret && } -
- {appSecret || appSecretPlaceholder} -
-
- )} -
-
- ) -} diff --git a/frontend/app/[team]/apps/[app]/layout.tsx b/frontend/app/[team]/apps/[app]/layout.tsx index 74277d1b..2e41160b 100644 --- a/frontend/app/[team]/apps/[app]/layout.tsx +++ b/frontend/app/[team]/apps/[app]/layout.tsx @@ -1,16 +1,14 @@ 'use client' -import { Fragment, useEffect, useState } from 'react' +import { Fragment, useContext, useEffect, useState } from 'react' import { Tab } from '@headlessui/react' import clsx from 'clsx' import Link from 'next/link' -import { useQuery, useLazyQuery } from '@apollo/client' +import { useLazyQuery } from '@apollo/client' import { AppType } from '@/apollo/graphql' -import { GetOrganisations } from '@/apollo/queries/getOrganisations.gql' -import { GetAppDetail } from '@/apollo/queries/getAppDetail.gql' +import { GetAppDetail } from '@/graphql/queries/getAppDetail.gql' import { usePathname } from 'next/navigation' -import { Button } from '@/components/common/Button' -import { FaCopy } from 'react-icons/fa' +import { organisationContext } from '@/contexts/organisationContext' export default function AppLayout({ params, @@ -19,56 +17,63 @@ export default function AppLayout({ params: { team: string; app: string } children: React.ReactNode }) { + const { activeOrganisation: organisation } = useContext(organisationContext) const path = usePathname() const [tabIndex, setTabIndex] = useState(0) - const { data: orgsData } = useQuery(GetOrganisations) const [getApp, { data, loading }] = useLazyQuery(GetAppDetail) const app = data?.apps[0] as AppType + const [tabs, setTabs] = useState([ + { + name: 'Secrets', + link: '', + }, + { + name: 'Service tokens', + link: 'tokens', + }, + { + name: 'Logs', + link: 'logs', + }, + { + name: 'Members', + link: 'members', + }, + ]) + useEffect(() => { - if (orgsData) { - const organisationId = orgsData.organisations[0].id + if (organisation) { getApp({ variables: { - organisationId, + organisationId: organisation.id, appId: params.app, }, }) + + if (organisation.role!.toLowerCase() !== 'dev') { + setTabs((prevTabs) => + prevTabs.some((tab) => tab.name === 'Settings') + ? prevTabs + : [...prevTabs, { name: 'Settings', link: 'settings' }] + ) + } } - }, [getApp, orgsData, params.app]) + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [organisation, params.app]) useEffect(() => { const activeTabIndex = () => { if (app) { - const currentUrl = path?.split('/')[4] - if (currentUrl === '') return 0 - if (currentUrl === 'logs') return 1 - if (currentUrl === 'keys') return 2 - if (currentUrl === 'settings') return 3 + const currentUrl = path?.split('/')[4] || '' + const index = tabs.findIndex((tab) => tab.link === currentUrl) + return index >= 0 ? index : 0 } return 0 } - setTabIndex(activeTabIndex()) - }, [app, path]) - const tabs = [ - { - name: 'Home', - link: '', - }, - { - name: 'Logs', - link: 'logs', - }, - { - name: 'Keys', - link: 'keys', - }, - { - name: 'Settings', - link: 'settings', - }, - ] + setTabIndex(activeTabIndex()) + }, [app, path, tabs]) return (
(null) - const tableBodyRef = useRef(null) - const [getAppLogs, { data, loading }] = useLazyQuery(GetAppLogs) - const [totalCount, setTotalCount] = useState(0) - const [logList, setLogList] = useState([]) - - const [endofList, setEndofList] = useState(false) - - const getCurrentTimeStamp = () => Date.now() - const getLastLogTimestamp = () => - logList.length > 0 ? logList[logList.length - 1].timestamp : getCurrentTimeStamp() - - /** - * Fetches logs for the app with the given start and end timestamps, - * and then adds the result of the query to the current log list. - * - * @param {number} start - Start datetime as unix timestamp (ms) - * @param {number} end - End datetime as unix timestamp (ms) - * - * @returns {void} - */ - const fetchLogs = (start: number, end: number) => { - getAppLogs({ - variables: { - appId: params.app, - start, - end, - }, - fetchPolicy: 'network-only', - }).then((result) => { - if (result.data?.logs.length) { - setLogList(logList.concat(result.data.logs)) - } - if (result.data?.logs.length < DEFAULT_PAGE_SIZE) setEndofList(true) - }) - } - - const clearLogList = () => setLogList([]) - - /** - * Gets the first page of logs, by resetting the log list and fetching logs using the current unix timestamp. - * - * @returns {void} - */ - const getFirstPage = () => { - setEndofList(false) - fetchLogs(LOGS_START_DATE, getCurrentTimeStamp()) - } - - /** - * Gets the new page of logs by using the last available timestamp from the current log list - * - * @returns {void} - */ - const getNextPage = () => { - fetchLogs(LOGS_START_DATE, getLastLogTimestamp()) - } - - /** - * Hook to get the first page of logs on page load, or when the loglist is reset to empty - */ - useEffect(() => { - if (logList.length === 0) getFirstPage() - // eslint-disable-next-line react-hooks/exhaustive-deps - }, [params.app, logList]) - - /** - * Hook to update the log count once its available - */ - useEffect(() => { - if (data?.logsCount) setTotalCount(data.logsCount) - }, [data]) - - // useEffect(() => { - // const options = { - // root: null, - // rootMargin: '0px', - // threshold: 1.0, - // } - // const observer = new IntersectionObserver((entries) => { - // const [entry] = entries - // if (entry.isIntersecting) getNextPage() - // }, options) - - // if (loglistEndRef.current) { - // if (endofList) observer.unobserve(loglistEndRef.current) - // else observer.observe(loglistEndRef.current) - // } - - // return () => { - // if (loglistEndRef.current) observer.unobserve(loglistEndRef.current) - // } - - // // eslint-disable-next-line react-hooks/exhaustive-deps - // }, [loglistEndRef]) - - const LogRow = (props: { log: KmsLogType }) => { - const { log } = props - - const SDKIcon = (sdkName: string) => { - const sdks = [ - { - name: 'node-js', - label: 'Node.js', - icon: , - color: 'bg-[#339933]', - }, - { - name: 'python', - label: 'Python', - icon: , - color: 'bg-[#3776AB]', - }, - ] - - const sdk = sdks.find((sdk) => sdkName.toLowerCase().includes(sdk.name)) || sdks[0] - - return ( -
- {sdk.icon} -
- ) - } - - const relativeTimeStamp = () => { - return relativeTimeFromDates(new Date(log.timestamp)) - } - - const verboseTimeStamp = () => { - const date = new Date(log.timestamp) - return date.toISOString() - } - - const LogField = (props: { label: string; children: ReactNode }) => { - return ( -
- {props.label}: - {props.children} -
- ) - } - - return ( - - {({ open }) => ( - <> - - {/* */} - - - - {SDKIcon(log.phaseNode!)} - {log.eventType} - - {humanFileSize(log.phSize!)} - - - {log.city} {log.country ? getUnicodeFlagIcon(log.country) : 'Not available'} - - - {relativeTimeStamp()} - - {/* */} - - - - -
- Log ID: - {log.id} -
-
- -
- {SDKIcon(log.phaseNode!)} {log.phaseNode} -
-
- - - {log.eventType} - - - {humanFileSize(log.phSize!)} - - {(log.city || log.country) && ( - - {' '} - {log.city}, {log.country}{' '} - {log.country ? getUnicodeFlagIcon(log.country) : 'Not available'} - - )} - - {log.ipAddress} - - {verboseTimeStamp()} -
-
- -
- - )} -
- ) - } - - const SkeletonRow = (props: { rows: number }) => { - const SKELETON_BASE_STYLE = 'dark:bg-neutral-700 bg-neutral-300 animate-pulse' - return ( - <> - {[...Array(props.rows)].map((_, n) => ( - - - - - -
- - -
- - -
- - -
- - -
- - - ))} - - ) - } +export default function Logs({ params }: { params: { team: string; app: string } }) { + const [tabIndex, setTabIndex] = useState(0) + const { activeOrganisation: organisation } = useContext(organisationContext) + + const tabs = [ + { + label: 'Secrets', + component: , + }, + { + label: 'KMS', + component: , + }, + ] return (
-
- - {totalCount && } Events - - -
- - - - - - - - - - - - - {logList.map((log, n) => ( - - ))} - {loading && } - - - - -
SDKEventDataLocationTime
-
- {!endofList && ( - + {organisation?.role?.toLowerCase() === 'owner' ? ( + setTabIndex(index)}> + + {tabs.map((tab) => ( + + {({ selected }) => ( +
+ {tab.label} +
)} - {endofList && `No${logList.length ? ' more ' : ' '}logs to show`} -
-
+ + ))} + + + {tabs.map((tab) => ( + {tab.component} + ))} + + + ) : ( + + )}
) } diff --git a/frontend/app/[team]/apps/[app]/members/page.tsx b/frontend/app/[team]/apps/[app]/members/page.tsx new file mode 100644 index 00000000..38918692 --- /dev/null +++ b/frontend/app/[team]/apps/[app]/members/page.tsx @@ -0,0 +1,930 @@ +'use client' + +import GetOrganisationMembers from '@/graphql/queries/organisation/getOrganisationMembers.gql' +import AddMemberToApp from '@/graphql/mutations/apps/addAppMember.gql' +import RemoveMemberFromApp from '@/graphql/mutations/apps/removeAppMember.gql' +import UpdateEnvScope from '@/graphql/mutations/apps/updateEnvScope.gql' +import GetAppMembers from '@/graphql/queries/apps/getAppMembers.gql' +import { GetAppEnvironments } from '@/graphql/queries/secrets/getAppEnvironments.gql' +import { GetEnvironmentKey } from '@/graphql/queries/secrets/getEnvironmentKey.gql' +import { useLazyQuery, useMutation, useQuery } from '@apollo/client' +import { Fragment, useContext, useEffect, useState } from 'react' +import { OrganisationMemberType, EnvironmentType } from '@/apollo/graphql' +import { Button } from '@/components/common/Button' +import { organisationContext } from '@/contexts/organisationContext' +import { relativeTimeFromDates } from '@/utils/time' +import { Combobox, Dialog, Listbox, Transition } from '@headlessui/react' +import { + FaCheckSquare, + FaChevronDown, + FaEye, + FaEyeSlash, + FaPlus, + FaSquare, + FaTimes, + FaUserCog, + FaUserTimes, +} from 'react-icons/fa' +import clsx from 'clsx' +import { toast } from 'react-toastify' +import { useSession } from 'next-auth/react' +import { Avatar } from '@/components/common/Avatar' +import { KeyringContext } from '@/contexts/keyringContext' +import { unwrapEnvSecretsForUser, wrapEnvSecretsForUser } from '@/utils/environments' +import { OrganisationKeyring, cryptoUtils } from '@/utils/auth' +import { userIsAdmin } from '@/utils/permissions' +import { RoleLabel } from '@/components/users/RoleLabel' +import { Alert } from '@/components/common/Alert' +import Link from 'next/link' + +export default function Members({ params }: { params: { team: string; app: string } }) { + const { data } = useQuery(GetAppMembers, { variables: { appId: params.app } }) + + const { keyring, setKeyring } = useContext(KeyringContext) + const { activeOrganisation: organisation } = useContext(organisationContext) + + const activeUserIsAdmin = organisation ? userIsAdmin(organisation.role!) : false + + const [getEnvKey] = useLazyQuery(GetEnvironmentKey) + + const { data: session } = useSession() + + const validateKeyring = async (password: string) => { + return new Promise(async (resolve) => { + if (keyring) resolve(keyring) + else { + const decryptedKeyring = await cryptoUtils.getKeyring( + session?.user?.email!, + organisation!.id, + password + ) + setKeyring(decryptedKeyring) + resolve(decryptedKeyring) + } + }) + } + + const AddMemberDialog = () => { + const [getMembers, { data: orgMembersData }] = useLazyQuery(GetOrganisationMembers) + + useEffect(() => { + if (organisation) { + getMembers({ + variables: { + organisationId: organisation.id, + role: null, + }, + }) + } + }, [getMembers]) + + const memberOptions = + orgMembersData?.organisationMembers.filter( + (orgMember: OrganisationMemberType) => + !data?.appUsers + .map((appUser: OrganisationMemberType) => appUser.id) + .includes(orgMember.id) + ) ?? [] + + const [addMember] = useMutation(AddMemberToApp) + + const { data: appEnvsData } = useQuery(GetAppEnvironments, { + variables: { + appId: params.app, + }, + }) + + const envOptions = + appEnvsData?.appEnvironments.map((env: EnvironmentType) => { + const { id, name } = env + + return { + id, + name, + } + }) ?? [] + + const [isOpen, setIsOpen] = useState(false) + const [selectedMember, setSelectedMember] = useState(null) + const [query, setQuery] = useState('') + const [envScope, setEnvScope] = useState>>([]) + const [showEnvHint, setShowEnvHint] = useState(false) + const [password, setPassword] = useState('') + const [showPw, setShowPw] = useState(false) + + const filteredPeople = + query === '' + ? memberOptions + : memberOptions.filter((member: OrganisationMemberType) => { + const memberQueryableName = member.fullName || member.email! + return memberQueryableName.toLowerCase().includes(query.toLowerCase()) + }) + + const closeModal = () => { + setIsOpen(false) + } + + const openModal = () => { + setIsOpen(true) + } + + const handleAddMember = async (e: { preventDefault: () => void }) => { + e.preventDefault() + + if (envScope.length === 0) { + setShowEnvHint(true) + return false + } + + const keyring = await validateKeyring(password) + + const appEnvironments = appEnvsData.appEnvironments as EnvironmentType[] + + const envKeyPromises = appEnvironments + .filter((env) => envScope.map((selectedEnv) => selectedEnv.id).includes(env.id)) + .map(async (env: EnvironmentType) => { + const { data } = await getEnvKey({ + variables: { + envId: env.id, + appId: params.app, + }, + }) + + const { + wrappedSeed: userWrappedSeed, + wrappedSalt: userWrappedSalt, + identityKey, + } = data.environmentKeys[0] + + const { seed, salt } = await unwrapEnvSecretsForUser( + userWrappedSeed, + userWrappedSalt, + keyring! + ) + + const { wrappedSeed, wrappedSalt } = await wrapEnvSecretsForUser( + { seed, salt }, + selectedMember! + ) + + return { + envId: env.id, + userId: selectedMember!.id, + identityKey, + wrappedSeed, + wrappedSalt, + } + }) + + const envKeyInputs = await Promise.all(envKeyPromises) + + await addMember({ + variables: { memberId: selectedMember!.id, appId: params.app, envKeys: envKeyInputs }, + refetchQueries: [ + { + query: GetAppMembers, + variables: { appId: params.app }, + }, + ], + }) + + toast.success('Added member to App', { autoClose: 2000 }) + } + + return ( + <> +
+ +
+ + + + +
+ + +
+
+ + + +

+ Add a member +

+ + +
+ + {memberOptions.length === 0 ? ( +
+ +

+ All organisation members are added to this App. You can invite more + users from the{' '} + + organisation members + {' '} + page. +

+
+
+ ) : ( +
+ + {({ open }) => ( + <> +
+ + + +
+ setQuery(event.target.value)} + required + displayValue={(person: OrganisationMemberType) => + person?.fullName || person?.email! + } + /> +
+ + + +
+
+
+ + +
+ {filteredPeople.map((person: OrganisationMemberType) => ( + + {({ active, selected }) => ( +
+ + + {person.fullName || person.email} + +
+ )} +
+ ))} +
+
+
+ + )} +
+ +
+ {envScope.length === 0 && showEnvHint && ( + + Select an environment scope + + )} + + {({ open }) => ( + <> + + + + +
+ + {envScope + .map((env: Partial) => env.name) + .join(' + ')} + + +
+
+ + +
+ {envOptions.map((env: Partial) => ( + + {({ active, selected }) => ( +
+ {selected ? ( + + ) : ( + + )} + + {env.name} + +
+ )} +
+ ))} +
+
+
+ + {!keyring && ( +
+ +
+ setPassword(e.target.value)} + type={showPw ? 'text' : 'password'} + minLength={16} + required + autoFocus + className="custom w-full text-zinc-800 font-mono dark:text-white bg-zinc-100 dark:bg-zinc-800 rounded-md ph-no-capture" + /> + +
+
+ )} + + )} +
+
+ +
+ + +
+
+ )} +
+
+
+
+
+
+ + ) + } + + const RemoveMemberConfirmDialog = (props: { member: OrganisationMemberType }) => { + const { member } = props + + const [removeMember] = useMutation(RemoveMemberFromApp) + + const [isOpen, setIsOpen] = useState(false) + + const closeModal = () => { + setIsOpen(false) + } + + const openModal = () => { + setIsOpen(true) + } + + const handleRemoveMember = async () => { + await removeMember({ + variables: { memberId: member.id, appId: params.app }, + refetchQueries: [ + { + query: GetAppMembers, + variables: { appId: params.app }, + }, + ], + }) + toast.success('Removed member from app', { autoClose: 2000 }) + } + + return ( + <> +
+ +
+ + + + +
+ + +
+
+ + + +

+ Remove member +

+ + +
+ +
+

+ Are you sure you want to remove {member.fullName || member.email} from this + app? +

+
+ + +
+
+
+
+
+
+
+
+ + ) + } + + const ManageUserAccessDialog = (props: { member: OrganisationMemberType }) => { + const [updateScope] = useMutation(UpdateEnvScope) + const [getUserEnvScope] = useLazyQuery(GetAppEnvironments) + + const { data: appEnvsData } = useQuery(GetAppEnvironments, { + variables: { + appId: params.app, + }, + }) + + const envOptions = + appEnvsData?.appEnvironments.map((env: EnvironmentType) => { + const { id, name } = env + + return { + id, + name, + } + }) ?? [] + + const [isOpen, setIsOpen] = useState(false) + + const [envScope, setEnvScope] = useState>>([]) + const [showEnvHint, setShowEnvHint] = useState(false) + const [password, setPassword] = useState('') + const [showPw, setShowPw] = useState(false) + + const memberIsAdmin = userIsAdmin(props.member.role) || false + + const closeModal = () => { + setIsOpen(false) + } + + const openModal = () => { + setIsOpen(true) + } + + useEffect(() => { + if (isOpen) { + const handleGetCurrentSCope = async () => { + const { data: currentScope } = await getUserEnvScope({ + variables: { + appId: params.app, + memberId: props.member.id, + }, + fetchPolicy: 'no-cache', + }) + + setEnvScope( + currentScope?.appEnvironments.map((env: EnvironmentType) => { + const { id, name } = env + + return { + id, + name, + } + }) ?? [] + ) + } + + if (isOpen) handleGetCurrentSCope() + } + }, [getUserEnvScope, isOpen, props.member.id]) + + const handleUpdateScope = async (e: { preventDefault: () => void }) => { + e.preventDefault() + + if (envScope.length === 0) { + setShowEnvHint(true) + return false + } + + const keyring = await validateKeyring(password) + + const appEnvironments = appEnvsData.appEnvironments as EnvironmentType[] + + const envKeyPromises = appEnvironments + .filter((env) => envScope.map((selectedEnv) => selectedEnv.id).includes(env.id)) + .map(async (env: EnvironmentType) => { + const { data } = await getEnvKey({ + variables: { + envId: env.id, + appId: params.app, + }, + }) + + const { + wrappedSeed: userWrappedSeed, + wrappedSalt: userWrappedSalt, + identityKey, + } = data.environmentKeys[0] + + const { seed, salt } = await unwrapEnvSecretsForUser( + userWrappedSeed, + userWrappedSalt, + keyring! + ) + + const { wrappedSeed, wrappedSalt } = await wrapEnvSecretsForUser( + { seed, salt }, + props.member! + ) + + return { + envId: env.id, + userId: props.member!.id, + identityKey, + wrappedSeed, + wrappedSalt, + } + }) + + const envKeyInputs = await Promise.all(envKeyPromises) + + await updateScope({ + variables: { memberId: props.member!.id, appId: params.app, envKeys: envKeyInputs }, + refetchQueries: [ + { + query: GetAppMembers, + variables: { appId: params.app }, + }, + ], + }) + + toast.success('Updated user access', { autoClose: 2000 }) + } + + return ( + <> +
+ +
+ + + + +
+ + +
+
+ + + +

+ Manage access for {props.member.fullName || props.member.email} +

+ + +
+ +
+
+ {envScope.length === 0 && showEnvHint && ( + + Select an environment scope + + )} + + {({ open }) => ( + <> + + + + +
+ + {envScope + .map((env: Partial) => env.name) + .join(' + ')} + + +
+
+ + +
+ {envOptions.map((env: Partial) => ( + + {({ active, selected }) => ( +
+ {selected ? ( + + ) : ( + + )} + + {env.name} + +
+ )} +
+ ))} +
+
+
+ + )} +
+
+ {!keyring && !memberIsAdmin && ( +
+ +
+ setPassword(e.target.value)} + type={showPw ? 'text' : 'password'} + minLength={16} + required + autoFocus + className="custom w-full text-zinc-800 font-mono dark:text-white bg-zinc-100 dark:bg-zinc-800 rounded-md ph-no-capture" + /> + +
+
+ )} + + {memberIsAdmin && ( + +

+ This user is an , and has access to all + environments in this App. To restrict their access, change their role to{' '} + from the{' '} + + organisation members + {' '} + page. +

+
+ )} + +
+ + +
+
+
+
+
+
+
+
+ + ) + } + + return ( +
+
+
+ +
+ + + + + + + + {activeUserIsAdmin && } + + + + {data?.appUsers.map((member: OrganisationMemberType) => ( + + + + + {activeUserIsAdmin && ( + + )} + + ))} + +
+ User + + Joined +
+ +
+
+ {member.fullName || member.email} + +
+ {member.fullName && ( + {member.email} + )} +
+
+ {relativeTimeFromDates(new Date(member.createdAt))} + + {member.email !== session?.user?.email && + member.role.toLowerCase() !== 'owner' && ( +
+ + +
+ )} +
+
+
+ ) +} diff --git a/frontend/app/[team]/apps/[app]/old-home.tsx.archive b/frontend/app/[team]/apps/[app]/old-home.tsx.archive new file mode 100644 index 00000000..f89afaad --- /dev/null +++ b/frontend/app/[team]/apps/[app]/old-home.tsx.archive @@ -0,0 +1,120 @@ +'use client' + +import { GetAppDetail } from '@/graphql/queries/getAppDetail.gql' +import { GetAppLogCount } from '@/graphql/queries/getAppLogCount.gql' +import { useLazyQuery } from '@apollo/client' +import { AppType } from '@/apollo/graphql' +import { useContext, useEffect } from 'react' +import { AppActivityChart } from '@/components/apps/AppActivityChart' +import { FaArrowRight } from 'react-icons/fa' +import { Button } from '@/components/common/Button' +import Spinner from '@/components/common/Spinner' +import { relativeTimeFromDates } from '@/utils/time' +import Link from 'next/link' +import { usePathname } from 'next/navigation' +import { Count } from 'reaviz' +import { organisationContext } from '@/contexts/organisationContext' + +export default function App({ params }: { params: { team: string; app: string } }) { + const { activeOrganisation: organisation } = useContext(organisationContext) + + const [getAppLogCount, { data: countData, loading: countLoading }] = useLazyQuery(GetAppLogCount) + const [getApp, { data, loading: appDataLoading }] = useLazyQuery(GetAppDetail) + + const app = data?.apps[0] as AppType + + useEffect(() => { + if (organisation) { + getApp({ + variables: { + organisationId: organisation.id, + appId: params.app, + }, + }) + getAppLogCount({ + variables: { + appId: params.app, + }, + fetchPolicy: 'cache-and-network', + }) + } + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [organisation, params.app]) + + const totalLogCount = countData?.logsCount ?? 0 + + const showTotalLogCountSpinner = countLoading && totalLogCount === 0 + + return ( +
+ {!app && ( +
+ )} + {app && ( +
+
+ +
+
+ )} + +
+
+ {showTotalLogCountSpinner && } + {!showTotalLogCountSpinner && ( + + + + )} +

Total Decrypts

+
+
+ + + +
+
+ +
+
+
+ 1 +

Active key

+
+ +
+
+ + + +
+
+ +
+
+ {appDataLoading && } + {app && ( + + {relativeTimeFromDates(new Date(app.createdAt))} + + )} +

App created

+
+
+ + + +
+
+
+ ) +} diff --git a/frontend/app/[team]/apps/[app]/page.tsx b/frontend/app/[team]/apps/[app]/page.tsx index 6f6c7b48..0025326b 100644 --- a/frontend/app/[team]/apps/[app]/page.tsx +++ b/frontend/app/[team]/apps/[app]/page.tsx @@ -1,120 +1,474 @@ 'use client' -import { GetOrganisations } from '@/apollo/queries/getOrganisations.gql' -import { GetAppDetail } from '@/apollo/queries/getAppDetail.gql' -import { GetAppLogCount } from '@/apollo/queries/getAppLogCount.gql' -import { useLazyQuery, useQuery } from '@apollo/client' -import { AppType } from '@/apollo/graphql' -import { useEffect } from 'react' -import { AppActivityChart } from '@/components/apps/AppActivityChart' -import { FaArrowRight } from 'react-icons/fa' -import { Button } from '@/components/common/Button' -import Spinner from '@/components/common/Spinner' -import { humanReadableNumber } from '@/utils/dataUnits' -import { relativeTimeFromDates } from '@/utils/time' +import { GetAppEnvironments } from '@/graphql/queries/secrets/getAppEnvironments.gql' +import { GetEnvSecretsKV } from '@/graphql/queries/secrets/getSecretKVs.gql' +import { InitAppEnvironments } from '@/graphql/mutations/environments/initAppEnvironments.gql' +import { GetOrganisationAdminsAndSelf } from '@/graphql/queries/organisation/getOrganisationAdminsAndSelf.gql' +import { LogSecretRead } from '@/graphql/mutations/environments/readSecret.gql' +import { useLazyQuery, useMutation, useQuery } from '@apollo/client' +import { useContext, useEffect, useState } from 'react' +import { createNewEnv, decryptEnvSecretKVs, unwrapEnvSecretsForUser } from '@/utils/environments' +import { ApiEnvironmentEnvTypeChoices, EnvironmentType, SecretType } from '@/apollo/graphql' +import _sodium from 'libsodium-wrappers-sumo' +import { KeyringContext } from '@/contexts/keyringContext' +import UnlockKeyringDialog from '@/components/auth/UnlockKeyringDialog' +import { + FaArrowRight, + FaCheckCircle, + FaChevronRight, + FaCircle, + FaCopy, + FaExternalLinkAlt, + FaRegEye, + FaRegEyeSlash, + FaSearch, + FaTimesCircle, +} from 'react-icons/fa' import Link from 'next/link' import { usePathname } from 'next/navigation' -import { Count } from 'reaviz' +import { organisationContext } from '@/contexts/organisationContext' +import { Button } from '@/components/common/Button' +import clsx from 'clsx' +import { Disclosure, Transition } from '@headlessui/react' +import { copyToClipBoard } from '@/utils/clipboard' +import { toast } from 'react-toastify' +import { userIsAdmin } from '@/utils/permissions' + +type EnvSecrets = { + env: EnvironmentType + secrets: SecretType[] +} + +type AppSecret = { + key: string + envs: Array<{ + env: Partial + secret: SecretType | null + }> +} + +export default function Secrets({ params }: { params: { team: string; app: string } }) { + const { data } = useQuery(GetAppEnvironments, { + variables: { + appId: params.app, + }, + }) + + const pathname = usePathname() -export default function App({ params }: { params: { team: string; app: string } }) { - const { data: orgsData } = useQuery(GetOrganisations) - const [getAppLogCount, { data: countData, loading: countLoading }] = useLazyQuery(GetAppLogCount) - const [getApp, { data, loading: appDataLoading }] = useLazyQuery(GetAppDetail) + const [getEnvSecrets] = useLazyQuery(GetEnvSecretsKV) + const [getOrgAdmins, { data: orgAdminsData }] = useLazyQuery(GetOrganisationAdminsAndSelf) + const [appSecrets, setAppSecrets] = useState([]) + const [searchQuery, setSearchQuery] = useState('') + const [initAppEnvironments] = useMutation(InitAppEnvironments) - const app = data?.apps[0] as AppType + const { keyring } = useContext(KeyringContext) + const { activeOrganisation: organisation } = useContext(organisationContext) + + const activeUserIsAdmin = organisation ? userIsAdmin(organisation.role!) : false + + const filteredSecrets = + searchQuery === '' + ? appSecrets + : appSecrets.filter((secret) => { + const searchRegex = new RegExp(searchQuery, 'i') + return searchRegex.test(secret.key) + }) useEffect(() => { - if (orgsData) { - const organisationId = orgsData.organisations[0].id - getApp({ + if (organisation) { + getOrgAdmins({ variables: { - organisationId, - appId: params.app, + organisationId: organisation.id, }, }) - getAppLogCount({ - variables: { - appId: params.app, - }, - fetchPolicy: 'cache-and-network', + } + }, [getOrgAdmins, organisation, params.app]) + + useEffect(() => { + const fetchAndDecryptAppEnvs = async (appEnvironments: EnvironmentType[]) => { + const envSecrets = [] as EnvSecrets[] + + for (const env of appEnvironments) { + const { data } = await getEnvSecrets({ + variables: { + envId: env.id, + }, + }) + + const { wrappedSeed, wrappedSalt } = data.environmentKeys[0] + + const { publicKey, privateKey } = await unwrapEnvSecretsForUser( + wrappedSeed, + wrappedSalt, + keyring! + ) + + const decryptedSecrets = await decryptEnvSecretKVs(data.secrets, { + publicKey, + privateKey, + }) + + envSecrets.push({ env, secrets: decryptedSecrets }) + } + + // Create a list of unique secret keys + const secretKeys = Array.from( + new Set(envSecrets.flatMap((envCard) => envCard.secrets.map((secret) => secret.key))) + ) + + // Transform envCards into an array of AppSecret objects + const appSecrets = secretKeys.map((key) => { + const envs = envSecrets.map((envCard) => ({ + env: envCard.env, + secret: envCard.secrets.find((secret) => secret.key === key) || null, + })) + return { key, envs } }) + + setAppSecrets(appSecrets) } - }, [getApp, getAppLogCount, orgsData, params.app]) - const totalLogCount = countData?.logsCount ?? 0 + if (keyring !== null && data?.appEnvironments) fetchAndDecryptAppEnvs(data?.appEnvironments) + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [data?.appEnvironments, keyring]) - const showTotalLogCountSpinner = countLoading && totalLogCount === 0 + const initAppEnvs = async () => { + const mutationPayload = { + devEnv: await createNewEnv( + params.app, + 'Development', + ApiEnvironmentEnvTypeChoices.Dev, + orgAdminsData.organisationAdminsAndSelf + ), + stagingEnv: await createNewEnv( + params.app, + 'Staging', + ApiEnvironmentEnvTypeChoices.Staging, + orgAdminsData.organisationAdminsAndSelf + ), + prodEnv: await createNewEnv( + params.app, + 'Production', + ApiEnvironmentEnvTypeChoices.Prod, + orgAdminsData.organisationAdminsAndSelf + ), + } - return ( -
- {!app && ( -
- )} - {app && ( -
-
- -
-
- )} - -
-
- {showTotalLogCountSpinner && } - {!showTotalLogCountSpinner && ( - - - - )} -

Total Decrypts

-
-
- - + await initAppEnvironments({ + variables: { + devEnv: mutationPayload.devEnv.createEnvPayload, + stagingEnv: mutationPayload.stagingEnv.createEnvPayload, + prodEnv: mutationPayload.prodEnv.createEnvPayload, + devAdminKeys: mutationPayload.devEnv.adminKeysPayload, + stagAdminKeys: mutationPayload.stagingEnv.adminKeysPayload, + prodAdminKeys: mutationPayload.prodEnv.adminKeysPayload, + }, + refetchQueries: [ + { + query: GetAppEnvironments, + variables: { + appId: params.app, + }, + }, + ], + }) + } + + const setupRequired = data?.appEnvironments.length === 0 ?? true + + const EnvSecret = (props: { + envSecret: { + env: Partial + secret: SecretType | null + } + sameAsProd: boolean + }) => { + const { envSecret, sameAsProd } = props + + const [readSecret] = useMutation(LogSecretRead) + + const [showValue, setShowValue] = useState(false) + + const handleRevealSecret = async () => { + setShowValue(true) + await readSecret({ variables: { id: envSecret.secret!.id } }) + } + + const handleHideSecret = () => setShowValue(false) + + const toggleShowValue = () => { + showValue ? handleHideSecret() : handleRevealSecret() + } + + const handleCopy = async (val: string) => { + copyToClipBoard(val) + toast.info('Copied', { autoClose: 2000 }) + await readSecret({ variables: { id: envSecret.secret!.id } }) + } + + return ( +
+
+ +
{envSecret.env.envType}
+
-
-
-
-
- 1 -

Active key

+ {envSecret.secret === null ? ( + missing + ) : envSecret.secret.value.length === 0 ? ( + blank + ) : ( +
+ + {showValue ? ( +
{envSecret.secret.value}
+ ) : ( + {'*'.repeat(envSecret.secret.value.length)} + )} +
+ + {envSecret.secret !== null && ( +
+ + +
+ )}
- -
-
- - - -
+ )}
+ ) + } -
-
- {appDataLoading && } - {app && ( - - {relativeTimeFromDates(new Date(app.createdAt))} - - )} -

App created

-
-
- - - -
-
+ const AppSecretRow = (props: { appSecret: AppSecret }) => { + const { appSecret } = props + + const prodSecret = appSecret.envs.find( + (env) => env.env.envType?.toLowerCase() === 'prod' + )?.secret + + const secretIsSameAsProd = (env: { + env: Partial + secret: SecretType | null + }) => + prodSecret !== null && + env.secret?.value === prodSecret?.value && + env.env.envType?.toLowerCase() !== 'prod' + + const tooltipText = (env: { env: Partial; secret: SecretType | null }) => { + if (env.secret === null) return `This secret is missing in ${env.env.envType}` + else if (env.secret.value.length === 0) return `This secret is blank in ${env.env.envType}` + else if (secretIsSameAsProd(env)) return `This secret is the same as PROD.` + else return 'This secret is present' + } + + return ( + + {({ open }) => ( + <> + + + {appSecret.key} + + + {appSecret.envs.map((env) => ( + +
+ {env.secret !== null ? ( + env.secret.value.length === 0 ? ( + + ) : ( + + ) + ) : ( + + )} +
+ + ))} +
+ + + +
+ {appSecret.envs.map((envSecret) => ( + + ))} +
+
+ +
+ + )} +
+ ) + } + + return ( +
+ {organisation && } + {keyring !== null && + (setupRequired ? ( +
+

+ {activeUserIsAdmin + ? "There aren't any environments for this app yet" + : "You don't have access to any environments for this app yet. Contact the organisation owner or admins to get access."} +

+ {activeUserIsAdmin && ( + + )} +
+ ) : ( +
+
+
+

Secrets

+

+ An overview of secrets across all environments in this App. +

+
+
+ +
+
+
+ +
+ setSearchQuery(e.target.value)} + /> + setSearchQuery('')} + /> +
+ +
+
+ Secret is present +
+
+ Secret is the same as + Production +
+
+ Secret is blank +
+
+ Secret is missing +
+
+
+ + + + + + {data?.appEnvironments.map((env: EnvironmentType) => ( + + ))} + + + + {filteredSecrets.map((appSecret, index) => ( + + ))} + +
+ key + + + + +
+
+ ))}
) } diff --git a/frontend/app/[team]/apps/[app]/settings/page.tsx b/frontend/app/[team]/apps/[app]/settings/page.tsx index 2473d86f..1bf04bb0 100644 --- a/frontend/app/[team]/apps/[app]/settings/page.tsx +++ b/frontend/app/[team]/apps/[app]/settings/page.tsx @@ -1,28 +1,28 @@ 'use client' -import { GetOrganisations } from '@/apollo/queries/getOrganisations.gql' -import { GetAppDetail } from '@/apollo/queries/getAppDetail.gql' +import { GetOrganisations } from '@/graphql/queries/getOrganisations.gql' +import { GetAppDetail } from '@/graphql/queries/getAppDetail.gql' import { useLazyQuery, useQuery } from '@apollo/client' import { AppType } from '@/apollo/graphql' -import { useEffect } from 'react' +import { useContext, useEffect } from 'react' import DeleteAppDialog from '@/components/apps/DeleteAppDialog' +import { organisationContext } from '@/contexts/organisationContext' export default function AppSettings({ params }: { params: { team: string; app: string } }) { - const { data: orgsData } = useQuery(GetOrganisations) + const { activeOrganisation: organisation } = useContext(organisationContext) const [getApp, { data, loading }] = useLazyQuery(GetAppDetail) useEffect(() => { - if (orgsData) { - const organisationId = orgsData.organisations[0].id + if (organisation) { getApp({ variables: { - organisationId, + organisationId: organisation.id, appId: params.app, }, }) } - }, [getApp, orgsData, params.app]) + }, [getApp, organisation, params.app]) const app = data?.apps[0] as AppType @@ -42,12 +42,12 @@ export default function AppSettings({ params }: { params: { team: string; app: s

Permanently delete this app

- {app && ( + {organisation && app && ( )}
diff --git a/frontend/app/[team]/apps/[app]/tokens/page.tsx b/frontend/app/[team]/apps/[app]/tokens/page.tsx new file mode 100644 index 00000000..5a53aa1c --- /dev/null +++ b/frontend/app/[team]/apps/[app]/tokens/page.tsx @@ -0,0 +1,287 @@ +'use client' + +import { GetAppDetail } from '@/graphql/queries/getAppDetail.gql' +import { RotateAppKey } from '@/graphql/mutations/rotateAppKeys.gql' +import { useLazyQuery, useMutation } from '@apollo/client' +import { AppType } from '@/apollo/graphql' +import { Fragment, useContext, useEffect, useState } from 'react' +import { Button } from '@/components/common/Button' +import { copyToClipBoard } from '@/utils/clipboard' +import { FaCopy, FaExclamationTriangle, FaInfo, FaTimes } from 'react-icons/fa' +import { MdContentCopy, MdOutlineRotateLeft } from 'react-icons/md' +import { toast } from 'react-toastify' +import { Dialog, Transition } from '@headlessui/react' +import { cryptoUtils } from '@/utils/auth' +import { splitSecret } from '@/utils/keyshares' +import { Alert } from '@/components/common/Alert' +import UnlockKeyringDialog from '@/components/auth/UnlockKeyringDialog' +import { KeyringContext } from '@/contexts/keyringContext' +import clsx from 'clsx' +import { SecretTokens } from '@/components/apps/tokens/SecretTokens' +import { organisationContext } from '@/contexts/organisationContext' + +export default function Tokens({ params }: { params: { team: string; app: string } }) { + const [getApp, { data }] = useLazyQuery(GetAppDetail) + + const app = data?.apps[0] as AppType + + const [activePanel, setActivePanel] = useState<'secrets' | 'kms'>('secrets') + + const { activeOrganisation: organisation } = useContext(organisationContext) + + const { keyring } = useContext(KeyringContext) + + useEffect(() => { + if (organisation) { + getApp({ + variables: { + organisationId: organisation.id, + appId: params.app, + }, + }) + } + }, [getApp, organisation, params.app]) + + const handleCopy = (val: string) => { + copyToClipBoard(val) + toast.info('Copied') + } + + const KmsPanel = () => { + const appId = `phApp:v${app?.appVersion}:${app?.identityKey}` + + const [appSecret, setAppSecret] = useState('') + + const appSecretPlaceholder = '*'.repeat(295) + + const RotateAppDialog = () => { + const [pw, setPw] = useState('') + const [showPw, setShowPw] = useState(false) + const [loading, setLoading] = useState(false) + const [isOpen, setIsOpen] = useState(false) + const [rotateAppKeys] = useMutation(RotateAppKey) + + const closeModal = () => { + setPw('') + setIsOpen(false) + } + + const handleGenerateNewAppKey = async () => { + const APP_VERSION = 1 + + return new Promise(async (resolve, reject) => { + setTimeout(async () => { + setLoading(true) + try { + const wrapKey = await cryptoUtils.newAppWrapKey() + const newAppToken = await cryptoUtils.newAppToken() + const appSeed = await cryptoUtils.decryptedAppSeed(app.appSeed, keyring!.symmetricKey) + + const appKeys = await cryptoUtils.appKeyring(appSeed) + const appKeyShares = await splitSecret(appKeys.privateKey) + const wrappedShare = await cryptoUtils.wrappedKeyShare(appKeyShares[1], wrapKey) + await rotateAppKeys({ + variables: { + id: app.id, + appToken: newAppToken, + wrappedKeyShare: wrappedShare, + }, + }) + + setAppSecret(`pss:v${APP_VERSION}:${newAppToken}:${appKeyShares[0]}:${wrapKey}`) + + setLoading(false) + resolve(true) + } catch (error) { + console.log(error) + setLoading(false) + reject() + } + }, 500) + }) + } + + const handleSubmit = async (event: { preventDefault: () => void }) => { + event.preventDefault() + toast + .promise(handleGenerateNewAppKey, { + pending: 'Generating app keys', + success: 'Success!', + error: 'Something went wrong! Please check your password and try again.', + }) + .then(() => closeModal()) + } + + return ( + <> + + + {}}> + +
+ + +
+
+ + + +

+ Genereate new app secret +

+ +
+ + + Generate a new app secret for {app.name} + + +
+
+
+ +
+ +
+ Warning: This will revoke your current app keys. Your application + won't be able to decrypt data using the current keys. +
+
+
+ + +
+ +
+ Your new keys will be available to use immediately. You will be + able to decrypt any existing data with your new keys. Please allow + up to 60 seconds for your old keys to be revoked. +
+
+
+
+ +
+ + +
+
+
+
+
+
+
+
+
+ + ) + } + + return ( +
+
+
+ app id + +
+ {appId} +
+ +
+
+ app secret +
+ {appSecret && ( +
+ +
{"Copy this value. You won't see it again!"}
+
+ )} + {appSecret && ( + + )} +
+ {!appSecret && } +
+ + {appSecret || appSecretPlaceholder} + +
+
+ ) + } + + return ( +
+
+ {organisation && } + {keyring !== null && ( +
+
+
setActivePanel('secrets')} + className={clsx( + 'p-4 cursor-pointer border-l transition ease -ml-px w-60', + activePanel === 'secrets' + ? 'bg-zinc-300 dark:bg-zinc-800 font-semibold border-emerald-500' + : 'bg-zinc-200 dark:bg-zinc-900 hover:font-semibold border-neutral-500/40' + )} + > + Secrets +
+ {organisation?.role?.toLowerCase() === 'owner' && ( +
setActivePanel('kms')} + className={clsx( + 'p-4 cursor-pointer border-l transition ease -ml-px w-60', + activePanel === 'kms' + ? 'bg-zinc-300 dark:bg-zinc-800 font-semibold border-emerald-500' + : 'bg-zinc-200 dark:bg-zinc-900 hover:font-semibold border-neutral-500/40' + )} + > + KMS +
+ )} +
+
+ {app && activePanel === 'secrets' && ( + + )} + {app && activePanel === 'kms' && } +
+
+ )} +
+
+ ) +} diff --git a/frontend/app/[team]/apps/page.tsx b/frontend/app/[team]/apps/page.tsx index a1fcf10f..82c96679 100644 --- a/frontend/app/[team]/apps/page.tsx +++ b/frontend/app/[team]/apps/page.tsx @@ -1,32 +1,27 @@ 'use client' -import { useLazyQuery, useQuery } from '@apollo/client' -import { GetApps } from '@/apollo/queries/getApps.gql' -import { GetOrganisations } from '@/apollo/queries/getOrganisations.gql' -import { AppType, OrganisationType } from '@/apollo/graphql' +import { useLazyQuery } from '@apollo/client' +import { GetApps } from '@/graphql/queries/getApps.gql' +import { AppType } from '@/apollo/graphql' import NewAppDialog from '@/components/apps/NewAppDialog' import { FaPlus } from 'react-icons/fa' -import { useEffect, useState } from 'react' +import { useContext, useEffect } from 'react' import Link from 'next/link' import Spinner from '@/components/common/Spinner' import { AppCard } from '@/components/apps/AppCard' +import { organisationContext } from '@/contexts/organisationContext' export default function AppsHome({ params }: { params: { team: string } }) { - const [organisation, setOrganisation] = useState(undefined) - - const { data: orgsData } = useQuery(GetOrganisations) + const { activeOrganisation: organisation } = useContext(organisationContext) const [getApps, { data, loading }] = useLazyQuery(GetApps) useEffect(() => { - if (orgsData?.organisations) { + if (organisation) { const fetchData = async () => { - const org = orgsData.organisations[0] - setOrganisation(org) - const organisationId = org.id getApps({ variables: { - organisationId, + organisationId: organisation.id, appId: '', }, }) @@ -34,7 +29,8 @@ export default function AppsHome({ params }: { params: { team: string } }) { fetchData() } - }, [getApps, orgsData]) + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [organisation]) const apps = data?.apps as AppType[] @@ -50,7 +46,7 @@ export default function AppsHome({ params }: { params: { team: string } }) { ))} - {organisation && !loading && ( + {organisation && apps && (
{ + if (!loading && organisations !== null) { + // if there are no organisations for this user, send to onboarding + if (organisations.length === 0) { + router.push('/signup') + } + + // try and get org being access from route params in the list of organisations for this user + const org = organisations.find((org) => org.name === params.team) + + // update active organisation if it exists + if (org) setActiveOrganisation(org) + // else update the route to the active organisation + else router.push(`/${activeOrganisation!.name}`) + } + }, [activeOrganisation, organisations, params.team, router, loading, setActiveOrganisation]) + const path = usePathname() const showNav = !path?.split('/').includes('newdevice') @@ -21,14 +46,14 @@ export default function RootLayout({ return (
{showNav && } {showNav && } -
{children}
+
{children}
) } diff --git a/frontend/app/[team]/members/page.tsx b/frontend/app/[team]/members/page.tsx new file mode 100644 index 00000000..15c5d191 --- /dev/null +++ b/frontend/app/[team]/members/page.tsx @@ -0,0 +1,802 @@ +'use client' + +import GetOrganisationMembers from '@/graphql/queries/organisation/getOrganisationMembers.gql' +import GetInvites from '@/graphql/queries/organisation/getInvites.gql' +import GetApps from '@/graphql/queries/getApps.gql' +import { GetAppEnvironments } from '@/graphql/queries/secrets/getAppEnvironments.gql' +import { GetEnvironmentKey } from '@/graphql/queries/secrets/getEnvironmentKey.gql' +import InviteMember from '@/graphql/mutations/organisation/inviteNewMember.gql' +import DeleteOrgInvite from '@/graphql/mutations/organisation/deleteInvite.gql' +import RemoveMember from '@/graphql/mutations/organisation/deleteOrgMember.gql' +import UpdateMemberRole from '@/graphql/mutations/organisation/updateOrgMemberRole.gql' +import AddMemberToApp from '@/graphql/mutations/apps/addAppMember.gql' +import { useLazyQuery, useMutation, useQuery } from '@apollo/client' +import { Fragment, useContext, useEffect, useState } from 'react' +import { + OrganisationMemberInviteType, + OrganisationMemberType, + AppType, + ApiOrganisationMemberRoleChoices, + EnvironmentType, +} from '@/apollo/graphql' +import { Button } from '@/components/common/Button' +import { organisationContext } from '@/contexts/organisationContext' +import { relativeTimeFromDates } from '@/utils/time' +import { Dialog, Listbox, Transition } from '@headlessui/react' +import { + FaCheckSquare, + FaChevronDown, + FaCopy, + FaPlus, + FaSquare, + FaTimes, + FaTrashAlt, + FaUserAlt, +} from 'react-icons/fa' +import clsx from 'clsx' +import { cryptoUtils } from '@/utils/auth' +import { copyToClipBoard } from '@/utils/clipboard' +import { toast } from 'react-toastify' +import { useSession } from 'next-auth/react' +import { Avatar } from '@/components/common/Avatar' +import { userIsAdmin } from '@/utils/permissions' +import { RoleLabel } from '@/components/users/RoleLabel' +import { KeyringContext } from '@/contexts/keyringContext' +import { unwrapEnvSecretsForUser, wrapEnvSecretsForUser } from '@/utils/environments' +import UnlockKeyringDialog from '@/components/auth/UnlockKeyringDialog' + +const handleCopy = (val: string) => { + copyToClipBoard(val) + toast.info('Copied', { autoClose: 2000 }) +} + +const inviteIsExpired = (invite: OrganisationMemberInviteType) => { + return new Date(invite.expiresAt) < new Date() +} + +const RoleSelector = (props: { member: OrganisationMemberType }) => { + const { member } = props + + const { activeOrganisation: organisation } = useContext(organisationContext) + const { keyring } = useContext(KeyringContext) + + const { data: appsData, loading: appsLoading } = useQuery(GetApps, { + variables: { organisationId: organisation!.id, appId: '' }, + }) + const [getAppEnvs] = useLazyQuery(GetAppEnvironments) + const [getEnvKey] = useLazyQuery(GetEnvironmentKey) + const [updateRole] = useMutation(UpdateMemberRole) + const [addMemberToApp] = useMutation(AddMemberToApp) + + const [role, setRole] = useState(member.role) + + const isOwner = role.toLowerCase() === 'owner' + + const activeUserIsAdmin = organisation ? userIsAdmin(organisation.role!) : false + + /** + * Handles the upgrade of a user from 'dev' to 'admin'. + * Env keys for all apps, are fetched and decrypted by the active user, then each key is re-encrypted for the new user and saved on the backend via the addMemberToApp mutation + * + * @returns {void} + */ + const upgradeDevToAdmin = () => { + if (appsData) { + const apps = appsData.apps + + // Function to process an individual app + const processApp = async (app: AppType) => { + //const keyring = await validateKeyring(password); + const { data: appEnvsData } = await getAppEnvs({ variables: { appId: app.id } }) + + const appEnvironments = appEnvsData.appEnvironments as EnvironmentType[] + + const envKeyPromises = appEnvironments.map(async (env: EnvironmentType) => { + const { data } = await getEnvKey({ + variables: { + envId: env.id, + appId: app.id, + }, + }) + + const { + wrappedSeed: userWrappedSeed, + wrappedSalt: userWrappedSalt, + identityKey, + } = data.environmentKeys[0] + + const { seed, salt } = await unwrapEnvSecretsForUser( + userWrappedSeed, + userWrappedSalt, + keyring! + ) + + const { wrappedSeed, wrappedSalt } = await wrapEnvSecretsForUser({ seed, salt }, member) + + return { + envId: env.id, + userId: member.id, + identityKey, + wrappedSeed, + wrappedSalt, + } + }) + + const envKeyInputs = await Promise.all(envKeyPromises) + + await addMemberToApp({ + variables: { memberId: member.id, appId: app.id, envKeys: envKeyInputs }, + }) + } + + // Process each app sequentially + const processAppsSequentially = async () => { + for (const app of apps) { + await processApp(app) + } + } + + // Call the function to process all apps sequentially + processAppsSequentially() + .then(async () => { + // All apps have been processed + await updateRole({ + variables: { + memberId: member.id, + role: 'admin', + }, + }) + toast.success('Updated member role', { autoClose: 2000 }) + }) + .catch((error) => { + console.error('Error processing apps:', error) + }) + } + } + + const handleUpdateRole = async (newRole: string) => { + setRole(newRole) + if (newRole.toLowerCase() === 'admin') upgradeDevToAdmin() + else { + await updateRole({ + variables: { + memberId: member.id, + role: newRole, + }, + }) + toast.success('Updated member role', { autoClose: 2000 }) + } + } + + const roleOptions = Object.keys(ApiOrganisationMemberRoleChoices).filter( + (option) => option !== 'Owner' + ) + + const disabled = isOwner || !activeUserIsAdmin || member.self! + + return disabled ? ( + + ) : ( +
+ + {({ open }) => ( + <> + +
+ + {!disabled && ( + + )} +
+
+ +
+ {roleOptions.map((role: string) => ( + + {({ active, selected }) => ( +
+ +
+ )} +
+ ))} +
+
+ + )} +
+
+ ) +} + +const InviteDialog = (props: { organisationId: string }) => { + const { organisationId } = props + + const { data: appsData, loading: appsLoading } = useQuery(GetApps, { + variables: { organisationId, appId: '' }, + }) + const [createInvite] = useMutation(InviteMember) + + const [isOpen, setIsOpen] = useState(false) + + const [email, setEmail] = useState('') + const [apps, setApps] = useState[]>([]) + + const [inviteLink, setInviteLink] = useState('') + + const roleOptions = Object.keys(ApiOrganisationMemberRoleChoices).filter( + (option) => option !== 'Owner' + ) + + const isLoading = appsLoading + + const reset = () => { + setEmail('') + setApps([]) + setInviteLink('') + } + + const closeModal = () => { + reset() + setIsOpen(false) + } + + const openModal = () => { + setIsOpen(true) + } + + const handleClose = () => { + closeModal() + } + + const handleInvite = async (event: { preventDefault: () => void }) => { + event.preventDefault() + const { data } = await createInvite({ + variables: { + email, + orgId: organisationId, + apps: apps.map((app) => app.id), + role: 'dev', + }, + refetchQueries: [ + { + query: GetInvites, + variables: { + orgId: organisationId, + }, + }, + ], + }) + + setInviteLink(cryptoUtils.getInviteLink(data?.inviteOrganisationMember.invite.id)) + } + + const AppSelector = (props: { app: AppType }) => { + const { id: appId, name: appName } = props.app + + const isSelected = apps.map((app) => app.name).includes(appName) + + const handleAppClick = () => { + if (isSelected) { + setApps(apps.filter((app) => app.name !== appName)) + } else setApps([...apps, ...[{ id: appId, name: appName }]]) + } + + return ( +
+ {isSelected ? ( + + ) : ( + + )} +
+ {appName} +
+
+ ) + } + + return ( + <> +
+ +
+ + + + +
+ + +
+
+ + + +

+ Invite a new member +

+ + +
+ + {!isLoading && ( +
+
+ {!inviteLink && ( +
+
+
+ + setEmail(e.target.value)} + className="w-3/4" + /> +
+
+ +
+ + +
+ {appsData.apps.map((appOption: AppType) => ( + + ))} +
+
+ +
+ + +
+
+ )} + {inviteLink && ( +
+
+

+ Invite sent +

+

+ An invite link has been sent by email to{' '} + {email}. You can also share the + link below to invite this user to your organisation. This invite + will expire in 72 hours. +

+
+
+
+ {inviteLink} +
+ +
+
+ )} +
+
+ )} +
+
+
+
+
+
+ + ) +} + +export default function Members({ params }: { params: { team: string } }) { + const [getMembers, { data: membersData }] = useLazyQuery(GetOrganisationMembers) + const [getInvites, { data: invitesData }] = useLazyQuery(GetInvites) + const [deleteInvite] = useMutation(DeleteOrgInvite) + + const sortedInvites: OrganisationMemberInviteType[] = + invitesData?.organisationInvites + ?.slice() // Create a shallow copy of the array to avoid modifying the original + .sort((a: OrganisationMemberInviteType, b: OrganisationMemberInviteType) => { + // Compare the createdAt timestamps in descending order + return new Date(b.createdAt).getTime() - new Date(a.createdAt).getTime() + }) || [] + + const { activeOrganisation: organisation } = useContext(organisationContext) + + const activeUserIsAdmin = organisation ? userIsAdmin(organisation.role!) : false + + const { data: session } = useSession() + + useEffect(() => { + if (organisation) { + getMembers({ + variables: { + organisationId: organisation.id, + role: null, + }, + pollInterval: 5000, + }) + getInvites({ + variables: { + orgId: organisation.id, + }, + pollInterval: 5000, + }) + } + }, [getInvites, getMembers, organisation]) + + const DeleteInviteConfirmDialog = (props: { inviteId: string }) => { + const { inviteId } = props + + const [isOpen, setIsOpen] = useState(false) + + const closeModal = () => { + setIsOpen(false) + } + + const openModal = () => { + setIsOpen(true) + } + + const handleDeleteInvite = async (inviteId: string) => { + await deleteInvite({ + variables: { + inviteId, + }, + refetchQueries: [ + { + query: GetInvites, + variables: { + orgId: organisation!.id, + }, + }, + ], + }) + } + + return ( + <> +
+ +
+ + + + +
+ + +
+
+ + + +

+ Delete Invite +

+ + +
+ +
+

+ Are you sure you want to delete this invite? +

+
+ + +
+
+
+
+
+
+
+
+ + ) + } + + const DeleteMemberConfirmDialog = (props: { member: OrganisationMemberType }) => { + const { member } = props + + const [removeMember] = useMutation(RemoveMember) + + const [isOpen, setIsOpen] = useState(false) + + const closeModal = () => { + setIsOpen(false) + } + + const openModal = () => { + setIsOpen(true) + } + + const handleRemoveMember = async () => { + await removeMember({ + variables: { memberId: member.id }, + refetchQueries: [ + { + query: GetOrganisationMembers, + variables: { organisationId: organisation?.id, role: null }, + }, + ], + }) + } + + const allowDelete = !member.self! && activeUserIsAdmin && member.role.toLowerCase() !== 'owner' + + return ( + <> + {allowDelete && ( +
+ +
+ )} + + + + +
+ + +
+
+ + + +

+ Remove member +

+ + +
+ +
+

+ Are you sure you want to remove {member.fullName || member.email} from this + organisation? +

+
+ + +
+
+
+
+
+
+
+
+ + ) + } + + return ( +
+
+
+

{params.team} Members

+

Manage organisation members and roles.

+
+
+
+ {organisation && } +
+ + + + + + + + + {activeUserIsAdmin && } + + + + {membersData?.organisationMembers.map((member: OrganisationMemberType) => ( + + + + + + + ))} + {sortedInvites.map((invite: OrganisationMemberInviteType) => ( + + + + + + + ))} + +
+ User + + Role + + Joined +
+ +
+ {member.fullName || member.email} + {member.fullName && ( + {member.email} + )} +
+
+
+ +
+
+ {relativeTimeFromDates(new Date(member.createdAt))} + + {!member.self! && + activeUserIsAdmin && + member.role.toLowerCase() !== 'owner' && ( + + )} +
+
+ +
+
+
+ {invite.inviteeEmail}{' '} + + (invited by{' '} + {invite.invitedBy.self + ? 'You' + : invite.invitedBy.fullName || invite.invitedBy.email} + ) + +
+
+
+ {inviteIsExpired(invite) + ? `Expired ${relativeTimeFromDates(new Date(invite.expiresAt))}` + : `Invited ${relativeTimeFromDates(new Date(invite.createdAt))}`} + + {!inviteIsExpired(invite) && ( + + )} + +
+
+
+ {activeUserIsAdmin && organisation && ( + + )} +
+ ) +} diff --git a/frontend/app/[team]/newdevice/page.tsx b/frontend/app/[team]/newdevice/page.tsx index 0a3778bb..d119422b 100644 --- a/frontend/app/[team]/newdevice/page.tsx +++ b/frontend/app/[team]/newdevice/page.tsx @@ -4,44 +4,82 @@ import { Button } from '@/components/common/Button' import { AccountPassword } from '@/components/onboarding/AccountPassword' import { AccountSeedChecker } from '@/components/onboarding/AccountSeedChecker' import { Step, Stepper } from '@/components/onboarding/Stepper' -import { useEffect, useState } from 'react' +import { useContext, useEffect, useState } from 'react' import { MdContentPaste, MdOutlineKey } from 'react-icons/md' -import { GetOrganisations } from '@/apollo/queries/getOrganisations.gql' -import { useQuery } from '@apollo/client' +import { GetOrganisations } from '@/graphql/queries/getOrganisations.gql' +import { useMutation, useQuery } from '@apollo/client' import { OrganisationType } from '@/apollo/graphql' +import UpdateWrappedSecrets from '@/graphql/mutations/organisation/updateUserWrappedSecrets.gql' import { cryptoUtils } from '@/utils/auth' import { useSession } from 'next-auth/react' import { toast } from 'react-toastify' -import { setLocalOrg } from '@/utils/localStorage' +import { setLocalKeyring } from '@/utils/localStorage' import { useRouter } from 'next/navigation' import UserMenu from '@/components/UserMenu' +import { organisationContext } from '@/contexts/organisationContext' +import { FaEye, FaEyeSlash, FaInfo } from 'react-icons/fa' +import { KeyringContext } from '@/contexts/keyringContext' export default function NewDevice({ params }: { params: { team: string } }) { const { data: session } = useSession() const [inputs, setInputs] = useState>([]) const [pw, setPw] = useState('') const [pw2, setPw2] = useState('') + const [showPw, setShowPw] = useState(false) const [step, setStep] = useState(0) - const { loading, error, data } = useQuery(GetOrganisations) - const router = useRouter() - - const steps: Step[] = [ + const [steps, setSteps] = useState([ { index: 0, - name: 'Recovery phrase', - icon: , - title: 'Recovery phrase', - description: 'Please enter the your account recovery phrase in the correct order below.', - }, - { - index: 1, name: 'Sudo password', icon: , title: 'Sudo password', description: - "Please set up a strong 'sudo' password to continue. This will be used to to perform administrative tasks and to encrypt keys locally on this device.", + "Please set up a strong 'sudo' password to continue. This will be used to encrypt keys and perform administrative tasks.", }, - ] + ]) + + const [updateWrappedSecrets] = useMutation(UpdateWrappedSecrets) + + const [recoveryRequired, setRecoveryRequired] = useState(false) + + const router = useRouter() + + const { organisations } = useContext(organisationContext) + + const { setKeyring } = useContext(KeyringContext) + + const org = organisations?.find((org) => org.name === params.team) ?? null + + //const recoveryRequired = org?.keyring === null + + useEffect(() => { + if (org) { + setRecoveryRequired(org.keyring === null || org.keyring === '') + } + }, [org]) + + useEffect(() => { + if (recoveryRequired) + setSteps([ + { + index: 0, + name: 'Recovery phrase', + icon: , + title: 'Recovery phrase', + description: 'Please enter the your account recovery phrase in the correct order below.', + }, + { + index: 1, + name: 'Sudo password', + icon: , + title: 'Sudo password', + description: + "Please set up a strong 'sudo' password to continue. This will be used to encrypt keys and perform administrative tasks.", + }, + ]) + + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [recoveryRequired]) const handleInputUpdate = (newValue: string, index: number) => { if (newValue.split(' ').length === 24) { @@ -52,31 +90,72 @@ export default function NewDevice({ params }: { params: { team: string } }) { const handleLocalAccountSetup = () => { return new Promise<{ publicKey: string; encryptedKeyring: string }>((resolve, reject) => { setTimeout(async () => { - const mnemonic = inputs.join(' ') - const orgs = data.organisations as OrganisationType[] - const org = orgs.find((org) => org.name === params.team) - const accountSeed = await cryptoUtils.organisationSeed(mnemonic, org?.id!) - - const accountKeyRing = await cryptoUtils.organisationKeyring(accountSeed) - if (accountKeyRing.publicKey !== org?.identityKey) { - toast.error('Incorrect account recovery key!') - reject('Incorrect account recovery key') - } + if (recoveryRequired) { + const mnemonic = inputs.join(' ') - const deviceKey = await cryptoUtils.deviceVaultKey(pw, session?.user?.email!) - const encryptedKeyring = await cryptoUtils.encryptAccountKeyring(accountKeyRing, deviceKey) - const encryptedMnemonic = await cryptoUtils.encryptAccountRecovery(mnemonic, deviceKey) - setLocalOrg({ - email: session?.user?.email!, - org: org!, - keyring: encryptedKeyring, - recovery: encryptedMnemonic, - }) + const accountSeed = await cryptoUtils.organisationSeed(mnemonic, org?.id!) - resolve({ - publicKey: accountKeyRing.publicKey, - encryptedKeyring, - }) + const accountKeyRing = await cryptoUtils.organisationKeyring(accountSeed) + if (accountKeyRing.publicKey !== org?.identityKey) { + toast.error('Incorrect account recovery key!') + reject('Incorrect account recovery key') + } + + const deviceKey = await cryptoUtils.deviceVaultKey(pw, session?.user?.email!) + const encryptedKeyring = await cryptoUtils.encryptAccountKeyring( + accountKeyRing, + deviceKey + ) + const encryptedMnemonic = await cryptoUtils.encryptAccountRecovery(mnemonic, deviceKey) + + setKeyring(accountKeyRing) + + setLocalKeyring({ + email: session?.user?.email!, + org: org!, + keyring: encryptedKeyring, + recovery: encryptedMnemonic, + }) + + await updateWrappedSecrets({ + variables: { + orgId: org!.id, + wrappedKeyring: encryptedKeyring, + wrappedRecovery: encryptedMnemonic, + }, + }) + + resolve({ + publicKey: accountKeyRing.publicKey, + encryptedKeyring, + }) + } else { + try { + const encryptedKeyring = org!.keyring! + const deviceKey = await cryptoUtils.deviceVaultKey(pw, session?.user?.email!) + const accountKeyRing = await cryptoUtils.decryptAccountKeyring( + encryptedKeyring, + deviceKey + ) + + setKeyring(accountKeyRing) + + setLocalKeyring({ + email: session?.user?.email!, + org: org!, + keyring: encryptedKeyring, + recovery: org?.recovery!, + }) + + resolve({ + publicKey: accountKeyRing.publicKey, + encryptedKeyring, + }) + } catch (error) { + toast.error('Something went wrong! Please check your sudo password and try again') + reject('Something went wrong! Please check your sudo password and try again') + } + } }, 1000) }) } @@ -84,9 +163,9 @@ export default function NewDevice({ params }: { params: { team: string } }) { const incrementStep = async (event: { preventDefault: () => void }) => { event.preventDefault() - if (step !== steps.length - 1) setStep(step + 1) + if (step !== steps.length - 1 && recoveryRequired) setStep(step + 1) if (step === steps.length - 1) { - if (pw !== pw2) { + if (recoveryRequired && pw !== pw2) { toast.error("Passwords don't match") return false } @@ -119,19 +198,22 @@ export default function NewDevice({ params }: { params: { team: string } }) { Welcome back

- { - "Looks like your signing in on a new browser or machine. You'll need to setup your account keys for this device before you can proceed." - } + {recoveryRequired + ? "Looks like you are signing in on a new browser or machine. You'll need to setup your account keys for this device before you can proceed." + : 'Looks like you are signing in on a new browser or machine. Please enter your sudo password to setup your keyring on this device.'}

-
- -
- {step === 0 && ( + {recoveryRequired && ( +
+ +
+ )} + + {step === 0 && recoveryRequired && ( )} - {step === 1 && } -
-
- {step !== 0 && ( - +
+
+ +
+
+
+ - )} +
-
- + )} + {recoveryRequired && ( +
+
+ {step !== 0 && ( + + )} +
+
+ +
-
+ )}
diff --git a/frontend/app/[team]/page.tsx b/frontend/app/[team]/page.tsx index f4deb228..6980829c 100644 --- a/frontend/app/[team]/page.tsx +++ b/frontend/app/[team]/page.tsx @@ -2,21 +2,25 @@ import AppsHomeCard from '@/components/apps/AppsHomeCard' import { useQuery } from '@apollo/client' -import GetOrganisations from '@/apollo/queries/getOrganisations.gql' +import GetOrganisations from '@/graphql/queries/getOrganisations.gql' import Link from 'next/link' +import { useContext } from 'react' +import { organisationContext } from '@/contexts/organisationContext' export default function AppsHome({ params }: { params: { team: string } }) { - const { loading, error, data } = useQuery(GetOrganisations) + //const { loading, error, data } = useQuery(GetOrganisations) + + const { activeOrganisation: organisation } = useContext(organisationContext) return ( <>
-

{params.team} Home

+

{organisation?.name} Home

- {!loading && ( + {organisation && ( - + )}
diff --git a/frontend/app/[team]/settings/page.tsx b/frontend/app/[team]/settings/page.tsx index 02c83b30..6cb7cc37 100644 --- a/frontend/app/[team]/settings/page.tsx +++ b/frontend/app/[team]/settings/page.tsx @@ -1,12 +1,259 @@ +'use client' + +import { Alert } from '@/components/common/Alert' +import { Avatar } from '@/components/common/Avatar' +import { Button } from '@/components/common/Button' import { ModeToggle } from '@/components/common/ModeToggle' +import { AccountRecovery } from '@/components/onboarding/AccountRecovery' +import { RoleLabel } from '@/components/users/RoleLabel' +import { organisationContext } from '@/contexts/organisationContext' +import { cryptoUtils } from '@/utils/auth' +import { copyRecoveryKit, generateRecoveryPdf } from '@/utils/recovery' +import { Dialog, Transition } from '@headlessui/react' +import { useSession } from 'next-auth/react' +import { Fragment, useContext, useState } from 'react' +import { FaEye, FaEyeSlash, FaMoon, FaSun, FaTimes } from 'react-icons/fa' +import { toast } from 'react-toastify' + +const ViewRecoveryDialog = () => { + const { activeOrganisation } = useContext(organisationContext) + + const { data: session } = useSession() + + const [isOpen, setIsOpen] = useState(false) + const [password, setPassword] = useState('') + const [showPw, setShowPw] = useState(false) + const [recovery, setRecovery] = useState('') + + const handleDecryptRecovery = async (event: { preventDefault: () => void }) => { + event.preventDefault() + + const deviceKey = await cryptoUtils.deviceVaultKey(password, session?.user?.email!) + + const decryptedRecovery = await cryptoUtils.decryptAccountRecovery( + activeOrganisation?.recovery!, + deviceKey + ) + setRecovery(decryptedRecovery) + } + + const reset = () => { + setRecovery('') + setPassword('') + } + + const closeModal = () => { + reset() + setIsOpen(false) + } + + const openModal = () => { + setIsOpen(true) + } + + const handleDownloadRecoveryKit = async () => { + toast.promise( + generateRecoveryPdf( + recovery, + session?.user?.email!, + activeOrganisation!.name, + session?.user?.name || undefined + ), + { + pending: 'Generating recovery kit', + success: 'Downloaded recovery kit', + } + ) + } + + const handleCopyRecoveryKit = () => { + copyRecoveryKit( + recovery, + session?.user?.email!, + activeOrganisation!.name, + session?.user?.name || undefined + ) + } + + return ( + <> +
+ +
+

Your account keys are encrypted.

+ +

+ Store your account recovery kit in a safe place if you haven't already. If you + forget your sudo password, it is the only way to restore your accout keys. +

+
+
+
+ +
+
+ + + + +
+ + +
+
+ + + +

+ View account recovery +

+ + +
+ +
+ {recovery && ( + + )} + + {!recovery && ( +
+
+

+ Please enter your sudo password to decrypt your account + recovery phrase. +

+
+
+
+ +
+ setPassword(e.target.value)} + type={showPw ? 'text' : 'password'} + minLength={16} + required + autoFocus + className="custom w-full text-zinc-800 font-mono dark:text-white bg-zinc-100 dark:bg-zinc-800 rounded-md ph-no-capture" + /> + +
+
+
+ +
+
+
+ )} +
+
+
+
+
+
+
+ + ) +} export default function Settings({ params }: { params: { team: string } }) { + const { activeOrganisation } = useContext(organisationContext) + + const { data: session } = useSession() + return ( -
-

{params.team} Settings

-
- Theme: +
+

Settings

+ + {activeOrganisation && ( +
+
+

Account

+

Account information and recovery.

+
+
+ +
+
+ {session?.user?.name} + {session?.user?.email} +
+ +
+ + at {activeOrganisation?.name} +
+
+
+ +
+
Recovery
+ +
+ +
+
Public key
+ + {activeOrganisation?.identityKey} + +
+
+ )} + +
+
+

App

+

Control the behavior and appearance of UI elements.

+
+
+
Theme
+
+ + + +
+
-
+ ) } diff --git a/frontend/app/[team]/tokens/page.tsx b/frontend/app/[team]/tokens/page.tsx new file mode 100644 index 00000000..76da4bf2 --- /dev/null +++ b/frontend/app/[team]/tokens/page.tsx @@ -0,0 +1,490 @@ +'use client' + +import { CreateNewUserToken } from '@/graphql/mutations/users/createUserToken.gql' +import { RevokeUserToken } from '@/graphql/mutations/users/deleteUserToken.gql' +import { GetUserTokens } from '@/graphql/queries/users/getUserTokens.gql' +import { generateUserToken } from '@/utils/environments' +import { ServiceTokenType, UserTokenType } from '@/apollo/graphql' +import { getUserKxPublicKey, getUserKxPrivateKey } from '@/utils/crypto' +import { useLazyQuery, useMutation } from '@apollo/client' +import { useState, useEffect, useContext, Fragment } from 'react' +import { KeyringContext } from '@/contexts/keyringContext' +import { Button } from '@/components/common/Button' +import { + FaCircle, + FaDotCircle, + FaExclamationTriangle, + FaKey, + FaPlus, + FaTimes, + FaTrashAlt, +} from 'react-icons/fa' +import { getUnixTimeStampinFuture, relativeTimeFromDates } from '@/utils/time' +import { Dialog, RadioGroup, Transition } from '@headlessui/react' +import { copyToClipBoard } from '@/utils/clipboard' +import { MdContentCopy } from 'react-icons/md' +import { toast } from 'react-toastify' +import clsx from 'clsx' +import { organisationContext } from '@/contexts/organisationContext' +import UnlockKeyringDialog from '@/components/auth/UnlockKeyringDialog' + +interface ExpiryOptionT { + name: string + getExpiry: () => number | null +} + +const handleCopy = (val: string) => { + copyToClipBoard(val) + toast.info('Copied', { + autoClose: 2000, + }) +} + +const tokenExpiryOptions: ExpiryOptionT[] = [ + { + name: 'Never', + getExpiry: () => null, + }, + { + name: '7 days', + getExpiry: () => getUnixTimeStampinFuture(7), + }, + { + name: '30 days', + getExpiry: () => getUnixTimeStampinFuture(30), + }, + { + name: '60 days', + getExpiry: () => getUnixTimeStampinFuture(60), + }, + { + name: '90 days', + getExpiry: () => getUnixTimeStampinFuture(90), + }, +] + +const compareExpiryOptions = (a: ExpiryOptionT, b: ExpiryOptionT) => { + return a.getExpiry() === b.getExpiry() +} + +const humanReadableExpiry = (expiryOption: ExpiryOptionT) => + expiryOption.getExpiry() === null + ? 'This token will never expire.' + : `This token will expire on ${new Date(expiryOption.getExpiry()!).toLocaleDateString()}.` + +const CreateUserTokenDialog = (props: { organisationId: string }) => { + const { organisationId } = props + + const { keyring } = useContext(KeyringContext) + + const [isOpen, setIsOpen] = useState(false) + const [name, setName] = useState('') + const [expiry, setExpiry] = useState(tokenExpiryOptions[0]) + + const [userToken, setUserToken] = useState('') + const [createUserToken] = useMutation(CreateNewUserToken) + + const reset = () => { + setName('') + setUserToken('') + } + + const closeModal = () => { + reset() + setIsOpen(false) + } + + const openModal = () => { + setIsOpen(true) + } + + const handleCreateNewUserToken = async (event: { preventDefault: () => void }) => { + event.preventDefault() + + if (name.length === 0) { + toast.error('You must enter a name for the token') + return false + } + + if (keyring) { + const userKxKeys = { + publicKey: await getUserKxPublicKey(keyring.publicKey), + privateKey: await getUserKxPrivateKey(keyring.privateKey), + } + + const { pssUser, mutationPayload } = await generateUserToken( + organisationId, + userKxKeys, + name, + expiry.getExpiry() + ) + + await createUserToken({ + variables: mutationPayload, + refetchQueries: [ + { + query: GetUserTokens, + variables: { + organisationId, + }, + }, + ], + }) + + setUserToken(pssUser) + } else { + console.log('keyring unavailable') + } + } + + return ( + <> +
+ +
+ + + {}}> + +
+ + +
+
+ + + +

+ Create a new User token +

+ + +
+ + {userToken ? ( +
+
+
+ + user token + +
+ {userToken && ( +
+ +
+ {"Copy this value. You won't see it again!"} +
+
+ )} + {userToken && ( + + )} +
+
+ + {userToken} + +
+
+ ) : ( +
+
+ + setName(e.target.value)} + /> +
+ +
+ + + + +
+ {tokenExpiryOptions.map((option) => ( + + {({ active, checked }) => ( +
+ {checked ? ( + + ) : ( + + )} + {option.name} +
+ )} +
+ ))} +
+
+ + {humanReadableExpiry(expiry)} + +
+ +
+ + +
+
+ )} +
+
+
+
+
+
+ + ) +} + +export default function Tokens({ params }: { params: { team: string } }) { + const [getUserTokens, { data: userTokensData }] = useLazyQuery(GetUserTokens) + + const [deleteUserToken] = useMutation(RevokeUserToken) + + const { activeOrganisation: organisation } = useContext(organisationContext) + + const organisationId = organisation?.id + + const handleDeleteUserToken = async (tokenId: string) => { + await deleteUserToken({ + variables: { tokenId }, + refetchQueries: [ + { + query: GetUserTokens, + variables: { + organisationId, + }, + }, + ], + }) + } + + useEffect(() => { + if (organisationId) { + getUserTokens({ + variables: { + organisationId, + }, + }) + } + }, [getUserTokens, organisationId]) + + const userTokens = + [...(userTokensData?.userTokens || [])].sort((a: UserTokenType, b: UserTokenType) => { + return new Date(b.createdAt).getTime() - new Date(a.createdAt).getTime() + }) || [] + + const DeleteConfirmDialog = (props: { + token: UserTokenType | ServiceTokenType + onDelete: Function + }) => { + const { token, onDelete } = props + + const [isOpen, setIsOpen] = useState(false) + + const closeModal = () => { + setIsOpen(false) + } + + const openModal = () => { + setIsOpen(true) + } + + return ( + <> +
+ +
+ + + + +
+ + +
+
+ + + +

+ Delete{' '} + + {token.name} + +

+ + +
+ +
+

+ Are you sure you want to delete this token? +

+
+ + +
+
+
+
+
+
+
+
+ + ) + } + + const CreatedToken = (props: { + token: ServiceTokenType | UserTokenType + deleteHandler: Function + }) => { + const { token, deleteHandler } = props + + const isExpired = token.expiresAt === null ? false : new Date(token.expiresAt) < new Date() + + return ( + + + +
+
{token.name}
+
+
+
Created {relativeTimeFromDates(new Date(token.createdAt))}
+
+
+
+ + + + {isExpired ? 'Expired' : 'Expires'}{' '} + {token.expiresAt ? relativeTimeFromDates(new Date(token.expiresAt)) : 'never'} + + + + + + + ) + } + + return ( +
+ {organisation && } +
+
+

User tokens

+

+ Tokens used to authenticate with the CLI from personal devices. Used for development and + manual configuration. +

+
+
+
+
+ +
+ + + + + + + + + + + {userTokens.map((userToken: UserTokenType) => ( + + ))} + +
+ token + + expiry +
+
+
+
+
+ ) +} diff --git a/frontend/app/globals.css b/frontend/app/globals.css index ec6a3eb8..8371afa0 100644 --- a/frontend/app/globals.css +++ b/frontend/app/globals.css @@ -39,6 +39,8 @@ --callout-border-rgb: 172, 175, 176; --card-rgb: 180, 185, 188; --card-border-rgb: 131, 134, 135; + + --toastify-color-success: rgb(16 185 129)!important; } @media (prefers-color-scheme: dark) { @@ -71,6 +73,8 @@ --callout-border-rgb: 108, 108, 108; --card-rgb: 100, 100, 100; --card-border-rgb: 200, 200, 200; + + --toastify-color-success: rgb(16 185 129)!important; } } @@ -109,14 +113,18 @@ a { @layer components { input { - @apply bg-white dark:bg-zinc-800 dark:bg-opacity-60 p-2 rounded-md text-black dark:text-white border border-zinc-300 dark:border-none focus:outline outline-emerald-500; + @apply p-2 focus:outline-none + } + + input:not(.custom) { + @apply bg-zinc-100 dark:bg-zinc-800 dark:bg-opacity-60 rounded-md text-zinc-800 dark:text-white ring-1 ring-inset ring-neutral-500/40 focus:ring-1 focus:ring-emerald-500 group-focus-within:invalid:ring-red-500 focus:ring-inset; } textarea { - @apply bg-white dark:bg-zinc-800 dark:bg-opacity-60 p-2 rounded-md text-black dark:text-white border border-zinc-300 dark:border-none focus:outline outline-emerald-500; + @apply bg-zinc-100 dark:bg-zinc-800 dark:bg-opacity-60 p-2 rounded-md text-zinc-800 dark:text-white border border-zinc-300 dark:border-none focus:outline outline-emerald-500; } select { - @apply bg-white dark:bg-zinc-800 dark:bg-opacity-60 p-2 rounded-md text-black dark:text-white border border-zinc-300 dark:border-none focus:outline outline-emerald-500; + @apply bg-zinc-100 dark:bg-zinc-800 dark:bg-opacity-60 p-2 rounded-md text-zinc-800 dark:text-white border border-zinc-300 dark:border-none focus:outline outline-emerald-500; } } diff --git a/frontend/app/head.tsx b/frontend/app/head.tsx index 3586955c..fb8ec7fd 100644 --- a/frontend/app/head.tsx +++ b/frontend/app/head.tsx @@ -4,6 +4,7 @@ export default function Head() { Phase Console + ) diff --git a/frontend/app/invite/[invite]/layout.tsx b/frontend/app/invite/[invite]/layout.tsx new file mode 100644 index 00000000..c188849b --- /dev/null +++ b/frontend/app/invite/[invite]/layout.tsx @@ -0,0 +1,11 @@ +import '@/app/globals.css' +import OnboardingNavbar from '@/components/layout/OnboardingNavbar' + +export default function RootLayout({ children }: { children: React.ReactNode }) { + return ( +
+ + {children} +
+ ) +} diff --git a/frontend/app/invite/[invite]/page.tsx b/frontend/app/invite/[invite]/page.tsx new file mode 100644 index 00000000..b7bce32a --- /dev/null +++ b/frontend/app/invite/[invite]/page.tsx @@ -0,0 +1,324 @@ +'use client' + +import { cryptoUtils } from '@/utils/auth' +import VerifyInvite from '@/graphql/queries/organisation/validateOrganisationInvite.gql' +import AcceptOrganisationInvite from '@/graphql/mutations/organisation/acceptInvite.gql' +import { useLazyQuery, useMutation } from '@apollo/client' +import { HeroPattern } from '@/components/common/HeroPattern' +import { Button } from '@/components/common/Button' +import { FaArrowRight } from 'react-icons/fa' +import Loading from '@/app/loading' +import { useEffect, useState } from 'react' +import { Step, Stepper } from '@/components/onboarding/Stepper' +import { AccountPassword } from '@/components/onboarding/AccountPassword' +import { AccountRecovery } from '@/components/onboarding/AccountRecovery' +import { MdKey, MdOutlinePassword } from 'react-icons/md' +import { toast } from 'react-toastify' +import { OrganisationMemberInviteType } from '@/apollo/graphql' +import { useSession } from 'next-auth/react' +import { setLocalKeyring } from '@/utils/localStorage' +import { Logo } from '@/components/common/Logo' +import { copyRecoveryKit, generateRecoveryPdf } from '@/utils/recovery' + +const bip39 = require('bip39') + +const errorToast = (message: string) => { + toast.error(message) +} + +const InvalidInvite = () => ( +
+
+

Something went wrong

+

+ This invite cannot be used by you. Please check that you are logged in to the correct + account, or contact the organisation owner to create a new invite. +

+
+
+) + +export default function Invite({ params }: { params: { invite: string } }) { + const [verifyInvite, { data, loading }] = useLazyQuery(VerifyInvite) + + const [acceptInvite] = useMutation(AcceptOrganisationInvite) + + const { data: session } = useSession() + + const invite: OrganisationMemberInviteType = data?.validateInvite + + const [showWelcome, setShowWelcome] = useState(true) + const [step, setStep] = useState(0) + + const [recoveryDownloaded, setRecoveryDownloaded] = useState(false) + const [success, setSuccess] = useState(false) + const [pw, setPw] = useState('') + const [pw2, setPw2] = useState('') + const [mnemonic, setMnemonic] = useState('') + const [isloading, setIsLoading] = useState(false) + + useEffect(() => { + const handleVerifyInvite = async () => { + const inviteId = await cryptoUtils.decodeb64string(params.invite) + + await verifyInvite({ + variables: { inviteId }, + }) + } + + if (params.invite) handleVerifyInvite() + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [params.invite]) + + const steps: Step[] = [ + { + index: 0, + name: 'Sudo Password', + icon: , + title: 'Set a sudo password', + description: + 'This will be used to encrypt your account keys. You will be need to enter this password to perform administrative tasks.', + }, + { + index: 1, + name: 'Account recovery', + icon: , + title: 'Account Recovery', + description: + 'If you forget your sudo password, you will need to use a recovery kit to regain access to your account.', + }, + ] + + const computeAccountKeys = () => { + return new Promise<{ publicKey: string; encryptedKeyring: string; encryptedMnemonic: string }>( + (resolve) => { + setTimeout(async () => { + const accountSeed = await cryptoUtils.organisationSeed(mnemonic, invite.organisation.id) + + const accountKeyRing = await cryptoUtils.organisationKeyring(accountSeed) + + const deviceKey = await cryptoUtils.deviceVaultKey(pw, session?.user?.email!) + + const encryptedKeyring = await cryptoUtils.encryptAccountKeyring( + accountKeyRing, + deviceKey + ) + + const encryptedMnemonic = await cryptoUtils.encryptAccountRecovery(mnemonic, deviceKey) + + resolve({ + publicKey: accountKeyRing.publicKey, + encryptedKeyring, + encryptedMnemonic, + }) + }, 1000) + } + ) + } + + const handleAccountInit = async () => { + return new Promise(async (resolve, reject) => { + setIsLoading(true) + const { publicKey, encryptedKeyring, encryptedMnemonic } = await computeAccountKeys() + + const { data } = await acceptInvite({ + variables: { + orgId: invite.organisation.id, + identityKey: publicKey, + wrappedKeyring: encryptedKeyring, + wrappedRecovery: encryptedMnemonic, + inviteId: invite.id, + }, + }) + + try { + setLocalKeyring({ + email: session?.user?.email!, + org: invite.organisation, + keyring: encryptedKeyring, + recovery: encryptedMnemonic, + }) + } catch (e) { + setIsLoading(false) + reject() + } + + setIsLoading(false) + if (data.createOrganisationMember.orgMember.id) { + setSuccess(true) + resolve(true) + } else { + reject() + } + }) + } + + const validateCurrentStep = () => { + if (step === 0) { + if (pw !== pw2) { + errorToast("Passwords don't match") + return false + } + } else if (step === 1 && !recoveryDownloaded) { + errorToast('Please download the your account recovery kit!') + return false + } + return true + } + + const incrementStep = async (event: { preventDefault: () => void }) => { + event.preventDefault() + + const isFormValid = validateCurrentStep() + if (step !== steps.length - 1 && isFormValid) setStep(step + 1) + if (step === steps.length - 1 && isFormValid) { + toast.promise(handleAccountInit, { + pending: 'Setting up your account', + success: 'Account setup complete!', + }) + } + } + + const decrementStep = () => { + if (step !== 0) setStep(step - 1) + } + + useEffect(() => { + setMnemonic(bip39.generateMnemonic(256)) + const id = crypto.randomUUID() + }, []) + + const WelcomePane = () => ( +
+
+
+ +
+ +

Welcome to Phase

+

+ You have been invited by{' '} + + {invite.invitedBy.email} + {' '} + to join the{' '} + + {invite.organisation.name} + {' '} + organisation. +

+
+ +
+ ) + + const SuccessPane = () => { + return ( +
+
+

You're All Set

+

Your account is ready to go!

+
+ +
+
+
+ ) + } + + const handleDownloadRecoveryKit = async () => { + toast + .promise( + generateRecoveryPdf( + mnemonic, + session?.user?.email!, + invite.organisation.name, + session?.user?.name || undefined + ), + { + pending: 'Generating recovery kit', + success: 'Downloaded recovery kit', + } + ) + .then(() => setRecoveryDownloaded(true)) + } + + const handleCopyRecoveryKit = () => { + copyRecoveryKit( + mnemonic, + session?.user?.email!, + invite.organisation.name, + session?.user?.name || undefined + ) + setRecoveryDownloaded(true) + } + + return ( + <> +
+ + +
+ {loading ? ( + + ) : invite ? ( + showWelcome ? ( + + ) : success ? ( + + ) : ( +
+
+ +
+ + {step === 0 && } + + {step === 1 && ( + + )} + +
+
+ {step !== 0 && ( + + )} +
+
+ +
+
+ + ) + ) : ( + + )} +
+
+ + ) +} diff --git a/frontend/app/layout.tsx b/frontend/app/layout.tsx index c8301948..2ecbad17 100644 --- a/frontend/app/layout.tsx +++ b/frontend/app/layout.tsx @@ -4,7 +4,7 @@ import '@/app/globals.css' import Providers from './providers' import { Inter } from '@next/font/google' import { ToastContainer } from 'react-toastify' -import 'react-toastify/scss/main.scss' +import 'react-toastify/dist/ReactToastify.min.css' import '@/utils/logoAnimation.css' const inter = Inter({ diff --git a/frontend/app/page.tsx b/frontend/app/page.tsx index 3f085b6a..5d80cb90 100644 --- a/frontend/app/page.tsx +++ b/frontend/app/page.tsx @@ -1,46 +1,106 @@ 'use client' -import { useQuery } from '@apollo/client' import { useRouter } from 'next/navigation' -import { useEffect } from 'react' +import { useContext, useEffect, useState } from 'react' import Loading from './loading' -import { GetOrganisations } from '@/apollo/queries/getOrganisations.gql' import { OrganisationType } from '@/apollo/graphql' -import { getLocalOrgs } from '@/utils/localStorage' +import { getLocalKeyrings } from '@/utils/localStorage' +import { organisationContext } from '@/contexts/organisationContext' +import { Button } from '@/components/common/Button' +import { FaArrowRight } from 'react-icons/fa' +import { useSession } from 'next-auth/react' +import { HeroPattern } from '@/components/common/HeroPattern' +import { Logo } from '@/components/common/Logo' +import { RoleLabel } from '@/components/users/RoleLabel' export default function Home() { - const { loading, error, data } = useQuery(GetOrganisations) - const router = useRouter() + const { data: session } = useSession() + + const { organisations, activeOrganisation, setActiveOrganisation, loading } = + useContext(organisationContext) + + const [showOrgCards, setShowOrgCards] = useState(false) + + const handleRouteToOrg = (org: OrganisationType) => { + const localOrgs = getLocalKeyrings() + + if ( + localOrgs?.find( + (localOrg) => localOrg.org.id === org!.id && localOrg.email === session?.user?.email + ) + ) { + router.push(`/${org!.name}`) + } else { + router.push(`${org!.name}/newdevice`) + } + } useEffect(() => { - if (data?.organisations) { - const orgs = data.organisations as OrganisationType[] + if (!loading && organisations !== null) { + const localOrgs = getLocalKeyrings() // if there is no org setup on the server, send to onboarding page - if (!orgs.length) router.push('/onboard') - else { - const defaultOrg = data.organisations[0].name - if (orgs.map((org) => org.name).includes(defaultOrg)) { - const localOrgs = getLocalOrgs() - // if org data exists on device - if (localOrgs?.find((localOrg) => localOrg.org.name === defaultOrg)) { - router.push(`/${defaultOrg}`) - } - // if no org data on device, send to new device login page - else { - router.push(`${orgs[0].name}/newdevice`) - } + if (organisations.length === 0) router.push('/signup') + else if (organisations.length === 1) { + const organisation = organisations[0] + setActiveOrganisation(organisation) + // if local keyring exists on device for active organisation + if ( + localOrgs?.find( + (localOrg) => + localOrg.org.id === organisation!.id && localOrg.email === session?.user?.email + ) + ) { + router.push(`/${organisation!.name}`) + } + // if no keyring on device, send to new device login page + else { + router.push(`${organisation!.name}/newdevice`) } + } else { + setShowOrgCards(true) } } - }, [data, router]) + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [organisations, router, loading]) - useEffect(() => { - if (error) { - throw error.message - } - }, [error]) + return ( +
+ {loading && } + {showOrgCards && ( + <> + - return
{loading && }
+
+
+
+ +
+

Choose a workspace

+
+
+ {organisations!.map((org: OrganisationType) => ( +
+

{org.name}

+
+ You are {org.role!.toLowerCase() === 'dev' ? 'a' : 'an'}{' '} + in this organisation +
+
+ +
+
+ ))} +
+
+ + )} +
+ ) } diff --git a/frontend/app/providers.tsx b/frontend/app/providers.tsx index 3e94646a..1b548335 100644 --- a/frontend/app/providers.tsx +++ b/frontend/app/providers.tsx @@ -4,12 +4,27 @@ import { ThemeProvider } from '@/contexts/themeContext' import { SessionProvider } from 'next-auth/react' import { ApolloProvider } from '@apollo/client' import { graphQlClient } from '@/apollo/client' +import { KeyringProvider } from '@/contexts/keyringContext' +import { OrganisationProvider } from '@/contexts/organisationContext' +import posthog from 'posthog-js' +import { PostHogProvider } from 'posthog-js/react' +import { initializePostHog } from '@/utils/posthog' + +const IS_CLOUD_HOSTED = process.env.APP_HOST || process.env.NEXT_PUBLIC_APP_HOST + +if (IS_CLOUD_HOSTED) initializePostHog() export default function Providers({ children }: { children: React.ReactNode }) { return ( - {children} + + + + {children} + + + ) diff --git a/frontend/app/onboard/head.tsx b/frontend/app/signup/head.tsx similarity index 100% rename from frontend/app/onboard/head.tsx rename to frontend/app/signup/head.tsx diff --git a/frontend/app/onboard/layout.tsx b/frontend/app/signup/layout.tsx similarity index 100% rename from frontend/app/onboard/layout.tsx rename to frontend/app/signup/layout.tsx diff --git a/frontend/app/onboard/page.tsx b/frontend/app/signup/page.tsx similarity index 61% rename from frontend/app/onboard/page.tsx rename to frontend/app/signup/page.tsx index f4444141..d028301b 100644 --- a/frontend/app/onboard/page.tsx +++ b/frontend/app/signup/page.tsx @@ -4,41 +4,34 @@ import { Button } from '@/components/common/Button' import { HeroPattern } from '@/components/common/HeroPattern' import { Step, Stepper } from '@/components/onboarding/Stepper' import { useEffect, useState } from 'react' -import { - MdOutlineVerifiedUser, - MdGroups, - MdOutlineKey, - MdKey, - MdOutlinePassword, -} from 'react-icons/md' +import { MdGroups, MdKey, MdOutlinePassword } from 'react-icons/md' import { TeamName } from '@/components/onboarding/TeamName' -import { AccountSeedGen } from '@/components/onboarding/AccountSeedGen' -import { AccountSeedChecker } from '@/components/onboarding/AccountSeedChecker' +import { AccountRecovery } from '@/components/onboarding/AccountRecovery' import { AccountPassword } from '@/components/onboarding/AccountPassword' import { cryptoUtils } from '@/utils/auth' import { useSession } from 'next-auth/react' import { toast } from 'react-toastify' -import { gql, useMutation } from '@apollo/client' +import { useMutation } from '@apollo/client' import { useRouter } from 'next/navigation' -import Link from 'next/link' -import { CreateOrganisation } from '@/apollo/mutations/createOrganisation.gql' -import { setLocalOrg } from '@/utils/localStorage' +import { CreateOrg } from '@/graphql/mutations/createOrganisation.gql' +import { setLocalKeyring } from '@/utils/localStorage' +import { copyRecoveryKit, generateRecoveryPdf } from '@/utils/recovery' const bip39 = require('bip39') const Onboard = () => { const { data: session } = useSession() - const [name, setName] = useState('') + const [teamName, setTeamName] = useState('') const [pw, setPw] = useState('') const [pw2, setPw2] = useState('') const [mnemonic, setMnemonic] = useState('') const [orgId, setOrgId] = useState('') const [inputs, setInputs] = useState>([]) const [step, setStep] = useState(0) - const [showWelcome, setShowWelcome] = useState(true) - const [createOrganisation, { data, loading, error }] = useMutation(CreateOrganisation) + + const [createOrganisation, { data, loading, error }] = useMutation(CreateOrg) const [isloading, setIsLoading] = useState(false) - const [seedDownloaded, setSeedDownloaded] = useState(false) + const [recoveryDownloaded, setRecoveryDownloaded] = useState(false) const [success, setSuccess] = useState(false) const router = useRouter() @@ -46,61 +39,55 @@ const Onboard = () => { toast.error(message) } - const handleInputUpdate = (newValue: string, index: number) => { - if (newValue.split(' ').length === 24) { - setInputs(newValue.split(' ')) - } else setInputs(inputs.map((input: string, i: number) => (index === i ? newValue : input))) - } - const steps: Step[] = [ { index: 0, - name: 'Team name', + name: 'Team Name', icon: , title: 'Choose a name for your team', - description: 'Your team name must be alphanumeric.', + description: ( +
+ Your team name can be alphanumeric. + +
[a-zA-Z0-9]
+
+
+ ), }, { index: 1, - name: 'Set up recovery phrase', - icon: , - title: 'Recovery', + name: 'Sudo Password', + icon: , + title: 'Set a sudo password', description: - "This is your 24 word recovery phrase. It's used to secure your application keys. Only you have access to it. Please write it down and store it somewhere safe like a password manager. You will need to enter your recovery phrase when logging in from a new device.", + 'This will be used to encrypt your account keys. You will be need to enter this password to perform administrative tasks.', }, { index: 2, - name: 'Verify recovery phrase', - icon: , - title: 'Verify recovery phrase', - description: 'Please enter the your recovery phrase in the correct order below.', - }, - { - index: 3, - name: 'Sudo password', - icon: , - title: 'Set a sudo password', + name: 'Account recovery', + icon: , + title: 'Account Recovery', description: - 'Please set up a strong sudo password to continue. This will be used to to perform administrative tasks and to encrypt keys locally on this device.', + 'If you forget your sudo password, you will need to use a recovery kit to regain access to your account.', }, ] const validateCurrentStep = () => { if (step === 0) { - if (!name) { + if (!teamName) { errorToast('Please enter a team name') //return false } - } else if (step === 2) { - if (inputs.join(' ') !== mnemonic && !seedDownloaded) { - errorToast('Incorrect account recovery key!') - return false // TODO: UNCOMMENT THIS!! - } - } else if (step === 3) { + } else if (step === 1) { if (pw !== pw2) { errorToast("Passwords don't match") return false } + } else if (step === 2) { + if (!recoveryDownloaded) { + errorToast('Please download the your account recovery kit!') + return false + } } return true } @@ -132,6 +119,28 @@ const Onboard = () => { ) } + const handleDownloadRecoveryKit = async () => { + toast + .promise( + generateRecoveryPdf( + mnemonic, + session?.user?.email!, + teamName, + session?.user?.name || undefined + ), + { + pending: 'Generating recovery kit', + success: 'Downloaded recovery kit', + } + ) + .then(() => setRecoveryDownloaded(true)) + } + + const handleCopyRecoveryKit = () => { + copyRecoveryKit(mnemonic, session?.user?.email!, teamName, session?.user?.name || undefined) + setRecoveryDownloaded(true) + } + const handleAccountInit = async () => { return new Promise(async (resolve, reject) => { setIsLoading(true) @@ -141,13 +150,15 @@ const Onboard = () => { const result = await createOrganisation({ variables: { id: orgId, - name, + name: teamName, identityKey: publicKey, + wrappedKeyring: encryptedKeyring, + wrappedRecovery: encryptedMnemonic, }, }) const { data } = result const newOrg = data.createOrganisation.organisation - setLocalOrg({ + setLocalKeyring({ email: session?.user?.email!, org: newOrg, keyring: encryptedKeyring, @@ -180,10 +191,6 @@ const Onboard = () => { if (step !== 0) setStep(step - 1) } - const skipSeedCheckerStep = () => { - if (seedDownloaded) setStep(3) - } - useEffect(() => { setMnemonic(bip39.generateMnemonic(256)) const id = crypto.randomUUID() @@ -200,34 +207,16 @@ const Onboard = () => { setInputs([...Array(mnemonic.split(' ').length)].map(() => '')) }, [mnemonic]) - const WelcomePane = () => { - return ( -
-

- Welcome to Phase -

-

- Setting up your account will take just a few minutes -

-
- -
-
- ) - } - const SuccessPane = () => { return (
-

Success!

-

Your account is setup!

+

You're All Set

+

Your account is ready to go!

@@ -241,27 +230,29 @@ const Onboard = () => {
- {showWelcome && } - {!showWelcome && !success && ( + {!success && (
+ {step >= 0 && ( +
+ Welcome to Phase +
+ )}
- {step === 0 && } - {step === 1 && } + {step === 0 && } + {step === 1 && } {step === 2 && ( - )} - {step === 3 && }
@@ -272,13 +263,13 @@ const Onboard = () => { )}
- {seedDownloaded && step === 2 && ( - - )} -
diff --git a/frontend/app/webauth/[requestCode]/page.tsx b/frontend/app/webauth/[requestCode]/page.tsx new file mode 100644 index 00000000..2d56e6ad --- /dev/null +++ b/frontend/app/webauth/[requestCode]/page.tsx @@ -0,0 +1,416 @@ +'use client' + +import { OrganisationType } from '@/apollo/graphql' +import { Button } from '@/components/common/Button' +import { HeroPattern } from '@/components/common/HeroPattern' +import Spinner from '@/components/common/Spinner' +import OnboardingNavbar from '@/components/layout/OnboardingNavbar' +import { RoleLabel } from '@/components/users/RoleLabel' +import { organisationContext } from '@/contexts/organisationContext' +import { CreateNewUserToken } from '@/graphql/mutations/users/createUserToken.gql' +import { OrganisationKeyring, cryptoUtils } from '@/utils/auth' +import { copyToClipBoard } from '@/utils/clipboard' +import { getUserKxPublicKey, getUserKxPrivateKey, encryptAsymmetric } from '@/utils/crypto' +import { generateUserToken } from '@/utils/environments' +import { useMutation } from '@apollo/client' +import { Disclosure, Transition } from '@headlessui/react' +import axios from 'axios' +import clsx from 'clsx' +import { useSession } from 'next-auth/react' +import { useRouter } from 'next/navigation' +import { useContext, useEffect, useState } from 'react' +import { + FaEyeSlash, + FaEye, + FaChevronRight, + FaExclamationTriangle, + FaCheckCircle, +} from 'react-icons/fa' +import { MdContentCopy } from 'react-icons/md' +import { SiGithub, SiGnometerminal, SiSlack } from 'react-icons/si' +import { toast } from 'react-toastify' + +interface WebAuthRequestParams { + port: number + publicKey: string + requestedTokenName: string +} + +const handleCopy = (val: string) => { + copyToClipBoard(val) + toast.info('Copied', { + autoClose: 2000, + }) +} + +const getWebAuthRequestParams = (hash: string): WebAuthRequestParams => { + const delimiter = '-' + const params = hash.split(delimiter) + + return { + port: Number(params[0]), + publicKey: params[1], + requestedTokenName: params[2], + } +} + +export default function WebAuth({ params }: { params: { requestCode: string } }) { + const router = useRouter() + const { organisations } = useContext(organisationContext) + const [status, setStatus] = useState< + 'validating' | 'in progress' | 'success' | 'error' | 'invalid' + >('validating') + const [userToken, setUserToken] = useState('') + + const [createUserToken] = useMutation(CreateNewUserToken) + + const [requestParams, setRequestParams] = useState(null) + + const { data: session } = useSession() + + const handleCreatePat = (name: string, organisationId: string, keyring: OrganisationKeyring) => { + return new Promise(async (resolve, reject) => { + if (keyring) { + const userKxKeys = { + publicKey: await getUserKxPublicKey(keyring.publicKey), + privateKey: await getUserKxPrivateKey(keyring.privateKey), + } + + const { pssUser, mutationPayload } = await generateUserToken( + organisationId, + userKxKeys, + name, + null + ) + + const { data } = await createUserToken({ + variables: mutationPayload, + }) + + if (data) resolve(pssUser) + else reject('Failed to create user token') + } else { + reject('Keyring is locked') + } + }) + } + + const validateKeyring = async (password: string, organisation: OrganisationType) => { + return new Promise(async (resolve) => { + const decryptedKeyring = await cryptoUtils.getKeyring( + session?.user?.email!, + organisation!.id, + password + ) + + resolve(decryptedKeyring) + }) + } + + const authenticate = async (organisation: OrganisationType, password: string) => { + if (!requestParams) { + toast.error('Invalid webauth request') + setStatus('error') + return false + } + + try { + const keyring = await validateKeyring(password, organisation) + + const pssUser = await handleCreatePat( + requestParams.requestedTokenName, + organisation.id, + keyring + ) + setUserToken(pssUser) + + const encryptedUserToken = await encryptAsymmetric(pssUser, requestParams.publicKey) + const encryptedEmail = await encryptAsymmetric(session?.user?.email!, requestParams.publicKey) + + const cliResponse = await axios.post(`http://127.0.0.1:${requestParams.port}`, { + email: encryptedEmail, + pss: encryptedUserToken, + }) + + if (cliResponse.status === 200) { + toast.success('CLI authentication complete') + setStatus('success') + } else { + toast.error('Something went wrong.') + setStatus('error') + } + } catch (error) { + setStatus('error') + } + } + + useEffect(() => { + const validateWebAuthRequest = async () => { + const decodedWebAuthReq = await cryptoUtils.decodeb64string( + decodeURIComponent(params.requestCode) + ) + const authRequestParams = getWebAuthRequestParams(decodedWebAuthReq) + + if (!authRequestParams.publicKey || !authRequestParams.requestedTokenName) + setStatus('invalid') + else { + setStatus('in progress') + setRequestParams(authRequestParams) + } + } + + validateWebAuthRequest() + }, [params.requestCode]) + + useEffect(() => { + if (organisations?.length === 0) router.push('/signup') + }, [organisations, router]) + + const OrganisationSelectPanel = (props: { + organisation: OrganisationType + defaultOpen: boolean + }) => { + const { organisation, defaultOpen } = props + + const [password, setPassword] = useState('') + const [showPw, setShowPw] = useState(false) + + const handleSubmit = async (e: { preventDefault: () => void }) => { + e.preventDefault() + await authenticate(organisation, password) + } + + return ( + + {({ open }) => ( + <> + +
+
+

+ {organisation.name} +

+ + + +
+ +
+
+ + + + +
+ +
+ setPassword(e.target.value)} + type={showPw ? 'text' : 'password'} + minLength={16} + required + autoFocus + className="custom w-full text-zinc-800 font-mono dark:text-white bg-zinc-100 dark:bg-zinc-800 rounded-md ph-no-capture" + /> + +
+
+ + +
+
+ + )} +
+ ) + } + + return ( +
+ + + + {status == 'validating' && ( +
+ +
+ )} + + {status == 'in progress' && ( +
+
+
+ +
+

+ CLI Authentication +

+

+ Choose an account below to authenticate with the Phase CLI +

+
+
+ {organisations?.map((organisation, index) => ( + + ))} +
+
+ )} + + {status === 'success' && ( +
+
+ +
+

+ CLI Authentication complete +

+

+ You have logged into the Phase CLI as{' '} + {session?.user?.email}.
You can head + back to your terminal and close this screen now +

+
+ )} + + {status === 'error' && ( +
+
+
+ +
+

+ CLI Authentication error +

+

+ Something went wrong authenticating with the CLI. Please try the following steps: +

+
+ +
    +
  1. + Retry authentication with{' '} + handleCopy('phase auth --mode token')} + > + phase auth --mode token + + . +
  2. +
  3. Paste the following token into your terminal when prompted:
  4. +
+
+
+
+ user token +
+ {userToken && ( + + )} +
+
+ {userToken} +
+
+ +
+
Still having issues? Get in touch.
+ +
+
+ )} + + {status === 'invalid' && ( +
+
+
+ +
+

+ CLI Authentication error +

+

+ This authentication link is invalid. Please try again. +

+
+ +
+
Still having issues? Get in touch.
+ +
+
+ )} +
+ ) +} diff --git a/frontend/codegen.ts b/frontend/codegen.ts index ee8c981f..26aee833 100644 --- a/frontend/codegen.ts +++ b/frontend/codegen.ts @@ -3,11 +3,11 @@ import type { CodegenConfig } from '@graphql-codegen/cli' const config: CodegenConfig = { overwrite: true, schema: 'apollo/schema.graphql', - //documents: 'apollo/**/*.graphql', + documents: ['graphql/**/*.gql'], generates: { 'apollo/': { preset: 'client', - plugins: ['typescript'], + plugins: [], }, }, } diff --git a/frontend/components/UserMenu.tsx b/frontend/components/UserMenu.tsx index 85093bda..88f53041 100644 --- a/frontend/components/UserMenu.tsx +++ b/frontend/components/UserMenu.tsx @@ -7,6 +7,7 @@ import { useSession, signIn, signOut } from 'next-auth/react' import { MdLogout } from 'react-icons/md' import { handleSignout } from '@/apollo/client' import { Button } from './common/Button' +import { Avatar } from './common/Avatar' export default function UserMenu() { const { data: session } = useSession() @@ -18,10 +19,7 @@ export default function UserMenu() { @@ -34,7 +32,7 @@ export default function UserMenu() { leaveFrom="transform opacity-100 scale-100" leaveTo="transform opacity-0 scale-95" > - +
{({ active }) => ( diff --git a/frontend/components/apps/AppActivityChart.tsx b/frontend/components/apps/AppActivityChart.tsx index d5de9d20..81845680 100644 --- a/frontend/components/apps/AppActivityChart.tsx +++ b/frontend/components/apps/AppActivityChart.tsx @@ -1,5 +1,5 @@ import { AppType, ChartDataPointType, TimeRange } from '@/apollo/graphql' -import { GetAppActivityChart } from '@/apollo/queries/getAppActivityChart.gql' +import { GetAppActivityChart } from '@/graphql/queries/getAppActivityChart.gql' import { humanReadableNumber } from '@/utils/dataUnits' import { useLazyQuery } from '@apollo/client' import { useEffect, useState } from 'react' diff --git a/frontend/components/apps/AppCard.tsx b/frontend/components/apps/AppCard.tsx index 2c1c3908..6f137b09 100644 --- a/frontend/components/apps/AppCard.tsx +++ b/frontend/components/apps/AppCard.tsx @@ -1,104 +1,56 @@ import { useQuery } from '@apollo/client' -import { FaCube } from 'react-icons/fa' +import { FaBox, FaBoxes, FaCube, FaUser, FaUsers } from 'react-icons/fa' import { Card } from '../common/Card' -import Spinner from '../common/Spinner' -import { GetAppActivityChart } from '@/apollo/queries/getAppActivityChart.gql' -import { AppType, ChartDataPointType, TimeRange } from '@/apollo/graphql' -import { - Area, - AreaSeries, - AreaSparklineChart, - Gradient, - GradientStop, - Line, - Stripes, - TooltipArea, -} from 'reaviz' +import GetAppMembers from '@/graphql/queries/apps/getAppMembers.gql' +import GetAppEnvironments from '@/graphql/queries/secrets/getAppEnvironments.gql' +import { AppType } from '@/apollo/graphql' interface AppCardProps { app: AppType } export const AppCard = (props: AppCardProps) => { - const { name, id } = props.app - - const { data } = useQuery(GetAppActivityChart, { + const { name, id: appId } = props.app + const { data: appMembersData } = useQuery(GetAppMembers, { variables: { appId } }) + const { data: appEnvsData } = useQuery(GetAppEnvironments, { variables: { - appId: id, - period: TimeRange.Day, + appId, }, - fetchPolicy: 'cache-and-network', }) - const chartData = - data?.appActivityChart.map((dataPoint: ChartDataPointType) => { - const timestamp = new Date(dataPoint.date!) - return { - id: dataPoint.index!.toString(), - key: timestamp, - data: dataPoint.data, - } - }) || [] - - const chartIsLoading = chartData.length === 0 - return (
-
-
+
+
{name}
-
{id}
+
{appId}
-
- {!chartIsLoading && ( - } - markLine={null} - interpolation="smooth" - area={ - } - gradient={ - , - , - ]} - /> - } - /> - } - line={} - /> - } - /> - )} -
+
+
+
+ {appMembersData?.appUsers.length > 1 ? : } + {appMembersData?.appUsers.length} +
+ + {appMembersData?.appUsers.length > 1 ? 'Members' : 'Member'} + +
-
-
- {chartIsLoading && } - {data && ( -
- app is live - -
- )} +
+
+ {appEnvsData?.appEnvironments.length > 1 ? : } + {appEnvsData?.appEnvironments.length} +
+ + {appEnvsData?.appEnvironments.length > 1 ? 'Environments' : 'Environment'} +
diff --git a/frontend/components/apps/AppsHomeCard.tsx b/frontend/components/apps/AppsHomeCard.tsx index cb19989b..04cafeb3 100644 --- a/frontend/components/apps/AppsHomeCard.tsx +++ b/frontend/components/apps/AppsHomeCard.tsx @@ -1,7 +1,7 @@ 'use client' import { useQuery } from '@apollo/client' -import { GetApps } from '@/apollo/queries/getApps.gql' +import { GetApps } from '@/graphql/queries/getApps.gql' import { AppType } from '@/apollo/graphql' import Spinner from '../common/Spinner' import { FaCubes } from 'react-icons/fa' diff --git a/frontend/components/apps/DeleteAppDialog.tsx b/frontend/components/apps/DeleteAppDialog.tsx index 2ac74f64..e6c6c5b7 100644 --- a/frontend/components/apps/DeleteAppDialog.tsx +++ b/frontend/components/apps/DeleteAppDialog.tsx @@ -6,10 +6,11 @@ import { Fragment, useState } from 'react' import { FaTrash, FaTimes, FaExclamationTriangle } from 'react-icons/fa' import { toast } from 'react-toastify' import { Button } from '../common/Button' -import { DeleteApp } from '@/apollo/mutations/deleteApp.gql' -import { GetApps } from '@/apollo/queries/getApps.gql' +import { DeleteApplication } from '@/graphql/mutations/deleteApp.gql' +import { GetApps } from '@/graphql/queries/getApps.gql' import { useMutation } from '@apollo/client' import { useRouter } from 'next/navigation' +import { Alert } from '../common/Alert' export default function DeleteAppDialog(props: { organisationId: string @@ -20,7 +21,7 @@ export default function DeleteAppDialog(props: { const { organisationId, appId, appName, teamName } = props const [isOpen, setIsOpen] = useState(false) const [typedName, setTypedName] = useState('') - const [deleteApp, { loading }] = useMutation(DeleteApp) + const [deleteApp, { loading }] = useMutation(DeleteApplication) const router = useRouter() const reset = () => { @@ -126,14 +127,21 @@ export default function DeleteAppDialog(props: {

Permanently delete this App

-
- -
- Warning: This is permanent!
- Once you delete this App, you will not be able to decrypt any data that - was encrypted with these keys. + +
+

Warning: This is permanent!

+ +

+ Deleting this App will permanently delete all environments and secrets + associated with it. +

+ +

+ Once you delete this App, you will not be able to decrypt any data that + was encrypted with this App's KMS keys. +

-
+
diff --git a/frontend/components/apps/NewAppDialog.tsx b/frontend/components/apps/NewAppDialog.tsx index 64c087dd..649afe62 100644 --- a/frontend/components/apps/NewAppDialog.tsx +++ b/frontend/components/apps/NewAppDialog.tsx @@ -1,24 +1,40 @@ -import { cryptoUtils } from '@/utils/auth' +import { OrganisationKeyring, cryptoUtils } from '@/utils/auth' import { copyToClipBoard } from '@/utils/clipboard' -import { getLocalKeyring } from '@/utils/localStorage' -import { Dialog, Transition } from '@headlessui/react' +import { Dialog, Switch, Transition } from '@headlessui/react' import { useSession } from 'next-auth/react' -import { Fragment, ReactNode, useEffect, useState } from 'react' -import { FaCopy, FaCross, FaExclamationTriangle, FaEye, FaEyeSlash, FaTimes } from 'react-icons/fa' +import { Fragment, ReactNode, useContext, useEffect, useState } from 'react' +import { FaCopy, FaExclamationTriangle, FaEye, FaEyeSlash, FaTimes } from 'react-icons/fa' import { toast } from 'react-toastify' import { Button } from '../common/Button' -import { GetApps } from '@/apollo/queries/getApps.gql' -import { CreateApp } from '@/apollo/mutations/createApp.gql' -import { useMutation } from '@apollo/client' +import { GetApps } from '@/graphql/queries/getApps.gql' +import { CreateApplication } from '@/graphql/mutations/createApp.gql' +import { CreateNewSecret } from '@/graphql/mutations/environments/createSecret.gql' +import { GetOrganisationAdminsAndSelf } from '@/graphql/queries/organisation/getOrganisationAdminsAndSelf.gql' +import { InitAppEnvironments } from '@/graphql/mutations/environments/initAppEnvironments.gql' +import { GetAppEnvironments } from '@/graphql/queries/secrets/getAppEnvironments.gql' +import { useLazyQuery, useMutation } from '@apollo/client' import { + ApiEnvironmentEnvTypeChoices, ApiOrganisationPlanChoices, + EnvironmentType, MutationCreateAppArgs, OrganisationType, + SecretInput, + SecretType, } from '@/apollo/graphql' import { splitSecret } from '@/utils/keyshares' import { UpgradeRequestForm } from '../forms/UpgradeRequestForm' +import { KeyringContext } from '@/contexts/keyringContext' +import { createNewEnv } from '@/utils/environments' +import { + decryptAsymmetric, + digest, + encryptAsymmetric, + getUserKxPrivateKey, + getUserKxPublicKey, +} from '@/utils/crypto' -const FREE_APP_LIMIT = 1 +const FREE_APP_LIMIT = 3 const PRO_APP_LIMIT = 10 export default function NewAppDialog(props: { @@ -33,9 +49,16 @@ export default function NewAppDialog(props: { const [pw, setPw] = useState('') const [showPw, setShowPw] = useState(false) const [appId, setAppId] = useState('') + const [createStarters, setCreateStarters] = useState(appCount === 0) const [appSecret, setAppSecret] = useState('') const { data: session } = useSession() - const [createApp, { data, loading, error }] = useMutation(CreateApp) + + const [createApp] = useMutation(CreateApplication) + const [initAppEnvironments] = useMutation(InitAppEnvironments) + const [createSecret] = useMutation(CreateNewSecret) + const [getAppEnvs] = useLazyQuery(GetAppEnvironments) + + const [getOrgAdmins, { data: orgAdminsData }] = useLazyQuery(GetOrganisationAdminsAndSelf) const IS_CLOUD_HOSTED = process.env.APP_HOST || process.env.NEXT_PUBLIC_APP_HOST @@ -44,6 +67,18 @@ export default function NewAppDialog(props: { variant: 'primary', } + const { keyring, setKeyring } = useContext(KeyringContext) + + useEffect(() => { + if (organisation) { + getOrgAdmins({ + variables: { + organisationId: organisation.id, + }, + }) + } + }, [getOrgAdmins, organisation]) + const complete = () => appId && appSecret const reset = () => { @@ -67,6 +102,234 @@ export default function NewAppDialog(props: { toast.info('Copied') } + const validateKeyring = async (password: string) => { + return new Promise(async (resolve, reject) => { + if (keyring) resolve(keyring) + else { + try { + const decryptedKeyring = await cryptoUtils.getKeyring( + session?.user?.email!, + organisation!.id, + password + ) + setKeyring(decryptedKeyring) + resolve(decryptedKeyring) + } catch (error) { + reject(error) + } + } + }) + } + + /** + * Encrypts a set of secrets for the given env and creates them server-side + * + * @param {EnvironmentType} env - The environment in which the secrets will be created. + * @param {Array>} secrets - An array of secrets to be processed. + * @returns {Promise} A Promise that resolves when the all secrets are encrypted and stored on the server. + * + * @throws {Error} If the specified environment is invalid or if an error occurs during processing. + */ + async function processSecrets(env: EnvironmentType, secrets: Array>) { + const keyring = await validateKeyring(pw) + + const userKxKeys = { + publicKey: await getUserKxPublicKey(keyring.publicKey), + privateKey: await getUserKxPrivateKey(keyring.privateKey), + } + + const envSalt = await decryptAsymmetric( + env.wrappedSalt, + userKxKeys.privateKey, + userKxKeys.publicKey + ) + + const promises = secrets.map(async (secret) => { + const { key, value, comment } = secret + + const encryptedKey = await encryptAsymmetric(key!, env.identityKey) + const encryptedValue = await encryptAsymmetric(value!, env.identityKey) + const keyDigest = await digest(key!, envSalt) + const encryptedComment = await encryptAsymmetric(comment!, env.identityKey) + + await createSecret({ + variables: { + newSecret: { + envId: env.id, + key: encryptedKey, + keyDigest, + value: encryptedValue, + folderId: null, + comment: encryptedComment, + tags: [], + } as SecretInput, + }, + }) + }) + + return Promise.all(promises) + } + + /** + * Handles the creation of example secrets for a given app. Defines the set of example secrets, fetches all envs for this app and handles creation of each set of secrets with the respective envs + * + * @param {string} appId + * @returns {Promise} + */ + const createExampleSecrets = async (appId: string) => { + const DEV_SECRETS = [ + { + key: 'AWS_ACCESS_KEY_ID', + value: 'AKIAIX4ONRSG6ODEFVJA', + comment: 'This is an example secret.', + }, + { + key: 'AWS_SECRET_ACCESS_KEY', + value: 'aCRAMarEbFC3Q5c24pi7AVMIt6TaCfHeFZ4KCf/a', + comment: 'This is an example secret.', + }, + { + key: 'JWT_SECRET', + value: + 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJyb2xlIjoic2VydmljZV9yb2xlIiwiaWF0IjoxNjMzNjIwMTcxLCJleHAiOjIyMDg5ODUyMDB9.pHnckabbMbwTHAJOkb5Z7G7B4chY6GllJf6K2m96z3A', + comment: 'This is an example secret.', + }, + { + key: 'STRIPE_SECRET_KEY', + value: 'sk_test_EeHnL644i6zo4Iyq4v1KdV9H', + comment: 'This is an example secret.', + }, + { + key: 'DJANGO_SECRET_KEY', + value: 'wwf*2#86t64!fgh6yav$aoeuo@u2o@fy&*gg76q!&%6x_wbduad', + comment: 'This is an example secret.', + }, + { + key: 'DJANGO_DEBUG', + value: 'True', + comment: 'This is an example secret.', + }, + { + key: 'POSTGRES_CONNECTION_STRING', + value: 'postgresql://${DB_USER}:${DB_PASSWORD}@${DB_HOST}:${DB_PORT}/${DB_NAME}', + comment: 'This is an example secret.', + }, + { + key: 'DB_USER', + value: 'postgres', + comment: 'This is an example secret.', + }, + { + key: 'DB_HOST', + value: 'mc-laren-prod-db.c9ufzjtplsaq.us-west-1.rds.amazonaws.com', + comment: 'This is an example secret.', + }, + { + key: 'DB_NAME', + value: 'XP1_LM', + comment: 'This is an example secret.', + }, + { + key: 'DB_PASSWORD', + value: '6c37810ec6e74ec3228416d2844564fceb99ebd94b29f4334c244db011630b0e', + comment: 'This is an example secret.', + }, + { + key: 'DB_PORT', + value: '5432', + comment: 'This is an example secret.', + }, + ] + + const STAG_SECRETS = [ + { + key: 'DJANGO_DEBUG', + value: 'False', + comment: 'This is an example secret.', + }, + ] + + const PROD_SECRETS = [ + { + key: 'STRIPE_SECRET_KEY', + value: 'sk_live_epISNGSkdeXov2frTey7RHAi', + comment: 'This is an example secret.', + }, + { + key: 'DJANGO_DEBUG', + value: 'False', + comment: 'This is an example secret.', + }, + ] + + const { data: appEnvsData } = await getAppEnvs({ variables: { appId } }) + + await processSecrets( + appEnvsData.appEnvironments.find( + (env: EnvironmentType) => env.envType === ApiEnvironmentEnvTypeChoices.Dev + ), + DEV_SECRETS + ) + await processSecrets( + appEnvsData.appEnvironments.find( + (env: EnvironmentType) => env.envType === ApiEnvironmentEnvTypeChoices.Staging + ), + STAG_SECRETS + ) + await processSecrets( + appEnvsData.appEnvironments.find( + (env: EnvironmentType) => env.envType === ApiEnvironmentEnvTypeChoices.Prod + ), + PROD_SECRETS + ) + } + + /** + * Initialize application environments for a given application ID. + * + * @param {string} appId - The ID of the application for which environments will be initialized. + * @returns {Promise} A Promise that resolves to `true` when initialization is complete. + * + * @throws {Error} If there are any errors during the environment initialization process. + */ + const initAppEnvs = async (appId: string) => { + return new Promise(async (resolve, reject) => { + const mutationPayload = { + devEnv: await createNewEnv( + appId, + 'Development', + ApiEnvironmentEnvTypeChoices.Dev, + orgAdminsData.organisationAdminsAndSelf + ), + stagingEnv: await createNewEnv( + appId, + 'Staging', + ApiEnvironmentEnvTypeChoices.Staging, + orgAdminsData.organisationAdminsAndSelf + ), + prodEnv: await createNewEnv( + appId, + 'Production', + ApiEnvironmentEnvTypeChoices.Prod, + orgAdminsData.organisationAdminsAndSelf + ), + } + + await initAppEnvironments({ + variables: { + devEnv: mutationPayload.devEnv.createEnvPayload, + stagingEnv: mutationPayload.stagingEnv.createEnvPayload, + prodEnv: mutationPayload.prodEnv.createEnvPayload, + devAdminKeys: mutationPayload.devEnv.adminKeysPayload, + stagAdminKeys: mutationPayload.stagingEnv.adminKeysPayload, + prodAdminKeys: mutationPayload.prodEnv.adminKeysPayload, + }, + }) + + resolve(true) + }) + } + const handleCreateApp = async () => { const APP_VERSION = 1 @@ -78,24 +341,14 @@ export default function NewAppDialog(props: { const id = crypto.randomUUID() try { - const deviceKey = await cryptoUtils.deviceVaultKey(pw, session?.user?.email!) - const encryptedKeyring = getLocalKeyring(organisation.id) - if (!encryptedKeyring) throw 'Error fetching local encrypted keys from browser' - const decryptedKeyring = await cryptoUtils.decryptAccountKeyring( - encryptedKeyring!, - deviceKey - ) - if (!decryptedKeyring) throw 'Failed to decrypt keys' - const encryptedAppSeed = await cryptoUtils.encryptedAppSeed( - appSeed, - decryptedKeyring.symmetricKey - ) + const keyring = await validateKeyring(pw) + const encryptedAppSeed = await cryptoUtils.encryptedAppSeed(appSeed, keyring.symmetricKey) const appKeys = await cryptoUtils.appKeyring(appSeed) const appKeyShares = await splitSecret(appKeys.privateKey) const wrappedShare = await cryptoUtils.wrappedKeyShare(appKeyShares[1], wrapKey) - await createApp({ + const { data } = await createApp({ variables: { id, name, @@ -117,10 +370,19 @@ export default function NewAppDialog(props: { ], }) + const newAppId = data.createApp.app.id + + await initAppEnvs(newAppId) + + if (createStarters) { + await createExampleSecrets(newAppId) + } + setAppSecret(`pss:v${APP_VERSION}:${appToken}:${appKeyShares[0]}:${wrapKey}`) setAppId(`phApp:v${APP_VERSION}:${appKeys.publicKey}`) resolve(true) + closeModal() } catch (error) { reject(error) } @@ -133,9 +395,7 @@ export default function NewAppDialog(props: { toast.promise(handleCreateApp, { pending: 'Setting up your app', success: 'App created!', - error: error?.message - ? undefined - : 'Something went wrong! Please check your password and try again.', + error: 'Something went wrong! Please check your sudo password and try again.', }) } @@ -152,14 +412,13 @@ export default function NewAppDialog(props: { return { planName: 'Free', dialogTitle: 'Upgrade to Pro', - description: - 'The Free plan is limited to a single application. To create more applications please upgrade to Pro.', + description: `The Free plan is limited to ${FREE_APP_LIMIT} Apps. To create more Apps, please upgrade to Pro.`, } else if (organisation.plan === ApiOrganisationPlanChoices.Pr) return { planName: 'Pro', dialogTitle: 'Upgrade to Enterprise', - description: `The Pro plan is limited to ${PRO_APP_LIMIT} applications. To create more applications please upgrade to Enterprise.`, + description: `The Pro plan is limited to ${PRO_APP_LIMIT} Apps. To create more Apps, please upgrade to Enterprise.`, } } @@ -212,12 +471,12 @@ export default function NewAppDialog(props: { {!complete() && allowNewApp() && (
-
+

Create a new app by entering an app name below. A new set of encryption keys will be created to secure your app.

-
+
-
+ {!keyring && ( +
+ +
+ setPw(e.target.value)} + type={showPw ? 'text' : 'password'} + minLength={16} + required + className="w-full ph-no-capture" + /> + +
+
+ )} + +
-
- setPw(e.target.value)} - type={showPw ? 'text' : 'password'} - minLength={16} - required - className="w-full " - /> - -
+ setCreateStarters(!createStarters)} + className={`${ + createStarters + ? 'bg-emerald-400/10 ring-emerald-400/20' + : 'bg-neutral-500/40 ring-neutral-500/30' + } relative inline-flex h-6 w-11 items-center rounded-full ring-1 ring-inset`} + > + Initialize with example secrets + +
diff --git a/frontend/components/apps/tokens/SecretTokens.tsx b/frontend/components/apps/tokens/SecretTokens.tsx new file mode 100644 index 00000000..b25f8faf --- /dev/null +++ b/frontend/components/apps/tokens/SecretTokens.tsx @@ -0,0 +1,629 @@ +import { RevokeServiceToken } from '@/graphql/mutations/environments/deleteServiceToken.gql' +import { CreateNewServiceToken } from '@/graphql/mutations/environments/createServiceToken.gql' +import { GetServiceTokens } from '@/graphql/queries/secrets/getServiceTokens.gql' +import { GetEnvironmentKey } from '@/graphql/queries/secrets/getEnvironmentKey.gql' +import { GetAppEnvironments } from '@/graphql/queries/secrets/getAppEnvironments.gql' +import { + newEnvToken, + newEnvWrapKey, + newServiceTokenKeys, + unwrapEnvSecretsForUser, + wrapEnvSecretsForServiceToken, +} from '@/utils/environments' +import { EnvironmentType, ServiceTokenType, UserTokenType } from '@/apollo/graphql' +import { cryptoUtils } from '@/utils/auth' +import { splitSecret } from '@/utils/keyshares' +import { useLazyQuery, useMutation, useQuery } from '@apollo/client' +import { useState, useEffect, useContext, Fragment } from 'react' +import { KeyringContext } from '@/contexts/keyringContext' +import { Button } from '@/components/common/Button' +import { + FaCheckSquare, + FaChevronDown, + FaCircle, + FaDotCircle, + FaExclamationTriangle, + FaKey, + FaPlus, + FaSquare, + FaTimes, + FaTrashAlt, +} from 'react-icons/fa' +import { getUnixTimeStampinFuture, relativeTimeFromDates } from '@/utils/time' +import { Dialog, Listbox, RadioGroup, Transition } from '@headlessui/react' +import { copyToClipBoard } from '@/utils/clipboard' +import { MdContentCopy } from 'react-icons/md' +import { toast } from 'react-toastify' +import clsx from 'clsx' +import { organisationContext } from '@/contexts/organisationContext' +import { userIsAdmin } from '@/utils/permissions' +import { Avatar } from '@/components/common/Avatar' + +interface ExpiryOptionT { + name: string + getExpiry: () => number | null +} + +const handleCopy = (val: string) => { + copyToClipBoard(val) + toast.info('Copied', { + autoClose: 2000, + }) +} + +const tokenExpiryOptions: ExpiryOptionT[] = [ + { + name: 'Never', + getExpiry: () => null, + }, + { + name: '7 days', + getExpiry: () => getUnixTimeStampinFuture(7), + }, + { + name: '30 days', + getExpiry: () => getUnixTimeStampinFuture(30), + }, + { + name: '60 days', + getExpiry: () => getUnixTimeStampinFuture(60), + }, + { + name: '90 days', + getExpiry: () => getUnixTimeStampinFuture(90), + }, +] + +const compareExpiryOptions = (a: ExpiryOptionT, b: ExpiryOptionT) => { + return a.getExpiry() === b.getExpiry() +} + +const humanReadableExpiry = (expiryOption: ExpiryOptionT) => + expiryOption.getExpiry() === null + ? 'This token will never expire.' + : `This token will expire on ${new Date(expiryOption.getExpiry()!).toLocaleDateString()}.` + +const CreateServiceTokenDialog = (props: { organisationId: string; appId: string }) => { + const { organisationId, appId } = props + + const { keyring } = useContext(KeyringContext) + + const [isOpen, setIsOpen] = useState(false) + const [name, setName] = useState('') + const [envScope, setEnvScope] = useState>>([]) + const [expiry, setExpiry] = useState(tokenExpiryOptions[0]) + const [showEnvHint, setShowEnvHint] = useState(false) + + const [serviceToken, setServiceToken] = useState('') + + const { data } = useQuery(GetAppEnvironments, { + variables: { + appId, + }, + }) + const [getEnvKey] = useLazyQuery(GetEnvironmentKey) + const [createServiceToken] = useMutation(CreateNewServiceToken) + + const reset = () => { + setName('') + setEnvScope([]) + setServiceToken('') + setShowEnvHint(false) + } + + const closeModal = () => { + reset() + setIsOpen(false) + } + + const openModal = () => { + setIsOpen(true) + } + + const envOptions = + data?.appEnvironments.map((env: EnvironmentType) => { + const { id, name } = env + + return { + id, + name, + } + }) ?? [] + + const handleCreateNewServiceToken = async (event: { preventDefault: () => void }) => { + event.preventDefault() + + if (envScope.length === 0) { + setShowEnvHint(true) + return false + } + + if (keyring) { + const appEnvironments = data.appEnvironments as EnvironmentType[] + + const token = await newEnvToken() + const wrapKey = await newEnvWrapKey() + + const tokenKeys = await newServiceTokenKeys() + const keyShares = await splitSecret(tokenKeys.privateKey) + const wrappedKeyShare = await cryptoUtils.wrappedKeyShare(keyShares[1], wrapKey) + + const pssService = `pss_service:v1:${token}:${tokenKeys.publicKey}:${keyShares[0]}:${wrapKey}` + + const envKeyPromises = appEnvironments + .filter((env) => envScope.map((selectedEnv) => selectedEnv.id).includes(env.id)) + .map(async (env: EnvironmentType) => { + const { data } = await getEnvKey({ + variables: { + envId: env.id, + appId, + }, + }) + + const { + wrappedSeed: userWrappedSeed, + wrappedSalt: userWrappedSalt, + identityKey, + } = data.environmentKeys[0] + + const { seed, salt } = await unwrapEnvSecretsForUser( + userWrappedSeed, + userWrappedSalt, + keyring! + ) + + const { wrappedSeed, wrappedSalt } = await wrapEnvSecretsForServiceToken( + { seed, salt }, + tokenKeys.publicKey + ) + + return { + envId: env.id, + identityKey, + wrappedSeed, + wrappedSalt, + } + }) + + const envKeyInputs = await Promise.all(envKeyPromises) + + await createServiceToken({ + variables: { + appId, + environmentKeys: envKeyInputs, + identityKey: tokenKeys.publicKey, + token, + wrappedKeyShare, + name, + expiry: expiry.getExpiry(), + }, + refetchQueries: [ + { + query: GetServiceTokens, + variables: { + appId, + }, + }, + ], + }) + + setServiceToken(pssService) + } + } + + return ( + <> +
+ +
+ + + {}}> + +
+ + +
+
+ + + +

+ Create a new Service token +

+ + +
+ + {serviceToken ? ( +
+
+
+ + service token + +
+ {serviceToken && ( +
+ +
+ {"Copy this value. You won't see it again!"} +
+
+ )} + {serviceToken && ( + + )} +
+
+ + {serviceToken} + +
+
+ ) : ( + +
+ + setName(e.target.value)} + /> +
+ +
+ {envScope.length === 0 && showEnvHint && ( + + Select an environment scope + + )} + + {({ open }) => ( + <> + + + + +
+ + {envScope + .map((env: Partial) => env.name) + .join(' + ')} + + +
+
+ + +
+ {envOptions.map((env: Partial) => ( + + {({ active, selected }) => ( +
+ {selected ? ( + + ) : ( + + )} + + {env.name} + +
+ )} +
+ ))} +
+
+
+ + )} +
+
+ +
+ + + + +
+ {tokenExpiryOptions.map((option) => ( + + {({ active, checked }) => ( +
+ {checked ? ( + + ) : ( + + )} + {option.name} +
+ )} +
+ ))} +
+
+ + {humanReadableExpiry(expiry)} + +
+ +
+ + +
+ + )} +
+
+
+
+
+
+ + ) +} + +export const SecretTokens = (props: { organisationId: string; appId: string }) => { + const { organisationId, appId } = props + + const [getServiceTokens, { data: serviceTokensData }] = useLazyQuery(GetServiceTokens) + + const [deleteServiceToken] = useMutation(RevokeServiceToken) + + const { activeOrganisation: organisation } = useContext(organisationContext) + + const handleDeleteServiceToken = async (tokenId: string) => { + await deleteServiceToken({ + variables: { tokenId }, + refetchQueries: [ + { + query: GetServiceTokens, + variables: { + organisationId, + appId, + }, + }, + ], + }) + } + + useEffect(() => { + if (organisationId && appId) { + getServiceTokens({ + variables: { + appId, + }, + }) + } + }, [appId, getServiceTokens, organisationId]) + + const DeleteConfirmDialog = (props: { + token: UserTokenType | ServiceTokenType + onDelete: Function + }) => { + const { token, onDelete } = props + + const [isOpen, setIsOpen] = useState(false) + + const closeModal = () => { + setIsOpen(false) + } + + const openModal = () => { + setIsOpen(true) + } + + return ( + <> +
+ +
+ + + + +
+ + +
+
+ + + +

+ Delete{' '} + + {token.name} + +

+ + +
+ +
+

+ Are you sure you want to delete this token? +

+
+ + +
+
+
+
+
+
+
+
+ + ) + } + + const CreatedToken = (props: { token: ServiceTokenType; deleteHandler: Function }) => { + const { token, deleteHandler } = props + + const isExpired = token.expiresAt === null ? false : new Date(token.expiresAt) < new Date() + + const activeUserIsAdmin = organisation ? userIsAdmin(organisation.role!) : false + + const allowDelete = activeUserIsAdmin || token.createdBy!.self + + return ( +
+
+ +
+
{token.name}
+
+
+
Created {relativeTimeFromDates(new Date(token.createdAt))}
+ {token.__typename === 'ServiceTokenType' && ( +
+ by + + {token.createdBy?.self + ? 'You' + : token.createdBy?.fullName || token.createdBy?.email} +
+ )} +
+ +
+ {isExpired ? 'Expired' : 'Expires'}{' '} + {token.expiresAt ? relativeTimeFromDates(new Date(token.expiresAt)) : 'never'} +
+
+
+
+ {allowDelete && ( +
+ +
+ )} +
+ ) + } + + return ( +
+
+
+

Service tokens

+

+ Tokens used to authenticate with the CLI from automated machines. Used for CI and + production environments. +

+
+
+ {serviceTokensData?.serviceTokens.map((serviceToken: ServiceTokenType) => ( + + ))} +
+ + +
+
+ ) +} diff --git a/frontend/components/auth/SignInButtons.tsx b/frontend/components/auth/SignInButtons.tsx index 2da76025..667cba3d 100644 --- a/frontend/components/auth/SignInButtons.tsx +++ b/frontend/components/auth/SignInButtons.tsx @@ -2,7 +2,7 @@ import clsx from 'clsx' import { signIn, useSession } from 'next-auth/react' -import { useRouter } from 'next/navigation' +import { useRouter, useSearchParams } from 'next/navigation' import { ReactNode, useEffect, useState } from 'react' import { FaGithub, FaGitlab, FaGoogle } from 'react-icons/fa' import { AnimatedLogo } from '../common/AnimatedLogo' @@ -44,6 +44,10 @@ export default function SignInButtons() { const { status } = useSession() const router = useRouter() + const searchParams = useSearchParams() + + const callbackUrl = searchParams.get('callbackUrl') + const titleText = () => (loading ? 'Logging you in' : 'Phase Console') useEffect(() => { @@ -57,7 +61,10 @@ export default function SignInButtons() { const handleProviderButtonClick = (providerId: string) => { setLoading(true) - signIn(providerId) + signIn(providerId, { + redirect: callbackUrl ? true : false, + callbackUrl: callbackUrl || '', + }) } return ( diff --git a/frontend/components/auth/UnlockKeyringDialog.tsx b/frontend/components/auth/UnlockKeyringDialog.tsx new file mode 100644 index 00000000..a1c7e111 --- /dev/null +++ b/frontend/components/auth/UnlockKeyringDialog.tsx @@ -0,0 +1,140 @@ +import { Dialog, Transition } from '@headlessui/react' +import { Fragment, useContext, useEffect, useState } from 'react' +import { FaEye, FaEyeSlash, FaLock, FaTimes } from 'react-icons/fa' +import { Button } from '../common/Button' +import { KeyringContext } from '@/contexts/keyringContext' +import { cryptoUtils } from '@/utils/auth' +import { getLocalKeyring } from '@/utils/localStorage' +import { useSession } from 'next-auth/react' +import clsx from 'clsx' +import { toast } from 'react-toastify' + +export default function UnlockKeyringDialog(props: { organisationId: string }) { + const [password, setPassword] = useState('') + const [showPw, setShowPw] = useState(false) + const [isOpen, setIsOpen] = useState(false) + const { keyring, setKeyring } = useContext(KeyringContext) + const { data: session } = useSession() + + useEffect(() => { + if (keyring === null) openModal() + }) + + const closeModal = () => { + setIsOpen(false) + } + + const openModal = () => { + setIsOpen(true) + } + + const decryptLocalKeyring = async (event: { preventDefault: () => void }) => { + event.preventDefault() + + try { + const decryptedKeyring = await cryptoUtils.getKeyring( + session?.user?.email!, + props.organisationId, + password + ) + setKeyring(decryptedKeyring) + toast.success('Unlocked user keyring.', { autoClose: 2000 }) + closeModal() + } catch (e) { + console.error(e) + toast.error('Failed to decrypt keys. Please verify your sudo password and try again.') + return false + } + } + + return ( + <> + + {}}> + +
+ + +
+
+ + + + +

+ Unlock User Keyring +

+
+
+
+

+ Please enter your sudo password to unlock the user keyring. + This is required for data to be decrypted on this screen. +

+
+
+
+ +
+ setPassword(e.target.value)} + type={showPw ? 'text' : 'password'} + minLength={16} + required + autoFocus + className="custom w-full text-zinc-800 font-mono dark:text-white bg-zinc-100 dark:bg-zinc-800 rounded-md ph-no-capture" + /> + +
+
+
+ +
+
+
+
+
+
+
+
+
+ + ) +} diff --git a/frontend/components/common/Alert.tsx b/frontend/components/common/Alert.tsx index 39cd9469..74bc1756 100644 --- a/frontend/components/common/Alert.tsx +++ b/frontend/components/common/Alert.tsx @@ -1,15 +1,42 @@ import clsx from 'clsx' import { ReactNode } from 'react' +import { FaCheck, FaExclamationTriangle, FaInfoCircle } from 'react-icons/fa' -export const Alert = (props: { children: ReactNode; variant: 'success' | 'warning' | 'info' }) => { - const variants = { +export const Alert = (props: { + children: ReactNode + variant: 'success' | 'warning' | 'info' | 'danger' + icon?: boolean + size?: 'sm' | 'md' +}) => { + const variantStyles = { success: 'bg-emerald-200/60 dark:bg-emerald-400/10 ring-emerald-500 text-emerald-500', - warning: 'bg-orange-800/20 dark:bg-orange-800/30 ring-orange-500 text-orange-500', - info: 'bg-sky-800/30 ring-sky-500 text-sky-500', + warning: + 'bg-amber-300/40 dark:bg-amber-400/10 ring-amber-500/40 text-black dark:text-amber-400', + info: 'bg-cyan-300/40 dark:bg-cyan-800/30 ring-cyan-400/10 text-black dark:text-cyan-400', + danger: 'bg-red-300/40 dark:bg-red-400/10 ring-red-500/40 text-black dark:text-red-400', + } + + const sizeStyles = { + sm: 'px-2 py-1 text-sm gap-2', + md: 'p-4 text-base gap-4', + } + + const variantIcons = { + success: , + warning: , + danger: , + info: , } return ( -
+
+ {props.icon && variantIcons[props.variant]} {props.children}
) diff --git a/frontend/components/common/Avatar.tsx b/frontend/components/common/Avatar.tsx new file mode 100644 index 00000000..8342dd29 --- /dev/null +++ b/frontend/components/common/Avatar.tsx @@ -0,0 +1,19 @@ +import clsx from 'clsx' + +export const Avatar = (props: { imagePath: string; size: 'sm' | 'md' | 'lg' | 'xl' }) => { + const sizes = { + sm: 'h-5 w-5', + md: 'h-8 w-8', + lg: 'h-12 w-12', + xl: 'h-20 w-20', + } + + const sizeStyle = sizes[props.size] + + return ( +
+ ) +} diff --git a/frontend/components/common/Button.tsx b/frontend/components/common/Button.tsx index b55b5a38..fb84f1fc 100644 --- a/frontend/components/common/Button.tsx +++ b/frontend/components/common/Button.tsx @@ -32,6 +32,10 @@ function ArrowIcon(props: { className: string }) { const variantStyles: Record = { primary: 'rounded-full bg-zinc-900 py-1 px-3 text-white hover:bg-zinc-700 dark:bg-emerald-400/10 dark:text-emerald-400 dark:ring-1 dark:ring-inset dark:ring-emerald-400/20 dark:hover:bg-emerald-400/10 dark:hover:text-emerald-300 dark:hover:ring-emerald-300', + warning: + 'rounded-full bg-amber-700 py-1 px-3 text-white hover:bg-amber-600 dark:bg-amber-400/10 dark:text-amber-400 dark:ring-1 dark:ring-inset dark:ring-amber-400/20 dark:hover:bg-amber-400/10 dark:hover:text-amber-300 dark:hover:ring-amber-300', + danger: + 'rounded-full bg-red-700 py-1 px-3 text-white hover:bg-red-600 dark:bg-red-400/10 dark:text-red-400 dark:ring-1 dark:ring-inset dark:ring-red-400/20 dark:hover:bg-red-400/10 dark:hover:text-red-300 dark:hover:ring-red-300', secondary: 'rounded-full bg-zinc-100 py-1 px-3 text-zinc-900 hover:bg-zinc-200 dark:bg-zinc-800/40 dark:text-zinc-400 dark:ring-1 dark:ring-inset dark:ring-zinc-800 dark:hover:bg-zinc-800 dark:hover:text-zinc-300', filled: diff --git a/frontend/components/common/ModeToggle.tsx b/frontend/components/common/ModeToggle.tsx index 58751d4a..b7d01d7e 100644 --- a/frontend/components/common/ModeToggle.tsx +++ b/frontend/components/common/ModeToggle.tsx @@ -20,17 +20,15 @@ export function ModeToggle() { checked={isDark} onChange={toggleTheme} className={`${ - isDark ? 'bg-sky-900' : 'bg-sky-200' - } relative inline-flex h-6 w-11 items-center rounded-full`} + isDark ? 'bg-emerald-400/10 ring-emerald-400/20' : 'bg-neutral-500/40 ring-neutral-500/30' + } relative inline-flex h-6 w-11 items-center rounded-full ring-1 ring-inset`} > Enable dark theme - {isDark ? : } - + isDark ? 'translate-x-6 bg-emerald-400' : 'translate-x-1 bg-black' + } flex items-center justify-center h-4 w-4 transform rounded-full transition`} + >
) diff --git a/frontend/components/environments/SecretPropertyDiffs.tsx b/frontend/components/environments/SecretPropertyDiffs.tsx new file mode 100644 index 00000000..8438107b --- /dev/null +++ b/frontend/components/environments/SecretPropertyDiffs.tsx @@ -0,0 +1,83 @@ +import { SecretEventType, SecretTagType, SecretType } from '@/apollo/graphql' +import { areTagsAreSame } from '@/utils/tags' +import { Tag } from './SecretRow' + +export const SecretPropertyDiffs = (props: { + secret: SecretType + historyItem: SecretEventType + index: number +}) => { + const { secret, historyItem, index } = props + + const previousItem = secret.history![index - 1]! + + const getAddedTags = () => { + const addedTags = historyItem!.tags.filter((currentTag: SecretTagType) => + previousItem.tags.every((previousTag: SecretTagType) => previousTag.id !== currentTag.id) + ) + return addedTags + } + + const getRemovedTags = () => { + const removedTags = previousItem.tags.filter((previousTag: SecretTagType) => + historyItem.tags.every((currentTag) => currentTag.id !== previousTag.id) + ) + return removedTags + } + + return ( + <> + {historyItem!.key !== previousItem.key && ( +
+ KEY: + + {previousItem.key} + + + {historyItem!.key} + +
+ )} + + {historyItem!.value !== previousItem.value && ( +
+ VALUE: + + {previousItem.value} + + + {historyItem!.value} + +
+ )} + + {historyItem!.comment !== previousItem.comment && ( +
+ COMMENT: + + {previousItem.comment} + + + {historyItem!.comment} + +
+ )} + + {!areTagsAreSame(historyItem!.tags, previousItem.tags) && ( +
+ TAGS: +
+ {getRemovedTags().map((tag) => ( + + ))} +
+
+ {getAddedTags().map((tag) => ( + + ))} +
+
+ )} + + ) +} diff --git a/frontend/components/environments/SecretRow.tsx b/frontend/components/environments/SecretRow.tsx new file mode 100644 index 00000000..174ca4cf --- /dev/null +++ b/frontend/components/environments/SecretRow.tsx @@ -0,0 +1,674 @@ +import { + ApiSecretEventEventTypeChoices, + Maybe, + SecretEventType, + SecretTagType, + SecretType, +} from '@/apollo/graphql' +import { Fragment, useEffect, useState } from 'react' +import { + FaEyeSlash, + FaEye, + FaTimes, + FaRegCommentDots, + FaTrashAlt, + FaHistory, + FaPlus, + FaUser, + FaTags, + FaCheckSquare, + FaSquare, + FaKey, + FaInfo, +} from 'react-icons/fa' +import { Button } from '../common/Button' +import { Dialog, Popover, Transition } from '@headlessui/react' +import { GetSecretTags } from '@/graphql/queries/secrets/getSecretTags.gql' +import { CreateNewSecretTag } from '@/graphql/mutations/environments/createSecretTag.gql' +import { LogSecretRead } from '@/graphql/mutations/environments/readSecret.gql' +import clsx from 'clsx' +import { relativeTimeFromDates } from '@/utils/time' +import { useLazyQuery, useMutation } from '@apollo/client' +import { areTagsAreSame } from '@/utils/tags' +import { Avatar } from '../common/Avatar' +import { SecretPropertyDiffs } from './SecretPropertyDiffs' + +export const Tag = (props: { tag: SecretTagType }) => { + const { name, color } = props.tag + + return ( +
+
+ {name} +
+ ) +} + +const TagsDialog = (props: { + orgId: string + secretId: string + secretName: string + tags: Array + handlePropertyChange: Function +}) => { + const { orgId, secretId, secretName, tags, handlePropertyChange } = props + + const [getOrgTags, { data: orgTags }] = useLazyQuery(GetSecretTags) + const [createSecretTag] = useMutation(CreateNewSecretTag) + + const [secretTags, setSecretTags] = useState>(tags) + + const [isOpen, setIsOpen] = useState(false) + + const [newTag, setNewTag] = useState>({ name: '', color: '' }) + + useEffect(() => { + if (isOpen) + getOrgTags({ + variables: { + orgId, + }, + }) + }, [getOrgTags, isOpen, orgId]) + + const closeModal = () => { + setIsOpen(false) + } + + const openModal = () => { + setIsOpen(true) + } + + const handleClose = () => { + handlePropertyChange(secretId, 'tags', secretTags) + closeModal() + } + + const handleNewTagNameChange = (name: string) => setNewTag({ ...newTag, ...{ name } }) + + const handleNewTagColorChange = (color: string) => setNewTag({ ...newTag, ...{ color } }) + + const handleCreateTag = async () => { + const { name, color } = newTag + + if (!name) return false + + await createSecretTag({ + variables: { + orgId, + name, + color, + }, + refetchQueries: [ + { + query: GetSecretTags, + variables: { + orgId, + }, + }, + ], + }) + setNewTag({ name: '', color: '' }) + } + + const TagSelector = (props: { tag: SecretTagType }) => { + const { id, name, color } = props.tag + + const isSelected = secretTags.map((secretTag) => secretTag.name).includes(name) + + const handleTagClick = () => { + if (isSelected) { + setSecretTags(secretTags.filter((tag) => tag.name !== name)) + } else setSecretTags([...secretTags, ...[{ id, name, color }]]) + } + + return ( +
+ {isSelected ? ( + + ) : ( + + )} +
+ +
+
+ ) + } + + return ( + <> + {tags.length > 0 ? ( +
+ {tags.map((tag) => ( + + ))} +
+ ) : ( +
+ +
+ )} + + + + +
+ + +
+
+ + + +

+ Update{' '} + + {secretName} + {' '} + tags +

+ + +
+ +
+
+ {orgTags?.secretTags.map((tag: SecretTagType) => ( + + ))} +
+
+ handleNewTagNameChange(e.target.value)} + /> + handleNewTagColorChange(e.target.value)} + /> +
+ +
+
+
+
+
+
+
+
+
+ + ) +} + +const HistoryDialog = (props: { secret: SecretType }) => { + const { secret } = props + + const [isOpen, setIsOpen] = useState(false) + + const closeModal = () => { + setIsOpen(false) + } + + const openModal = () => { + setIsOpen(true) + } + + const getEventTypeColor = (eventType: ApiSecretEventEventTypeChoices) => { + if (eventType === ApiSecretEventEventTypeChoices.C) return 'bg-emerald-500' + if (eventType === ApiSecretEventEventTypeChoices.U) return 'bg-yellow-500' + if (eventType === ApiSecretEventEventTypeChoices.R) return 'bg-blue-500' + if (eventType === ApiSecretEventEventTypeChoices.D) return 'bg-red-500' + } + + const getEventTypeText = (eventType: ApiSecretEventEventTypeChoices) => { + if (eventType === ApiSecretEventEventTypeChoices.C) return 'Created' + if (eventType === ApiSecretEventEventTypeChoices.U) return 'Updated' + if (eventType === ApiSecretEventEventTypeChoices.R) return 'Read' + if (eventType === ApiSecretEventEventTypeChoices.D) return 'Deleted' + } + + const secretHistory = secret.history?.filter( + (event: Maybe) => event?.eventType! !== ApiSecretEventEventTypeChoices.R + ) + + return ( + <> +
+ +
+ + + + +
+ + +
+
+ + + +

+ + {secret.key} + {' '} + history +

+ + +
+ +
+
+
+ {secretHistory?.map((historyItem, index) => ( +
+
+ +
+ {getEventTypeText(historyItem!.eventType)} +
+
+ {relativeTimeFromDates(new Date(historyItem!.timestamp))} +
{' '} + by +
+ {historyItem!.user ? ( +
+ + {historyItem?.user.fullName || historyItem?.user.email} +
+ ) : ( +
+ Service token +
+ )} +
+
+ {index > 0 && ( + + )} +
+ ))} +
+
+
+
+
+
+
+
+
+ + ) +} + +const CommentDialog = (props: { + secretId: string + secretName: string + comment: string + handlePropertyChange: Function +}) => { + const { secretId, secretName, comment, handlePropertyChange } = props + + const [commentValue, setCommentValue] = useState(comment) + + const [isOpen, setIsOpen] = useState(false) + + const closeModal = () => { + setIsOpen(false) + } + + const openModal = () => { + setIsOpen(true) + } + + const handleClose = () => { + handlePropertyChange(secretId, 'comment', commentValue) + closeModal() + } + + return ( + <> +
+ +
+ + + + +
+ + +
+
+ + + +

+ Update{' '} + + {secretName} + {' '} + comment +

+ + +
+ +
+ +
+ +
+ +
+
+
+
+
+
+
+ + ) +} + +const DeleteConfirmDialog = (props: { + secretId: string + secretName: string + onDelete: Function +}) => { + const { secretName, secretId, onDelete } = props + + const [isOpen, setIsOpen] = useState(false) + + const closeModal = () => { + setIsOpen(false) + } + + const openModal = () => { + setIsOpen(true) + } + + return ( + <> +
+ +
+ + + + +
+ + +
+
+ + + +

+ Delete{' '} + + {secretName} + +

+ + +
+ +
+

Are you sure you want to delete this secret?

+
+ + +
+
+
+
+
+
+
+
+ + ) +} + +export default function SecretRow(props: { + orgId: string + secret: SecretType + cannonicalSecret: SecretType | undefined + secretNames: Array> + handlePropertyChange: Function + handleDelete: Function +}) { + const { orgId, secret, cannonicalSecret, secretNames, handlePropertyChange, handleDelete } = props + + const [isRevealed, setIsRevealed] = useState(false) + + const [readSecret] = useMutation(LogSecretRead) + + const handleRevealSecret = async () => { + setIsRevealed(true) + await readSecret({ variables: { id: secret.id } }) + } + + const handleHideSecret = () => setIsRevealed(false) + + const toggleReveal = () => { + isRevealed ? handleHideSecret() : handleRevealSecret() + } + + const INPUT_BASE_STYLE = + 'w-full text-zinc-800 font-mono custom bg-zinc-100 dark:bg-zinc-800 dark:text-white transition ease ph-no-capture' + + const keyIsBlank = secret.key.length === 0 + + const keyIsDuplicate = + secretNames.findIndex((s) => s.key === secret.key && s.id !== secret.id) > -1 + + const secretHasBeenModified = () => { + if (cannonicalSecret === undefined) return true + return ( + secret.key !== cannonicalSecret.key || + secret.value !== cannonicalSecret.value || + secret.comment !== cannonicalSecret.comment || + !areTagsAreSame(secret.tags, cannonicalSecret.tags) + ) + } + + return ( +
+
+ handlePropertyChange(secret.id, 'key', e.target.value.toUpperCase())} + /> +
+
+ +
+
+
+
+ handlePropertyChange(secret.id, 'value', e.target.value)} + /> + +
+
+ +
+ +
+ +
+ +
+ +
+
+
+
+ +
+
+ ) +} diff --git a/frontend/components/layout/Navbar.tsx b/frontend/components/layout/Navbar.tsx index 461889f0..f9eba8ac 100644 --- a/frontend/components/layout/Navbar.tsx +++ b/frontend/components/layout/Navbar.tsx @@ -1,30 +1,31 @@ import { Logo } from '../common/Logo' import UserMenu from '../UserMenu' -import { useLazyQuery, useQuery } from '@apollo/client' -import GetOrganisations from '@/apollo/queries/getOrganisations.gql' -import { GetApps } from '@/apollo/queries/getApps.gql' +import { useLazyQuery } from '@apollo/client' +import { GetApps } from '@/graphql/queries/getApps.gql' +import { GetAppEnvironments } from '@/graphql/queries/secrets/getAppEnvironments.gql' import { usePathname } from 'next/navigation' -import { useEffect } from 'react' -import { AppType } from '@/apollo/graphql' +import { useContext, useEffect } from 'react' +import { AppType, EnvironmentType } from '@/apollo/graphql' import Link from 'next/link' import { Button } from '../common/Button' import { StatusIndicator } from '../common/StatusIndicator' +import { organisationContext } from '@/contexts/organisationContext' +import clsx from 'clsx' export const NavBar = (props: { team: string }) => { - const { data: orgsData } = useQuery(GetOrganisations) + const { activeOrganisation: organisation } = useContext(organisationContext) + const [getApps, { data: appsData }] = useLazyQuery(GetApps) + const [getAppEnvs, { data: appEnvsData }] = useLazyQuery(GetAppEnvironments) const IS_CLOUD_HOSTED = process.env.APP_HOST || process.env.NEXT_PUBLIC_APP_HOST useEffect(() => { - if (orgsData?.organisations) { + if (organisation) { const fetchData = async () => { - const org = orgsData.organisations[0] - - const organisationId = org.id getApps({ variables: { - organisationId, + organisationId: organisation.id, appId: '', }, }) @@ -32,14 +33,27 @@ export const NavBar = (props: { team: string }) => { fetchData() } - }, [getApps, orgsData]) + }, [getApps, organisation]) const apps = appsData?.apps as AppType[] + const envs: EnvironmentType[] = appEnvsData?.appEnvironments ?? [] + const appId = usePathname()?.split('/')[3] + const envId = usePathname()?.split('/')[5] + + const appPage = usePathname()?.split('/')[4] + const activeApp = apps?.find((app) => app.id === appId) + useEffect(() => { + if (activeApp) getAppEnvs({ variables: { appId: activeApp.id } }) + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [activeApp]) + + const activeEnv = activeApp ? envs.find((env) => env.id === envId) : undefined + return (
@@ -47,14 +61,42 @@ export const NavBar = (props: { team: string }) => { / + {!activeApp && {props.team}} + {activeApp && {props.team}} + {activeApp && /} - {activeApp && {activeApp.name}} + + {activeApp && + (appPage ? ( + {activeApp.name} + ) : ( + {activeApp.name} + ))} + + {appPage && /} + + {appPage && ( + + {appPage} + + )} + + {activeEnv && /} + + {activeEnv && {activeEnv.name}}
{IS_CLOUD_HOSTED && } - + + +
diff --git a/frontend/components/layout/OnboardingNavbar.tsx b/frontend/components/layout/OnboardingNavbar.tsx index 60e812e6..3324b720 100644 --- a/frontend/components/layout/OnboardingNavbar.tsx +++ b/frontend/components/layout/OnboardingNavbar.tsx @@ -2,6 +2,7 @@ import Link from 'next/link' import { AnimatedLogo } from '../common/AnimatedLogo' import { ModeToggle } from '../common/ModeToggle' import UserMenu from '../UserMenu' +import { FaSun, FaMoon } from 'react-icons/fa' const OnboardingNavbar = () => { return ( @@ -17,7 +18,11 @@ const OnboardingNavbar = () => { phase.dev
- +
+ + + +
diff --git a/frontend/components/layout/Sidebar.tsx b/frontend/components/layout/Sidebar.tsx index 749dd5b3..3fffa85e 100644 --- a/frontend/components/layout/Sidebar.tsx +++ b/frontend/components/layout/Sidebar.tsx @@ -1,10 +1,21 @@ 'use client' import Link from 'next/link' -import UserMenu from '../UserMenu' -import { usePathname } from 'next/navigation' +import { usePathname, useRouter } from 'next/navigation' import clsx from 'clsx' -import { FaCog, FaCubes, FaHome } from 'react-icons/fa' +import { + FaChevronDown, + FaCog, + FaCubes, + FaExchangeAlt, + FaHome, + FaKey, + FaUsersCog, +} from 'react-icons/fa' +import { organisationContext } from '@/contexts/organisationContext' +import { Fragment, useContext } from 'react' +import { OrganisationType } from '@/apollo/graphql' +import { Menu, Transition } from '@headlessui/react' export type SidebarLinkT = { name: string @@ -15,12 +26,12 @@ export type SidebarLinkT = { const SidebarLink = (props: SidebarLinkT) => { const { name, href, icon, active } = props - const iconStyles = 'hover:text-emerald-500' + return (
@@ -34,52 +45,133 @@ const SidebarLink = (props: SidebarLinkT) => { const Sidebar = () => { const team = usePathname()?.split('/')[1] + const { organisations, activeOrganisation, setActiveOrganisation } = + useContext(organisationContext) + + const showOrgsMenu = organisations === null ? false : organisations?.length > 1 + + const OrgsMenu = () => { + const router = useRouter() + const switchOrg = (org: OrganisationType) => { + router.push(`/${org!.name}`) + } + return ( + + {({ open }) => ( + <> + + {activeOrganisation?.name} + + + + +
+ {organisations?.map((org: OrganisationType) => ( + + {({ active }) => ( + + )} + + ))} +
+
+
+ + )} +
+ ) + } + const links: SidebarLinkT[] = [ { name: 'Home', href: `/${team}`, - icon: , + icon: , active: usePathname() === `/${team}`, }, { name: 'Apps', href: `/${team}/apps`, - icon: , - active: usePathname() === `/${team}/apps`, + icon: , + active: usePathname()?.split('/')[2] === 'apps', + }, + { + name: 'Members', + href: `/${team}/members`, + icon: , + active: usePathname() === `/${team}/members`, + }, + { + name: 'Personal access tokens', + href: `/${team}/tokens`, + icon: , + active: usePathname() === `/${team}/tokens`, }, { name: 'Settings', href: `/${team}/settings`, - icon: , + icon: , active: usePathname() === `/${team}/settings`, }, ] return ( - +
+ +
) } diff --git a/frontend/components/logs/KmsLogs.tsx b/frontend/components/logs/KmsLogs.tsx new file mode 100644 index 00000000..0b9e8923 --- /dev/null +++ b/frontend/components/logs/KmsLogs.tsx @@ -0,0 +1,339 @@ +'use client' + +import { GetAppKmsLogs } from '@/graphql/queries/getAppKmsLogs.gql' +import { useLazyQuery } from '@apollo/client' +import { KmsLogType } from '@/apollo/graphql' +import { Disclosure, Transition } from '@headlessui/react' +import clsx from 'clsx' +import { FaChevronRight } from 'react-icons/fa' +import { SiNodedotjs, SiPython } from 'react-icons/si' +import { FiRefreshCw, FiChevronsDown } from 'react-icons/fi' +import getUnicodeFlagIcon from 'country-flag-icons/unicode' +import { relativeTimeFromDates } from '@/utils/time' +import { humanFileSize } from '@/utils/dataUnits' +import { ReactNode, useEffect, useRef, useState } from 'react' +import { Button } from '@/components/common/Button' +import { Count } from 'reaviz' +import Spinner from '@/components/common/Spinner' + +// The historical start date for all log data (May 1st, 2023) +const LOGS_START_DATE = 1682904457000 + +export default function KMSLogs(props: { app: string }) { + const DEFAULT_PAGE_SIZE = 25 + const loglistEndRef = useRef(null) + const tableBodyRef = useRef(null) + const [getAppLogs, { data, loading }] = useLazyQuery(GetAppKmsLogs) + const [totalCount, setTotalCount] = useState(0) + const [logList, setLogList] = useState([]) + + const [endofList, setEndofList] = useState(false) + + const getCurrentTimeStamp = () => Date.now() + const getLastLogTimestamp = () => + logList.length > 0 ? logList[logList.length - 1].timestamp : getCurrentTimeStamp() + + /** + * Fetches logs for the app with the given start and end timestamps, + * and then adds the result of the query to the current log list. + * + * @param {number} start - Start datetime as unix timestamp (ms) + * @param {number} end - End datetime as unix timestamp (ms) + * + * @returns {void} + */ + const fetchLogs = (start: number, end: number) => { + getAppLogs({ + variables: { + appId: props.app, + start, + end, + }, + fetchPolicy: 'network-only', + }).then((result) => { + if (result.data?.logs.kms.length) { + setLogList(logList.concat(result.data.logs.kms)) + } + if (result.data?.logs.length < DEFAULT_PAGE_SIZE) setEndofList(true) + }) + } + + const clearLogList = () => setLogList([]) + + /** + * Gets the first page of logs, by resetting the log list and fetching logs using the current unix timestamp. + * + * @returns {void} + */ + const getFirstPage = () => { + setEndofList(false) + fetchLogs(LOGS_START_DATE, getCurrentTimeStamp()) + } + + /** + * Gets the new page of logs by using the last available timestamp from the current log list + * + * @returns {void} + */ + const getNextPage = () => { + fetchLogs(LOGS_START_DATE, getLastLogTimestamp()) + } + + /** + * Hook to get the first page of logs on page load, or when the loglist is reset to empty + */ + useEffect(() => { + if (logList.length === 0) getFirstPage() + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [props.app, logList]) + + /** + * Hook to update the log count once its available + */ + useEffect(() => { + if (data?.kmsLogsCount) setTotalCount(data.kmsLogsCount) + }, [data]) + + // useEffect(() => { + // const options = { + // root: null, + // rootMargin: '0px', + // threshold: 1.0, + // } + // const observer = new IntersectionObserver((entries) => { + // const [entry] = entries + // if (entry.isIntersecting) getNextPage() + // }, options) + + // if (loglistEndRef.current) { + // if (endofList) observer.unobserve(loglistEndRef.current) + // else observer.observe(loglistEndRef.current) + // } + + // return () => { + // if (loglistEndRef.current) observer.unobserve(loglistEndRef.current) + // } + + // // eslint-disable-next-line react-hooks/exhaustive-deps + // }, [loglistEndRef]) + + const LogRow = (props: { log: KmsLogType }) => { + const { log } = props + + const SDKIcon = (sdkName: string) => { + const sdks = [ + { + name: 'node-js', + label: 'Node.js', + icon: , + color: 'bg-[#339933]', + }, + { + name: 'python', + label: 'Python', + icon: , + color: 'bg-[#3776AB]', + }, + ] + + const sdk = sdks.find((sdk) => sdkName.toLowerCase().includes(sdk.name)) || sdks[0] + + return ( +
+ {sdk.icon} +
+ ) + } + + const relativeTimeStamp = () => { + return relativeTimeFromDates(new Date(log.timestamp)) + } + + const verboseTimeStamp = () => { + const date = new Date(log.timestamp) + return date.toISOString() + } + + const LogField = (props: { label: string; children: ReactNode }) => { + return ( +
+ {props.label}: + {props.children} +
+ ) + } + + return ( + + {({ open }) => ( + <> + + {/* */} + + + + {SDKIcon(log.phaseNode!)} + {log.eventType} + + {humanFileSize(log.phSize!)} + + + {log.city} {log.country ? getUnicodeFlagIcon(log.country) : 'Not available'} + + + {relativeTimeStamp()} + + {/* */} + + + + +
+ Log ID: + {log.id} +
+
+ +
+ {SDKIcon(log.phaseNode!)} {log.phaseNode} +
+
+ + + {log.eventType} + + + {humanFileSize(log.phSize!)} + + {(log.city || log.country) && ( + + {' '} + {log.city}, {log.country}{' '} + {log.country ? getUnicodeFlagIcon(log.country) : 'Not available'} + + )} + + {log.ipAddress} + + {verboseTimeStamp()} +
+
+ +
+ + )} +
+ ) + } + + const SkeletonRow = (props: { rows: number }) => { + const SKELETON_BASE_STYLE = 'dark:bg-neutral-700 bg-neutral-300 animate-pulse' + return ( + <> + {[...Array(props.rows)].map((_, n) => ( + + + + + +
+ + +
+ + +
+ + +
+ + +
+ + + ))} + + ) + } + + return ( +
+
+ + {totalCount && } Events + + +
+ + + + + + + + + + + + + {logList.map((log, n) => ( + + ))} + {loading && } + + + + +
SDKEventDataLocationTime
+
+ {!endofList && ( + + )} + {endofList && `No${logList.length ? ' more ' : ' '}logs to show`} +
+
+
+ ) +} diff --git a/frontend/components/logs/SecretLogs.tsx b/frontend/components/logs/SecretLogs.tsx new file mode 100644 index 00000000..bfc895d8 --- /dev/null +++ b/frontend/components/logs/SecretLogs.tsx @@ -0,0 +1,429 @@ +'use client' + +import { GetAppSecretsLogs } from '@/graphql/queries/secrets/getAppSecretsLogs.gql' +import { useLazyQuery } from '@apollo/client' +import { + ApiSecretEventEventTypeChoices, + EnvironmentKeyType, + KmsLogType, + SecretEventType, +} from '@/apollo/graphql' +import { Disclosure, Transition } from '@headlessui/react' +import clsx from 'clsx' +import { FaChevronRight, FaExternalLinkAlt, FaKey } from 'react-icons/fa' +import { SiNodedotjs, SiPython } from 'react-icons/si' +import { FiRefreshCw, FiChevronsDown } from 'react-icons/fi' +import getUnicodeFlagIcon from 'country-flag-icons/unicode' +import { dateToUnixTimestamp, relativeTimeFromDates } from '@/utils/time' +import { humanFileSize } from '@/utils/dataUnits' +import { ReactNode, useContext, useEffect, useRef, useState } from 'react' +import { Button } from '@/components/common/Button' +import { Count } from 'reaviz' +import Spinner from '@/components/common/Spinner' +import { Avatar } from '../common/Avatar' +import { EnvKeyring, envKeyring } from '@/utils/environments' +import { KeyringContext } from '@/contexts/keyringContext' +import { getUserKxPublicKey, getUserKxPrivateKey, decryptAsymmetric } from '@/utils/crypto' +import { organisationContext } from '@/contexts/organisationContext' +import UnlockKeyringDialog from '../auth/UnlockKeyringDialog' +import { usePathname } from 'next/navigation' +import Link from 'next/link' + +// The historical start date for all log data (May 1st, 2023) +const LOGS_START_DATE = 1682904457000 + +type EnvKey = { + envId: string + keys: EnvKeyring +} + +export default function SecretLogs(props: { app: string }) { + const DEFAULT_PAGE_SIZE = 25 + const loglistEndRef = useRef(null) + const tableBodyRef = useRef(null) + const [getAppLogs, { data, loading }] = useLazyQuery(GetAppSecretsLogs) + const [totalCount, setTotalCount] = useState(0) + const [logList, setLogList] = useState([]) + const [envKeys, setEnvKeys] = useState([]) + const [endofList, setEndofList] = useState(false) + + const { activeOrganisation: organisation } = useContext(organisationContext) + const { keyring } = useContext(KeyringContext) + + const getCurrentTimeStamp = () => Date.now() + const getLastLogTimestamp = () => + logList.length > 0 + ? dateToUnixTimestamp(logList[logList.length - 1].timestamp) + : getCurrentTimeStamp() + + /** + * Fetches logs for the app with the given start and end timestamps, + * and then adds the result of the query to the current log list. + * + * @param {number} start - Start datetime as unix timestamp (ms) + * @param {number} end - End datetime as unix timestamp (ms) + * + * @returns {void} + */ + const fetchLogs = (start: number, end: number) => { + getAppLogs({ + variables: { + appId: props.app, + start, + end, + }, + fetchPolicy: 'network-only', + }).then((result) => { + if (result.data?.logs.secrets.length) { + setLogList(logList.concat(result.data.logs.secrets)) + } + if (result.data?.logs.length < DEFAULT_PAGE_SIZE) setEndofList(true) + }) + } + + const clearLogList = () => setLogList([]) + + /** + * Gets the first page of logs, by resetting the log list and fetching logs using the current unix timestamp. + * + * @returns {void} + */ + const getFirstPage = () => { + setEndofList(false) + fetchLogs(LOGS_START_DATE, getCurrentTimeStamp()) + } + + /** + * Gets the new page of logs by using the last available timestamp from the current log list + * + * @returns {void} + */ + const getNextPage = () => { + fetchLogs(LOGS_START_DATE, getLastLogTimestamp()) + } + + /** + * Hook to get the first page of logs on page load, or when the loglist is reset to empty + */ + useEffect(() => { + if (logList.length === 0) getFirstPage() + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [props.app, logList]) + + /** + * Hook to update the log count once its available + */ + useEffect(() => { + if (data?.secretsLogsCount) setTotalCount(data.secretsLogsCount) + }, [data]) + + useEffect(() => { + const initEnvKeys = async () => { + const keys = [] as EnvKey[] + + const unwrapKeyPromises = data.environmentKeys.map(async (envKey: EnvironmentKeyType) => { + const { wrappedSeed } = envKey + + const userKxKeys = { + publicKey: await getUserKxPublicKey(keyring!.publicKey), + privateKey: await getUserKxPrivateKey(keyring!.privateKey), + } + const seed = await decryptAsymmetric( + wrappedSeed, + userKxKeys.privateKey, + userKxKeys.publicKey + ) + + const { publicKey, privateKey } = await envKeyring(seed) + + keys.push({ + envId: envKey.environment.id, + keys: { publicKey, privateKey }, + }) + }) + + await Promise.all(unwrapKeyPromises) + + setEnvKeys(keys) + } + + if (data && keyring) initEnvKeys() + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [data, keyring]) + + // useEffect(() => { + // const options = { + // root: null, + // rootMargin: '0px', + // threshold: 1.0, + // } + // const observer = new IntersectionObserver((entries) => { + // const [entry] = entries + // if (entry.isIntersecting) getNextPage() + // }, options) + + // if (loglistEndRef.current) { + // if (endofList) observer.unobserve(loglistEndRef.current) + // else observer.observe(loglistEndRef.current) + // } + + // return () => { + // if (loglistEndRef.current) observer.unobserve(loglistEndRef.current) + // } + + // // eslint-disable-next-line react-hooks/exhaustive-deps + // }, [loglistEndRef]) + + const LogRow = (props: { log: SecretEventType }) => { + const { log } = props + + const appPath = usePathname()?.split('/').slice(0, -1).join('/') + + const [decryptedEvent, setDecryptedEvent] = useState(null) + + useEffect(() => { + const decryptSecretEvent = async () => { + const event = log + + const decryptedEvent = structuredClone(event) + + const envKeyPair = envKeys.find((envKey) => envKey.envId === event.environment.id) + + const { publicKey, privateKey } = envKeyPair!.keys + + // Decrypt event fields + decryptedEvent!.key = await decryptAsymmetric(event!.key, privateKey, publicKey) + + setDecryptedEvent(decryptedEvent) + } + + if (log && envKeys.length > 0) decryptSecretEvent() + }, [log]) + + const relativeTimeStamp = () => { + return relativeTimeFromDates(new Date(log.timestamp)) + } + + const verboseTimeStamp = () => { + const date = new Date(log.timestamp) + return date.toISOString() + } + + const LogField = (props: { label: string; children: ReactNode }) => { + return ( +
+ {props.label}: + {props.children} +
+ ) + } + + const getEventTypeColor = (eventType: ApiSecretEventEventTypeChoices) => { + if (eventType === ApiSecretEventEventTypeChoices.C) return 'bg-emerald-500' + if (eventType === ApiSecretEventEventTypeChoices.U) return 'bg-yellow-500' + if (eventType === ApiSecretEventEventTypeChoices.R) return 'bg-blue-500' + if (eventType === ApiSecretEventEventTypeChoices.D) return 'bg-red-500' + } + + const getEventTypeText = (eventType: ApiSecretEventEventTypeChoices) => { + if (eventType === ApiSecretEventEventTypeChoices.C) return 'Created secret' + if (eventType === ApiSecretEventEventTypeChoices.U) return 'Updated secret' + if (eventType === ApiSecretEventEventTypeChoices.R) return 'Read secret' + if (eventType === ApiSecretEventEventTypeChoices.D) return 'Deleted secret' + } + + return ( + + {({ open }) => ( + <> + + {/* */} + + + + +
+ {log.user ? ( +
+ + {log.user.fullName || log.user.email} +
+ ) : ( +
+ Service token +
+ )} +
+ + +
+ +
+ {getEventTypeText(log.eventType)} +
+
+ + {log.environment.envType} + + {decryptedEvent?.key} + + + {relativeTimeStamp()} + + {/* */} +
+ + + +
+ Event ID: + {log.id} +
+ +
+ +
+ {decryptedEvent?.key} +
+
+ + {log.ipAddress} + + {log.userAgent} + + {verboseTimeStamp()} + +
+ +
+
+
+ +
+ + )} +
+ ) + } + + const SkeletonRow = (props: { rows: number }) => { + const SKELETON_BASE_STYLE = 'dark:bg-neutral-700 bg-neutral-300 animate-pulse' + return ( + <> + {[...Array(props.rows)].map((_, n) => ( + + + + + +
+ + +
+ + +
+ + +
+ + +
+ + + ))} + + ) + } + + return ( + <> + {organisation && } +
+
+ + {totalCount && } Events + + +
+ + + + + + + + + + + + + {logList.map((log, n) => ( + + ))} + {loading && } + + + + +
UserEventEnvironmentSecretTime
+
+ {!endofList && ( + + )} + {endofList && `No${logList.length ? ' more ' : ' '}logs to show`} +
+
+
+ + ) +} diff --git a/frontend/components/onboarding/AccountPassword.tsx b/frontend/components/onboarding/AccountPassword.tsx index f567645c..69805442 100644 --- a/frontend/components/onboarding/AccountPassword.tsx +++ b/frontend/components/onboarding/AccountPassword.tsx @@ -1,6 +1,7 @@ import { useEffect, useState } from 'react' import { ZXCVBNResult } from 'zxcvbn' -import { FaEye, FaEyeSlash, FaInfo } from 'react-icons/fa' +import { FaCheck, FaEye, FaEyeSlash, FaInfo } from 'react-icons/fa' +import clsx from 'clsx' interface AccountPasswordProps { pw: string @@ -27,23 +28,23 @@ export const AccountPassword = (props: AccountPasswordProps) => { * @returns {string} */ const pwStrengthColor = (): string => { - let color = 'red' + let color = 'bg-red-500' if (!pw) return color switch (pwStrength.score) { case 1: - color = 'orange' + color = 'bg-orange-500' break case 2: - color = 'yellow' + color = 'bg-yellow-500' break case 3: - color = 'blue' + color = 'bg-blue-500' break case 4: - color = 'green' + color = 'bg-emerald-500' break default: - color = 'red' + color = 'bg-red-500' break } @@ -56,66 +57,73 @@ export const AccountPassword = (props: AccountPasswordProps) => { return score } + const passwordIsStrong = pwStrength?.feedback?.suggestions?.length == 0 || false + return (
- -
- setPw(e.target.value)} - type={showPw ? 'text' : 'password'} - minLength={16} - required - className="w-full " - /> - +
+ +
+ setPw(e.target.value)} + type={showPw ? 'text' : 'password'} + minLength={16} + required + className="w-full ph-no-capture" + /> + +
- -
- setPw2(e.target.value)} - type={showPw2 ? 'text' : 'password'} - minLength={16} - required - className="w-full" - /> - + +
+ +
+ setPw2(e.target.value)} + type={showPw2 ? 'text' : 'password'} + minLength={16} + required + className="w-full ph-no-capture" + /> + +
-
+ +
- {pwStrength.feedback && ( -
- - {pwStrength.feedback.suggestions} -
- )} + +
+ {passwordIsStrong ? : } + {passwordIsStrong ? 'Strong password' : pwStrength?.feedback?.suggestions} +
) diff --git a/frontend/components/onboarding/AccountRecovery.tsx b/frontend/components/onboarding/AccountRecovery.tsx new file mode 100644 index 00000000..fcb286fd --- /dev/null +++ b/frontend/components/onboarding/AccountRecovery.tsx @@ -0,0 +1,47 @@ +import { FaCopy, FaFileDownload } from 'react-icons/fa' +import { Button } from '../common/Button' +import { Alert } from '../common/Alert' + +export const AccountRecovery = (props: { + mnemonic: string + onDownload: Function + onCopy: Function +}) => { + return ( +
+
+
Recovery kit
+

+ This recovery kit contains your account recovery phrase along with your account name, + email and other information to help you recover your account if you get locked out. +

+

+ Please download the recovery kit and keep it somewhere safe. You can also copy this + recovery kit and store it in a password manager. +

+ + +

+ If you forget your sudo password and lose your{' '} + recovery kit, your account cannot be recovered! +

+
+ +
+ + +
+
+
+ ) +} diff --git a/frontend/components/onboarding/AccountSeedChecker.tsx b/frontend/components/onboarding/AccountSeedChecker.tsx index 56fdbd58..997efa0a 100644 --- a/frontend/components/onboarding/AccountSeedChecker.tsx +++ b/frontend/components/onboarding/AccountSeedChecker.tsx @@ -29,8 +29,8 @@ export const AccountSeedChecker = (props: AccountSeedCheckProps) => { readOnly={isCorrect(index) ? true : false} //maxLength={25} className={clsx( - 'font-mono w-full', - isCorrect(index) && '!bg-emerald-400/20 !text-emerald-500 border' + 'font-mono w-full ph-no-capture', + isCorrect(index) && '!bg-emerald-400/20 !text-emerald-500' )} /> {isCorrect(index) && ( diff --git a/frontend/components/onboarding/AccountSeedGen.tsx b/frontend/components/onboarding/AccountSeedGen.tsx deleted file mode 100644 index 0dc4d6be..00000000 --- a/frontend/components/onboarding/AccountSeedGen.tsx +++ /dev/null @@ -1,37 +0,0 @@ -import { copyToClipBoard } from '@/utils/clipboard' -import { FaCopy } from 'react-icons/fa' -import { toast } from 'react-toastify' -import { Button } from '../common/Button' - -export const AccountSeedGen = (props: { mnemonic: string }) => { - const handleCopyClick = async () => { - const copied = await copyToClipBoard(props.mnemonic) - copied ? toast.success('Copied to clipboard') : toast.error('Failed to copy') - } - - return ( -
-
- -
- {props.mnemonic.split(' ').map((word: string, index: number) => ( -
- {index + 1} - {word} - ***** -
- ))} -
- ) -} diff --git a/frontend/components/onboarding/Stepper.tsx b/frontend/components/onboarding/Stepper.tsx index b4084679..f2a5d7c8 100644 --- a/frontend/components/onboarding/Stepper.tsx +++ b/frontend/components/onboarding/Stepper.tsx @@ -5,7 +5,7 @@ export type Step = { name: string icon: React.ReactNode title: string - description: string + description: React.ReactNode } interface StepperProps { @@ -78,7 +78,7 @@ export const Stepper = (props: StepperProps) => {
{props.steps[props.activeStep].title}
-

+

{props.steps[props.activeStep].description}

diff --git a/frontend/components/users/RoleLabel.tsx b/frontend/components/users/RoleLabel.tsx new file mode 100644 index 00000000..bd2f2ff7 --- /dev/null +++ b/frontend/components/users/RoleLabel.tsx @@ -0,0 +1,25 @@ +import clsx from 'clsx' + +export const RoleLabel = (props: { role: string }) => { + const role = props.role.toLowerCase() + + const roleStyle = () => { + if (role === 'dev') + return 'ring-neutral-500/40 bg-neutral-500/40 text-black dark:bg-zinc-800 dark:text-neutral-500' + if (role === 'admin') + return 'ring-emerald-400/10 bg-emerald-400 text-black dark:bg-zinc-800 dark:text-emerald-400' + if (role === 'owner') + return 'ring-amber-400/10 bg-amber-400 text-black dark:bg-zinc-800 dark:text-amber-400' + } + + return ( + + {role} + + ) +} diff --git a/frontend/contexts/keyringContext.tsx b/frontend/contexts/keyringContext.tsx new file mode 100644 index 00000000..13b6bca6 --- /dev/null +++ b/frontend/contexts/keyringContext.tsx @@ -0,0 +1,31 @@ +import { OrganisationKeyring } from '@/utils/auth' +import { createContext, useContext, useEffect, useState } from 'react' +import { organisationContext } from './organisationContext' + +interface KeyringContextValue { + keyring: OrganisationKeyring | null + setKeyring: (keyring: OrganisationKeyring) => void +} + +export const KeyringContext = createContext({ + keyring: null, + setKeyring: () => {}, +}) + +interface KeyringProviderProps { + children: React.ReactNode +} + +export const KeyringProvider: React.FC = ({ children }) => { + const [keyring, setKeyring] = useState(null) + + const { activeOrganisation } = useContext(organisationContext) + + useEffect(() => { + setKeyring(null) + }, [activeOrganisation]) + + return ( + {children} + ) +} diff --git a/frontend/contexts/organisationContext.tsx b/frontend/contexts/organisationContext.tsx new file mode 100644 index 00000000..8fab2be2 --- /dev/null +++ b/frontend/contexts/organisationContext.tsx @@ -0,0 +1,97 @@ +import { OrganisationType } from '@/apollo/graphql' +import { createContext, useEffect, useState } from 'react' +import GetOrganisations from '@/graphql/queries/getOrganisations.gql' +import UpdateWrappedSecrets from '@/graphql/mutations/organisation/updateUserWrappedSecrets.gql' +import { useLazyQuery, useMutation, useQuery } from '@apollo/client' +import { useSession } from 'next-auth/react' +import { getLocalKeyring } from '@/utils/localStorage' +import posthog from 'posthog-js' + +interface OrganisationContextValue { + activeOrganisation: OrganisationType | null + organisations: OrganisationType[] | null + setActiveOrganisation: (organisation: OrganisationType) => void + loading: boolean +} + +export const organisationContext = createContext({ + activeOrganisation: null, + organisations: null, + setActiveOrganisation: () => {}, + loading: true, +}) + +interface OrganisationProviderProps { + children: React.ReactNode +} + +export const OrganisationProvider: React.FC = ({ children }) => { + const [getOrgs, { data: orgsData, loading: queryLoading }] = useLazyQuery(GetOrganisations) + + const [updateWrappedSecrets] = useMutation(UpdateWrappedSecrets) + + const { data: session } = useSession() + + const [organisation, setOrganisation] = useState(null) + + const [loading, setLoading] = useState(true) + + const { organisations } = orgsData ?? { organisations: null } + + useEffect(() => { + if (session && organisation) { + if (session.user?.email) + posthog.identify(organisation.memberId!, { + email: session.user.email, + name: session.user.name, + organisation: organisation.name, + }) + } else posthog.reset() + }, [organisation, session]) + + useEffect(() => { + if (session?.user?.email) getOrgs() + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [session]) + + useEffect(() => { + if (organisation === null && orgsData) { + setOrganisation(orgsData.organisations[0]) + + orgsData.organisations.forEach((org: OrganisationType) => { + // Update wrapped secrets on the backend if they are blank + if (org.keyring === '' || org.recovery === '') { + const localKeyring = getLocalKeyring(session?.user?.email!, org.id) + + if (localKeyring?.keyring && localKeyring?.recovery) { + updateWrappedSecrets({ + variables: { + orgId: org.id, + wrappedKeyring: localKeyring.keyring, + wrappedRecovery: localKeyring.recovery, + }, + }) + } + } + }) + } + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [organisation, orgsData]) + + useEffect(() => { + setLoading(queryLoading) + }, [queryLoading]) + + return ( + + {children} + + ) +} diff --git a/frontend/ee/LICENSE b/frontend/ee/LICENSE index 5e43b4ed..6343eec5 100644 --- a/frontend/ee/LICENSE +++ b/frontend/ee/LICENSE @@ -9,13 +9,13 @@ and are in compliance with, the Phase Subscription Terms of Service, available at https://phase.dev/legal/terms, or other agreement governing the use of the Software, as agreed by you and Phase, and otherwise have a valid Phase Console Enterprise License for the -correct number of applications & user seats. Subject to the foregoing sentence, you are free to +correct number of user seats. Subject to the foregoing sentence, you are free to modify this Software and publish patches to the Software. You agree that Phase and/or its licensors (as applicable) retain all right, title and interest in and to all such modifications and/or patches, and all such modifications and/or patches may only be used, copied, modified, displayed, distributed, or otherwise exploited with a valid Phase Console Enterprise subscription for the correct number of -applications & user seats. Notwithstanding the foregoing, you may copy and modify +user seats. Notwithstanding the foregoing, you may copy and modify the Software for development and testing purposes, without requiring a subscription. You agree that Phase and/or its licensors (as applicable) retain all right, title and interest in and to all such modifications. You are not diff --git a/frontend/graphql/mutations/apps/addAppMember.gql b/frontend/graphql/mutations/apps/addAppMember.gql new file mode 100644 index 00000000..04d236b3 --- /dev/null +++ b/frontend/graphql/mutations/apps/addAppMember.gql @@ -0,0 +1,7 @@ +mutation AddMemberToApp($memberId: ID!, $appId: ID!, $envKeys: [EnvironmentKeyInput]) { + addAppMember(memberId: $memberId, appId: $appId, envKeys: $envKeys) { + app { + id + } + } +} diff --git a/frontend/graphql/mutations/apps/removeAppMember.gql b/frontend/graphql/mutations/apps/removeAppMember.gql new file mode 100644 index 00000000..61251f9d --- /dev/null +++ b/frontend/graphql/mutations/apps/removeAppMember.gql @@ -0,0 +1,7 @@ +mutation RemoveMemberFromApp($memberId: ID!, $appId: ID!) { + removeAppMember(memberId: $memberId, appId: $appId) { + app { + id + } + } +} diff --git a/frontend/graphql/mutations/apps/updateEnvScope.gql b/frontend/graphql/mutations/apps/updateEnvScope.gql new file mode 100644 index 00000000..3981df89 --- /dev/null +++ b/frontend/graphql/mutations/apps/updateEnvScope.gql @@ -0,0 +1,7 @@ +mutation UpdateEnvScope($memberId: ID!, $appId: ID!, $envKeys: [EnvironmentKeyInput]) { + updateMemberEnvironmentScope(memberId: $memberId, appId: $appId, envKeys: $envKeys) { + app { + id + } + } +} diff --git a/frontend/apollo/mutations/createApp.gql b/frontend/graphql/mutations/createApp.gql similarity index 94% rename from frontend/apollo/mutations/createApp.gql rename to frontend/graphql/mutations/createApp.gql index 812a5cfb..008c7940 100644 --- a/frontend/apollo/mutations/createApp.gql +++ b/frontend/graphql/mutations/createApp.gql @@ -1,4 +1,4 @@ -mutation CreateApp( +mutation CreateApplication( $id: ID! $organisationId: ID! $name: String! diff --git a/frontend/graphql/mutations/createOrganisation.gql b/frontend/graphql/mutations/createOrganisation.gql new file mode 100644 index 00000000..08405937 --- /dev/null +++ b/frontend/graphql/mutations/createOrganisation.gql @@ -0,0 +1,9 @@ +mutation CreateOrg($id: ID!, $name: String!, $identityKey: String!, $wrappedKeyring: String!, $wrappedRecovery: String!) { + createOrganisation(id: $id, name: $name, identityKey: $identityKey, wrappedKeyring: $wrappedKeyring, wrappedRecovery: $wrappedRecovery) { + organisation { + id + name + createdAt + } + } +} diff --git a/frontend/apollo/mutations/deleteApp.gql b/frontend/graphql/mutations/deleteApp.gql similarity index 58% rename from frontend/apollo/mutations/deleteApp.gql rename to frontend/graphql/mutations/deleteApp.gql index b88964f9..674b6707 100644 --- a/frontend/apollo/mutations/deleteApp.gql +++ b/frontend/graphql/mutations/deleteApp.gql @@ -1,4 +1,4 @@ -mutation DeleteApp($id: ID!) { +mutation DeleteApplication($id: ID!) { deleteApp(id: $id) { app { id diff --git a/frontend/graphql/mutations/environments/createEnvironment.gql b/frontend/graphql/mutations/environments/createEnvironment.gql new file mode 100644 index 00000000..ec020f4a --- /dev/null +++ b/frontend/graphql/mutations/environments/createEnvironment.gql @@ -0,0 +1,11 @@ + +mutation CreateEnv($input: EnvironmentInput!) { + createEnvironment(environmentData: $input) { + environment { + id + name + createdAt + identityKey + } + } +} diff --git a/frontend/graphql/mutations/environments/createEnvironmentKey.gql b/frontend/graphql/mutations/environments/createEnvironmentKey.gql new file mode 100644 index 00000000..11801aba --- /dev/null +++ b/frontend/graphql/mutations/environments/createEnvironmentKey.gql @@ -0,0 +1,8 @@ +mutation CreateEnvKey($envId: ID!, $userId: ID,$wrappedSeed: String!, $wrappedSalt: String!, $identityKey: String!) { + createEnvironmentKey(envId: $envId, userId: $userId, wrappedSeed: $wrappedSeed, wrappedSalt: $wrappedSalt, identityKey: $identityKey) { + environmentKey { + id + createdAt + } + } +} diff --git a/frontend/graphql/mutations/environments/createEnvironmentToken.gql b/frontend/graphql/mutations/environments/createEnvironmentToken.gql new file mode 100644 index 00000000..f0d55a1d --- /dev/null +++ b/frontend/graphql/mutations/environments/createEnvironmentToken.gql @@ -0,0 +1,8 @@ +mutation CreateEnvToken($envId: ID!, $name: String!, $identityKey: String!, $token: String!, $wrappedKeyShare: String!) { + createEnvironmentToken(envId: $envId, name: $name, identityKey: $identityKey, token: $token, wrappedKeyShare: $wrappedKeyShare) { + environmentToken { + id + createdAt + } + } +} diff --git a/frontend/graphql/mutations/environments/createSecret.gql b/frontend/graphql/mutations/environments/createSecret.gql new file mode 100644 index 00000000..ab9d0b45 --- /dev/null +++ b/frontend/graphql/mutations/environments/createSecret.gql @@ -0,0 +1,10 @@ +mutation CreateNewSecret($newSecret: SecretInput!) { + createSecret(secretData: $newSecret) { + secret { + id + key + value + createdAt + } + } +} diff --git a/frontend/graphql/mutations/environments/createSecretTag.gql b/frontend/graphql/mutations/environments/createSecretTag.gql new file mode 100644 index 00000000..b6bcbd4e --- /dev/null +++ b/frontend/graphql/mutations/environments/createSecretTag.gql @@ -0,0 +1,7 @@ +mutation CreateNewSecretTag($orgId: ID!, $name: String!, $color: String!) { + createSecretTag(orgId: $orgId, name: $name, color: $color) { + tag { + id + } + } +} diff --git a/frontend/graphql/mutations/environments/createServiceToken.gql b/frontend/graphql/mutations/environments/createServiceToken.gql new file mode 100644 index 00000000..1a802060 --- /dev/null +++ b/frontend/graphql/mutations/environments/createServiceToken.gql @@ -0,0 +1,9 @@ +mutation CreateNewServiceToken($appId: ID!, $environmentKeys: [EnvironmentKeyInput], $identityKey: String!, $token: String!, $wrappedKeyShare: String!, $name: String!, $expiry: BigInt) { + createServiceToken(appId: $appId, environmentKeys: $environmentKeys, identityKey: $identityKey, token: $token, wrappedKeyShare: $wrappedKeyShare, name: $name, expiry: $expiry) { + serviceToken { + id + createdAt + expiresAt + } + } +} diff --git a/frontend/graphql/mutations/environments/deleteSecret.gql b/frontend/graphql/mutations/environments/deleteSecret.gql new file mode 100644 index 00000000..dcc5032f --- /dev/null +++ b/frontend/graphql/mutations/environments/deleteSecret.gql @@ -0,0 +1,7 @@ +mutation DeleteSecretOp($id: ID!) { + deleteSecret(id: $id) { + secret { + id + } + } +} diff --git a/frontend/graphql/mutations/environments/deleteServiceToken.gql b/frontend/graphql/mutations/environments/deleteServiceToken.gql new file mode 100644 index 00000000..ed6d747a --- /dev/null +++ b/frontend/graphql/mutations/environments/deleteServiceToken.gql @@ -0,0 +1,5 @@ +mutation RevokeServiceToken($tokenId: ID!) { + deleteServiceToken(tokenId: $tokenId) { + ok + } +} diff --git a/frontend/graphql/mutations/environments/editSecret.gql b/frontend/graphql/mutations/environments/editSecret.gql new file mode 100644 index 00000000..1df5f432 --- /dev/null +++ b/frontend/graphql/mutations/environments/editSecret.gql @@ -0,0 +1,8 @@ +mutation UpdateSecret($id: ID!, $secretData: SecretInput!) { + editSecret(id: $id, secretData: $secretData) { + secret { + id + updatedAt + } + } +} diff --git a/frontend/graphql/mutations/environments/initAppEnvironments.gql b/frontend/graphql/mutations/environments/initAppEnvironments.gql new file mode 100644 index 00000000..aebaba50 --- /dev/null +++ b/frontend/graphql/mutations/environments/initAppEnvironments.gql @@ -0,0 +1,33 @@ +mutation InitAppEnvironments( + $devEnv: EnvironmentInput! + $stagingEnv: EnvironmentInput! + $prodEnv: EnvironmentInput! + $devAdminKeys: [EnvironmentKeyInput], + $stagAdminKeys: [EnvironmentKeyInput] + $prodAdminKeys: [EnvironmentKeyInput] +) { + devEnvironment: createEnvironment(environmentData: $devEnv, adminKeys: $devAdminKeys) { + environment { + id + name + createdAt + identityKey + } + } + stagingEnvironment: createEnvironment(environmentData: $stagingEnv, adminKeys: $stagAdminKeys) { + environment { + id + name + createdAt + identityKey + } + } + prodEnvironment: createEnvironment(environmentData: $prodEnv, adminKeys: $prodAdminKeys) { + environment { + id + name + createdAt + identityKey + } + } +} diff --git a/frontend/graphql/mutations/environments/readSecret.gql b/frontend/graphql/mutations/environments/readSecret.gql new file mode 100644 index 00000000..5816d2fb --- /dev/null +++ b/frontend/graphql/mutations/environments/readSecret.gql @@ -0,0 +1,5 @@ +mutation LogSecretRead($id: ID!) { + readSecret(id: $id) { + ok + } +} diff --git a/frontend/graphql/mutations/organisation/acceptInvite.gql b/frontend/graphql/mutations/organisation/acceptInvite.gql new file mode 100644 index 00000000..adf25d69 --- /dev/null +++ b/frontend/graphql/mutations/organisation/acceptInvite.gql @@ -0,0 +1,10 @@ +mutation AcceptOrganisationInvite($orgId: ID!, $identityKey: String!, $wrappedKeyring: String!, $wrappedRecovery: String!, $inviteId: ID!) { + createOrganisationMember(orgId: $orgId, identityKey: $identityKey, wrappedKeyring: $wrappedKeyring, wrappedRecovery: $wrappedRecovery, inviteId: $inviteId) { + orgMember{ + id + email + createdAt + role + } + } +} diff --git a/frontend/graphql/mutations/organisation/deleteInvite.gql b/frontend/graphql/mutations/organisation/deleteInvite.gql new file mode 100644 index 00000000..aa19e849 --- /dev/null +++ b/frontend/graphql/mutations/organisation/deleteInvite.gql @@ -0,0 +1,5 @@ +mutation DeleteOrgInvite($inviteId: ID!) { + deleteInvitation(inviteId: $inviteId) { + ok + } +} diff --git a/frontend/graphql/mutations/organisation/deleteOrgMember.gql b/frontend/graphql/mutations/organisation/deleteOrgMember.gql new file mode 100644 index 00000000..08495eba --- /dev/null +++ b/frontend/graphql/mutations/organisation/deleteOrgMember.gql @@ -0,0 +1,5 @@ +mutation RemoveMember($memberId: ID!) { + deleteOrganisationMember(memberId: $memberId) { + ok + } +} diff --git a/frontend/graphql/mutations/organisation/inviteNewMember.gql b/frontend/graphql/mutations/organisation/inviteNewMember.gql new file mode 100644 index 00000000..3480cb14 --- /dev/null +++ b/frontend/graphql/mutations/organisation/inviteNewMember.gql @@ -0,0 +1,7 @@ +mutation InviteMember($orgId: ID!, $email: String!, $apps: [String], $role: String) { + inviteOrganisationMember(orgId: $orgId, email: $email, apps: $apps, role: $role) { + invite { + id + } + } +} diff --git a/frontend/graphql/mutations/organisation/updateOrgMemberRole.gql b/frontend/graphql/mutations/organisation/updateOrgMemberRole.gql new file mode 100644 index 00000000..ba2c2934 --- /dev/null +++ b/frontend/graphql/mutations/organisation/updateOrgMemberRole.gql @@ -0,0 +1,8 @@ +mutation UpdateMemberRole($memberId: ID!, $role: String!) { + updateOrganisationMemberRole(memberId: $memberId, role: $role) { + orgMember { + id + role + } + } +} diff --git a/frontend/graphql/mutations/organisation/updateUserWrappedSecrets.gql b/frontend/graphql/mutations/organisation/updateUserWrappedSecrets.gql new file mode 100644 index 00000000..c1991600 --- /dev/null +++ b/frontend/graphql/mutations/organisation/updateUserWrappedSecrets.gql @@ -0,0 +1,7 @@ +mutation UpdateWrappedSecrets($orgId: ID!, $wrappedKeyring: String!, $wrappedRecovery: String!) { + updateMemberWrappedSecrets(orgId: $orgId, wrappedKeyring: $wrappedKeyring, wrappedRecovery: $wrappedRecovery) { + orgMember { + id + } + } +} diff --git a/frontend/apollo/mutations/rotateAppKeys.gql b/frontend/graphql/mutations/rotateAppKeys.gql similarity index 58% rename from frontend/apollo/mutations/rotateAppKeys.gql rename to frontend/graphql/mutations/rotateAppKeys.gql index c0a6123e..95b8ede3 100644 --- a/frontend/apollo/mutations/rotateAppKeys.gql +++ b/frontend/graphql/mutations/rotateAppKeys.gql @@ -1,4 +1,4 @@ -mutation RotateAppKeys($id: ID!, $appToken: String!, $wrappedKeyShare: String!) { +mutation RotateAppKey($id: ID!, $appToken: String!, $wrappedKeyShare: String!) { rotateAppKeys(id: $id, appToken: $appToken, wrappedKeyShare: $wrappedKeyShare) { app { id diff --git a/frontend/graphql/mutations/users/createUserToken.gql b/frontend/graphql/mutations/users/createUserToken.gql new file mode 100644 index 00000000..a1860641 --- /dev/null +++ b/frontend/graphql/mutations/users/createUserToken.gql @@ -0,0 +1,5 @@ +mutation CreateNewUserToken($orgId: ID!, $name: String!, $identityKey: String!, $token: String!, $wrappedKeyShare: String!, $expiry: BigInt) { + createUserToken(orgId: $orgId, name: $name, identityKey: $identityKey, token: $token, wrappedKeyShare: $wrappedKeyShare, expiry: $expiry) { + ok + } +} diff --git a/frontend/graphql/mutations/users/deleteUserToken.gql b/frontend/graphql/mutations/users/deleteUserToken.gql new file mode 100644 index 00000000..9586dc80 --- /dev/null +++ b/frontend/graphql/mutations/users/deleteUserToken.gql @@ -0,0 +1,5 @@ +mutation RevokeUserToken($tokenId: ID!) { + deleteUserToken(tokenId: $tokenId) { + ok + } +} diff --git a/frontend/graphql/queries/apps/getAppMembers.gql b/frontend/graphql/queries/apps/getAppMembers.gql new file mode 100644 index 00000000..73650864 --- /dev/null +++ b/frontend/graphql/queries/apps/getAppMembers.gql @@ -0,0 +1,11 @@ +query GetAppMembers($appId: ID!) { + appUsers(appId: $appId) { + id + identityKey + email + fullName + avatarUrl + createdAt + role + } +} diff --git a/frontend/apollo/queries/getAppActivityChart.gql b/frontend/graphql/queries/getAppActivityChart.gql similarity index 100% rename from frontend/apollo/queries/getAppActivityChart.gql rename to frontend/graphql/queries/getAppActivityChart.gql diff --git a/frontend/apollo/queries/getAppDetail.gql b/frontend/graphql/queries/getAppDetail.gql similarity index 100% rename from frontend/apollo/queries/getAppDetail.gql rename to frontend/graphql/queries/getAppDetail.gql diff --git a/frontend/graphql/queries/getAppKmsLogs.gql b/frontend/graphql/queries/getAppKmsLogs.gql new file mode 100644 index 00000000..d7744289 --- /dev/null +++ b/frontend/graphql/queries/getAppKmsLogs.gql @@ -0,0 +1,15 @@ +query GetAppKmsLogs($appId: ID!, $start: BigInt, $end: BigInt) { + logs(appId: $appId, start: $start, end: $end) { + kms { + id + timestamp + phaseNode + eventType + ipAddress + country + city + phSize + } + } + kmsLogsCount(appId: $appId) +} diff --git a/frontend/apollo/queries/getApps.gql b/frontend/graphql/queries/getApps.gql similarity index 100% rename from frontend/apollo/queries/getApps.gql rename to frontend/graphql/queries/getApps.gql diff --git a/frontend/apollo/queries/getOrganisations.gql b/frontend/graphql/queries/getOrganisations.gql similarity index 68% rename from frontend/apollo/queries/getOrganisations.gql rename to frontend/graphql/queries/getOrganisations.gql index 7be8f166..36235271 100644 --- a/frontend/apollo/queries/getOrganisations.gql +++ b/frontend/graphql/queries/getOrganisations.gql @@ -5,5 +5,9 @@ query GetOrganisations { identityKey createdAt plan + role + memberId + keyring + recovery } } diff --git a/frontend/graphql/queries/organisation/getInvites.gql b/frontend/graphql/queries/organisation/getInvites.gql new file mode 100644 index 00000000..022eb687 --- /dev/null +++ b/frontend/graphql/queries/organisation/getInvites.gql @@ -0,0 +1,13 @@ +query GetInvites($orgId: ID!) { + organisationInvites(orgId: $orgId) { + id + createdAt + expiresAt + invitedBy { + email + fullName + self + } + inviteeEmail + } +} diff --git a/frontend/graphql/queries/organisation/getOrganisationAdminsAndSelf.gql b/frontend/graphql/queries/organisation/getOrganisationAdminsAndSelf.gql new file mode 100644 index 00000000..c29c6c16 --- /dev/null +++ b/frontend/graphql/queries/organisation/getOrganisationAdminsAndSelf.gql @@ -0,0 +1,8 @@ +query GetOrganisationAdminsAndSelf($organisationId: ID!) { + organisationAdminsAndSelf(organisationId: $organisationId) { + id + role + identityKey + self + } +} diff --git a/frontend/graphql/queries/organisation/getOrganisationMembers.gql b/frontend/graphql/queries/organisation/getOrganisationMembers.gql new file mode 100644 index 00000000..dee108da --- /dev/null +++ b/frontend/graphql/queries/organisation/getOrganisationMembers.gql @@ -0,0 +1,12 @@ +query GetOrganisationMembers($organisationId: ID!, $role: [String]) { + organisationMembers(organisationId: $organisationId, role: $role) { + id + role + identityKey + email + fullName + avatarUrl + createdAt + self + } +} diff --git a/frontend/graphql/queries/organisation/validateOrganisationInvite.gql b/frontend/graphql/queries/organisation/validateOrganisationInvite.gql new file mode 100644 index 00000000..eb0ea0b9 --- /dev/null +++ b/frontend/graphql/queries/organisation/validateOrganisationInvite.gql @@ -0,0 +1,18 @@ +query VerifyInvite($inviteId: ID!) { + validateInvite(inviteId: $inviteId) { + id + organisation { + id + name + } + inviteeEmail + invitedBy { + email + } + apps { + id + name + } + + } +} diff --git a/frontend/graphql/queries/secrets/getAppEnvironments.gql b/frontend/graphql/queries/secrets/getAppEnvironments.gql new file mode 100644 index 00000000..2b0fa596 --- /dev/null +++ b/frontend/graphql/queries/secrets/getAppEnvironments.gql @@ -0,0 +1,11 @@ +query GetAppEnvironments($appId: ID!, $memberId: ID) { + appEnvironments(appId: $appId, environmentId: null, memberId: $memberId) { + id + name + envType + identityKey + wrappedSeed + wrappedSalt + createdAt + } +} diff --git a/frontend/graphql/queries/secrets/getAppSecretsLogs.gql b/frontend/graphql/queries/secrets/getAppSecretsLogs.gql new file mode 100644 index 00000000..9360d985 --- /dev/null +++ b/frontend/graphql/queries/secrets/getAppSecretsLogs.gql @@ -0,0 +1,45 @@ +query GetAppSecretsLogs($appId: ID!, $start: BigInt, $end: BigInt) { + logs(appId: $appId, start: $start, end: $end) { + secrets { + id + key + value + tags { + id + name + color + } + version + comment + timestamp + ipAddress + userAgent + user { + email + username + fullName + avatarUrl + } + eventType + environment { + id + envType + name + + } + secret { + id + } + } + } + secretsLogsCount(appId: $appId) + environmentKeys(appId: $appId) { + id + identityKey + wrappedSeed + wrappedSalt + environment { + id + } + } +} diff --git a/frontend/graphql/queries/secrets/getEnvironmentKey.gql b/frontend/graphql/queries/secrets/getEnvironmentKey.gql new file mode 100644 index 00000000..c37efff0 --- /dev/null +++ b/frontend/graphql/queries/secrets/getEnvironmentKey.gql @@ -0,0 +1,8 @@ +query GetEnvironmentKey($envId: ID!, $appId: ID!) { + environmentKeys(environmentId: $envId, appId: $appId) { + id + identityKey + wrappedSeed + wrappedSalt + } +} diff --git a/frontend/graphql/queries/secrets/getEnvironmentTokens.gql b/frontend/graphql/queries/secrets/getEnvironmentTokens.gql new file mode 100644 index 00000000..44c31e5f --- /dev/null +++ b/frontend/graphql/queries/secrets/getEnvironmentTokens.gql @@ -0,0 +1,8 @@ +query GetEnvironmentTokens($envId: ID!) { + environmentTokens(environmentId: $envId) { + id + name + wrappedKeyShare + createdAt + } +} diff --git a/frontend/graphql/queries/secrets/getSecretKVs.gql b/frontend/graphql/queries/secrets/getSecretKVs.gql new file mode 100644 index 00000000..f754406e --- /dev/null +++ b/frontend/graphql/queries/secrets/getSecretKVs.gql @@ -0,0 +1,13 @@ +query GetEnvSecretsKV($envId: ID!) { + secrets(envId: $envId) { + id + key + value + } + environmentKeys(environmentId: $envId) { + id + identityKey + wrappedSeed + wrappedSalt + } +} diff --git a/frontend/graphql/queries/secrets/getSecretTags.gql b/frontend/graphql/queries/secrets/getSecretTags.gql new file mode 100644 index 00000000..679e9d1e --- /dev/null +++ b/frontend/graphql/queries/secrets/getSecretTags.gql @@ -0,0 +1,7 @@ +query GetSecretTags($orgId: ID!) { + secretTags(orgId: $orgId) { + id + name + color + } +} diff --git a/frontend/graphql/queries/secrets/getSecrets.gql b/frontend/graphql/queries/secrets/getSecrets.gql new file mode 100644 index 00000000..7e7f7218 --- /dev/null +++ b/frontend/graphql/queries/secrets/getSecrets.gql @@ -0,0 +1,48 @@ +query GetSecrets($appId: ID!, $envId: ID!) { + secrets(envId: $envId) { + id + key + value + tags { + id + name + color + } + comment + createdAt + history { + id + key + value + tags { + id + name + color + } + version + comment + timestamp + ipAddress + userAgent + user { + email + username + fullName + avatarUrl + } + eventType + } + } + appEnvironments(appId: $appId, environmentId: $envId) { + id + name + envType + identityKey + } + environmentKeys(appId: $appId, environmentId: $envId) { + id + identityKey + wrappedSeed + wrappedSalt + } +} diff --git a/frontend/graphql/queries/secrets/getServiceTokens.gql b/frontend/graphql/queries/secrets/getServiceTokens.gql new file mode 100644 index 00000000..2d7f5f39 --- /dev/null +++ b/frontend/graphql/queries/secrets/getServiceTokens.gql @@ -0,0 +1,17 @@ +query GetServiceTokens($appId: ID!) { + serviceTokens(appId: $appId) { + id + name + createdAt + createdBy { + fullName + avatarUrl + self + } + expiresAt + keys { + id + + } + } +} diff --git a/frontend/graphql/queries/users/getUserTokens.gql b/frontend/graphql/queries/users/getUserTokens.gql new file mode 100644 index 00000000..140b174f --- /dev/null +++ b/frontend/graphql/queries/users/getUserTokens.gql @@ -0,0 +1,9 @@ +query GetUserTokens($organisationId: ID!) { + userTokens(organisationId: $organisationId) { + id + name + wrappedKeyShare + createdAt + expiresAt + } +} diff --git a/frontend/next.config.js b/frontend/next.config.js index 9d0435d3..2e88850e 100644 --- a/frontend/next.config.js +++ b/frontend/next.config.js @@ -1,14 +1,14 @@ /** @type {import('next').NextConfig} */ const ContentSecurityPolicy = ` default-src 'self'; - script-src 'self' 'unsafe-inline' 'wasm-unsafe-eval'; + script-src 'self' 'unsafe-inline' 'wasm-unsafe-eval' https://app.posthog.com; style-src 'self' 'unsafe-inline'; object-src 'none'; base-uri 'self'; - connect-src 'self' data: https://*.phase.dev https://phase.statuspage.io/api/v2/status.json; + connect-src 'self' data: http://127.0.0.1:* https://*.phase.dev https://phase.statuspage.io/api/v2/status.json https://app.posthog.com; font-src 'self'; frame-src 'self'; - img-src 'self' https://lh3.googleusercontent.com https://avatars.githubusercontent.com https://secure.gravatar.com https://gitlab.com; + img-src 'self' https://lh3.googleusercontent.com https://avatars.githubusercontent.com https://secure.gravatar.com https://gitlab.com; manifest-src 'self'; media-src 'self'; worker-src 'none'; diff --git a/frontend/package-lock.json b/frontend/package-lock.json index 452f8414..ef43ee0b 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -24,7 +24,7 @@ "eslint": "^8.34.0", "eslint-config-next": "13.1.6", "framer-motion": "^10.12.4", - "graphql": "^16.6.0", + "graphql": "^16.8.1", "graphql-tag": "^2.12.6", "install": "^0.13.0", "jsonwebtoken": "^9.0.0", @@ -44,7 +44,6 @@ "devDependencies": { "@graphql-codegen/cli": "3.2.2", "@graphql-codegen/client-preset": "2.1.1", - "@graphql-codegen/typescript": "^3.0.2", "@graphql-codegen/typescript-react-apollo": "^3.3.7", "@types/country-flag-icons": "^1.2.0", "@types/jsonwebtoken": "^9.0.1", @@ -5856,9 +5855,9 @@ "integrity": "sha512-bzh50DW9kTPM00T8y4o8vQg89Di9oLJVLW/KaOGIXJWP/iqCN6WKYkbNOF04vFLJhwcpYUh9ydh/+5vpOqV4YQ==" }, "node_modules/graphql": { - "version": "16.6.0", - "resolved": "https://registry.npmjs.org/graphql/-/graphql-16.6.0.tgz", - "integrity": "sha512-KPIBPDlW7NxrbT/eh4qPXz5FiFdL5UbaA0XUNz2Rp3Z3hqBSkbj0GVjwFDztsWVauZUWsbKHgMg++sk8UX0bkw==", + "version": "16.8.1", + "resolved": "https://registry.npmjs.org/graphql/-/graphql-16.8.1.tgz", + "integrity": "sha512-59LZHPdGZVh695Ud9lRzPBVTtlX9ZCV150Er2W43ro37wVof0ctenSaskPPjN7lVTIN8mSZt8PHUNKZuNQUuxw==", "engines": { "node": "^12.22.0 || ^14.16.0 || ^16.0.0 || >=17.0.0" } @@ -17564,9 +17563,9 @@ "integrity": "sha512-bzh50DW9kTPM00T8y4o8vQg89Di9oLJVLW/KaOGIXJWP/iqCN6WKYkbNOF04vFLJhwcpYUh9ydh/+5vpOqV4YQ==" }, "graphql": { - "version": "16.6.0", - "resolved": "https://registry.npmjs.org/graphql/-/graphql-16.6.0.tgz", - "integrity": "sha512-KPIBPDlW7NxrbT/eh4qPXz5FiFdL5UbaA0XUNz2Rp3Z3hqBSkbj0GVjwFDztsWVauZUWsbKHgMg++sk8UX0bkw==" + "version": "16.8.1", + "resolved": "https://registry.npmjs.org/graphql/-/graphql-16.8.1.tgz", + "integrity": "sha512-59LZHPdGZVh695Ud9lRzPBVTtlX9ZCV150Er2W43ro37wVof0ctenSaskPPjN7lVTIN8mSZt8PHUNKZuNQUuxw==" }, "graphql-config": { "version": "4.5.0", diff --git a/frontend/package.json b/frontend/package.json index 44de026d..4e874ea6 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -26,27 +26,28 @@ "eslint": "^8.34.0", "eslint-config-next": "13.1.6", "framer-motion": "^10.12.4", - "graphql": "^16.6.0", + "graphql": "^16.8.1", "graphql-tag": "^2.12.6", "install": "^0.13.0", "jsonwebtoken": "^9.0.0", + "jspdf": "^2.5.1", "libsodium-wrappers-sumo": "^0.7.11", "next": "13.1.6", "next-auth": "^4.20.1", "npm": "^9.5.0", + "posthog-js": "^1.87.5", "react": "18.2.0", "react-dom": "18.2.0", "react-icons": "^4.8.0", - "react-toastify": "^9.1.1", + "react-toastify": "^9.1.3", "reaviz": "^14.4.2", - "sass": "^1.59.3", + "sass": "^1.69.4", "typescript": "4.9.5", "zxcvbn": "^4.4.2" }, "devDependencies": { "@graphql-codegen/cli": "3.2.2", "@graphql-codegen/client-preset": "2.1.1", - "@graphql-codegen/typescript": "^3.0.2", "@graphql-codegen/typescript-react-apollo": "^3.3.7", "@types/country-flag-icons": "^1.2.0", "@types/jsonwebtoken": "^9.0.1", diff --git a/frontend/pages/api/auth/[...nextauth].ts b/frontend/pages/api/auth/[...nextauth].ts index e418c748..e3cb85ef 100644 --- a/frontend/pages/api/auth/[...nextauth].ts +++ b/frontend/pages/api/auth/[...nextauth].ts @@ -75,6 +75,10 @@ export const authOptions: NextAuthOptionsCallback = (_req, res) => { } try { + //get client user agent and ip + const userAgent = _req.headers['user-agent'] + const ip = _req.headers['x-forwarded-for'] + const response = await axios.post( UrlUtils.makeUrl( process.env.BACKEND_API_BASE!, @@ -83,7 +87,13 @@ export const authOptions: NextAuthOptionsCallback = (_req, res) => { account.provider ), loginPayload, - { withCredentials: true } + { + withCredentials: true, + headers: { + 'User-agent': userAgent, + 'X-forwarded-for': ip, + }, + } ) Object.entries(response.headers).forEach(([k, v]) => { @@ -102,7 +112,7 @@ export const authOptions: NextAuthOptionsCallback = (_req, res) => { }, }, pages: { - newUser: '/onboard', + newUser: '/signup', signIn: '/login', }, debug: process.env.DEBUG ? process.env.DEBUG === 'True' : false, diff --git a/frontend/public/assets/images/decrypt.svg b/frontend/public/assets/images/decrypt.svg deleted file mode 100644 index 43d5f263..00000000 --- a/frontend/public/assets/images/decrypt.svg +++ /dev/null @@ -1,154 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/frontend/public/assets/images/gradient-1.svg b/frontend/public/assets/images/gradient-1.svg deleted file mode 100644 index 08aa135e..00000000 --- a/frontend/public/assets/images/gradient-1.svg +++ /dev/null @@ -1,45 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/frontend/public/assets/images/gradient-purple.svg b/frontend/public/assets/images/gradient-purple.svg deleted file mode 100644 index 8d299084..00000000 --- a/frontend/public/assets/images/gradient-purple.svg +++ /dev/null @@ -1,9 +0,0 @@ - - - - - - - - - diff --git a/frontend/public/assets/images/grello-gradient.svg b/frontend/public/assets/images/grello-gradient.svg deleted file mode 100644 index dd07a8e7..00000000 --- a/frontend/public/assets/images/grello-gradient.svg +++ /dev/null @@ -1,9 +0,0 @@ - - - - - - - - - diff --git a/frontend/public/assets/images/hero-bg.webp b/frontend/public/assets/images/hero-bg.webp deleted file mode 100644 index 029183ca..00000000 Binary files a/frontend/public/assets/images/hero-bg.webp and /dev/null differ diff --git a/frontend/public/assets/images/logo.png b/frontend/public/assets/images/logo.png new file mode 100644 index 00000000..7ff7fbea Binary files /dev/null and b/frontend/public/assets/images/logo.png differ diff --git a/frontend/public/assets/images/meta.png b/frontend/public/assets/images/meta.png deleted file mode 100644 index c9f42cc7..00000000 Binary files a/frontend/public/assets/images/meta.png and /dev/null differ diff --git a/frontend/public/assets/images/sandbox.svg b/frontend/public/assets/images/sandbox.svg deleted file mode 100644 index c42fe442..00000000 --- a/frontend/public/assets/images/sandbox.svg +++ /dev/null @@ -1,16 +0,0 @@ - - - - - - - - - - - - - - - - diff --git a/frontend/scripts/replace-variable.sh b/frontend/scripts/replace-variable.sh index 32fd7dcb..67ed2736 100644 --- a/frontend/scripts/replace-variable.sh +++ b/frontend/scripts/replace-variable.sh @@ -16,4 +16,5 @@ while read file; do sed -i "s|BAKED_NEXT_PUBLIC_BACKEND_API_BASE|$NEXT_PUBLIC_BACKEND_API_BASE|g" "$file" sed -i "s|BAKED_NEXT_PUBLIC_NEXTAUTH_PROVIDERS|$NEXT_PUBLIC_NEXTAUTH_PROVIDERS|g" "$file" sed -i "s|BAKED_NEXT_PUBLIC_APP_HOST|$APP_HOST|g" "$file" + sed -i "s|BAKED_NEXT_PUBLIC_POSTHOG_KEY|$NEXT_PUBLIC_POSTHOG_KEY|g" "$file" done \ No newline at end of file diff --git a/frontend/utils/auth.ts b/frontend/utils/auth.ts index 3a107f2c..e5524272 100644 --- a/frontend/utils/auth.ts +++ b/frontend/utils/auth.ts @@ -1,5 +1,17 @@ import jwt from 'jsonwebtoken' import _sodium from 'libsodium-wrappers-sumo' +import { getLocalKeyring } from './localStorage' + +export type OrganisationKeyring = { + symmetricKey: string + publicKey: string + privateKey: string +} + +type AppKeyring = { + publicKey: string + privateKey: string +} export namespace JwtUtils { export const isJwtExpired = (token: string) => { @@ -43,23 +55,6 @@ export namespace UrlUtils { } export namespace cryptoUtils { - type OrganisationKeyring = { - symmetricKey: string - publicKey: string - privateKey: string - } - - type AppKeyring = { - publicKey: string - privateKey: string - } - - type ShamirShare = { - bits: number - id: number - data: string - } - /** * Returns 16 bytes from an input string that can be used as a salt for Argon2 * @@ -82,7 +77,7 @@ export namespace cryptoUtils { * @param {Uint8Array} key * @returns {Promise} - Ciphertext with appended nonce */ - const encryptRaw = async (plaintext: string, key: Uint8Array): Promise => { + export const encryptRaw = async (plaintext: string, key: Uint8Array): Promise => { await _sodium.ready const sodium = _sodium @@ -126,6 +121,36 @@ export namespace cryptoUtils { return plaintext } + /** + * Encrypts a single string with the given key. Returns the ciphertext as a base64 string + * + * @param {string} plaintext - Plaintext string to encrypt + * @param {Uint8Array} key - Symmetric encryption key + * @returns {string} + */ + export const encryptString = async (plaintext: string, key: Uint8Array) => { + await _sodium.ready + const sodium = _sodium + + return sodium.to_base64(await encryptRaw(plaintext, key), sodium.base64_variants.ORIGINAL) + } + + /** + * Decrypts a single base64 ciphertext string with the given key. Returns the plaintext as a string + * + * @param cipherText - base64 string ciphertext with appended nonce + * @param key - Symmetric encryption key + * @returns {string} + */ + export const decryptString = async (cipherText: string, key: Uint8Array) => { + await _sodium.ready + const sodium = _sodium + + return sodium.to_string( + await decryptRaw(sodium.from_base64(cipherText, sodium.base64_variants.ORIGINAL), key) + ) + } + /** * Computes the account recovery key from the mnemonic phrase and orgId. * Note: This can take between 15-20 seconds to resolve @@ -262,6 +287,25 @@ export namespace cryptoUtils { return sodium.to_hex(encryptedRecovery) } + /** + * Decrypts an Account recovery phrase + * + * @param encryptedRecovery - Hex encoded encrypted recovery phrase + * @param key - Hex encoded decryption key + * @returns {string} + */ + export const decryptAccountRecovery = async (encryptedRecovery: string, key: string) => { + await _sodium.ready + const sodium = _sodium + + const ciphertextBytes = sodium.from_hex(encryptedRecovery) + const keyBytes = sodium.from_hex(key) + + const plaintextBytes = await decryptRaw(ciphertextBytes, keyBytes) + const plaintext = sodium.to_string(plaintextBytes) + return plaintext + } + /** * Create a random seed for a new app * @@ -360,4 +404,34 @@ export namespace cryptoUtils { const wrappedKey = await encryptRaw(keyShare, keyBytes) return sodium.to_hex(wrappedKey) } + + export const getInviteLink = (inviteId: string) => { + const sodium = _sodium + + const hostname = `${window.location.protocol}//${window.location.host}` + const encodedInvite = sodium.to_base64(inviteId, sodium.base64_variants.ORIGINAL) + return `${hostname}/invite/${encodedInvite}` + } + + export const decodeb64string = async (b64string: string) => { + await _sodium.ready + const sodium = _sodium + + return sodium.to_string(sodium.from_base64(b64string, sodium.base64_variants.ORIGINAL)) + } + + export const getKeyring = async (email: string, organisationId: string, password: string) => { + return new Promise(async (resolve, reject) => { + const encryptedKeyring = getLocalKeyring(email, organisationId)!.keyring + if (!encryptedKeyring) reject('Error fetching local encrypted keys from browser') + + try { + const deviceKey = await deviceVaultKey(password, email) + const decryptedKeyring = await decryptAccountKeyring(encryptedKeyring!, deviceKey) + resolve(decryptedKeyring) + } catch (e) { + reject(`Error unlocking user keyring: ${e}`) + } + }) + } } diff --git a/frontend/utils/crypto.ts b/frontend/utils/crypto.ts new file mode 100644 index 00000000..b9cf06f1 --- /dev/null +++ b/frontend/utils/crypto.ts @@ -0,0 +1,172 @@ +import _sodium, { KeyPair } from 'libsodium-wrappers-sumo' + +import { cryptoUtils } from '@/utils/auth' + +const VERSION = 1 + +/** + * Returns an random key exchange keypair + * + * @returns {KeyPair} + */ +export const randomKeyPair = async () => { + await _sodium.ready + const sodium = _sodium + const keypair = await sodium.crypto_kx_keypair() + + return keypair +} + +/** + * Carries out diffie-hellman key exchange for client and returns a pair of symmetric encryption keys + * + * @param {KeyPair} ephemeralKeyPair + * @param {Uint8Array} recipientPubKey + * @returns + */ +export const clientSessionKeys = async (ephemeralKeyPair: KeyPair, recipientPubKey: Uint8Array) => { + await _sodium.ready + const sodium = _sodium + + const keys = await sodium.crypto_kx_client_session_keys( + ephemeralKeyPair.publicKey, + ephemeralKeyPair.privateKey, + recipientPubKey + ) + return keys +} + +/** + * Carries out diffie-hellman key exchange for server and returns a pair of symmetric encryption keys + * + * @param {KeyPair} ephemeralKeyPair + * @param {Uint8Array} recipientPubKey + * @returns + */ +export const serverSessionKeys = async ( + appKeyPair: { publicKey: Uint8Array; privateKey: Uint8Array }, + dataPubKey: Uint8Array +) => { + await _sodium.ready + const sodium = _sodium + const keys = await sodium.crypto_kx_server_session_keys( + appKeyPair.publicKey, + appKeyPair.privateKey, + dataPubKey + ) + return keys +} + +export const createSealedBox = async (plaintext: string, publicKey: string) => { + await _sodium.ready + const sodium = _sodium + + const sealedBox = await sodium.crypto_box_seal(plaintext, sodium.from_hex(publicKey)) + return `ph:${VERSION}:${sodium.to_base64(sealedBox)}` +} + +export const openSealedBox = async (ciphertext: string, publicKey: string, privateKey: string) => { + await _sodium.ready + const sodium = _sodium + + const ciphertextSegments = ciphertext.split(':') + + const plaintext = sodium.crypto_box_seal_open( + sodium.from_base64(ciphertextSegments[2]), + sodium.from_hex(publicKey), + sodium.from_hex(privateKey) + ) + return sodium.to_string(plaintext) +} + +export const encryptAsymmetric = async (plaintext: string, publicKey: string): Promise => { + await _sodium.ready + const sodium = _sodium + + return new Promise(async (resolve, reject) => { + try { + const oneTimeKeyPair = await randomKeyPair() + + const symmetricKeys = await clientSessionKeys(oneTimeKeyPair, sodium.from_hex(publicKey)) + + const ciphertext = await cryptoUtils.encryptString(plaintext, symmetricKeys.sharedTx) + + // Use sodium.memzero to wipe the keys from memory + sodium.memzero(oneTimeKeyPair.privateKey) + sodium.memzero(symmetricKeys.sharedTx) + sodium.memzero(symmetricKeys.sharedRx) + + resolve(`ph:v${VERSION}:${sodium.to_hex(oneTimeKeyPair.publicKey)}:${ciphertext}`) + } catch (error) { + reject(`Something went wrong: ${error}`) + } + }) +} + +export const decryptAsymmetric = async ( + ciphertextString: string, + privateKey: string, + publicKey: string +): Promise => { + await _sodium.ready + const sodium = _sodium + + return new Promise(async (resolve, reject) => { + const ciphertextSegments = ciphertextString.split(':') + + if (ciphertextSegments.length !== 4) reject('Invalid ciphertext') + + const ciphertext = { + prefix: ciphertextSegments[0], + version: ciphertextSegments[1], + pubKey: ciphertextSegments[2], + data: ciphertextSegments[3], + } + + try { + const sessionKeys = await serverSessionKeys( + { + publicKey: sodium.from_hex(publicKey) as Uint8Array, + privateKey: sodium.from_hex(privateKey) as Uint8Array, + }, + sodium.from_hex(ciphertext.pubKey) + ) + + const plaintext = await cryptoUtils.decryptString(ciphertext.data, sessionKeys.sharedRx) + + // Use sodium.memzero to wipe the keys from memory + sodium.memzero(sessionKeys.sharedRx) + sodium.memzero(sessionKeys.sharedTx) + + resolve(plaintext) + } catch (error) { + reject(`Something went wrong: ${error}`) + } + }) +} + +export const getUserKxPublicKey = async (signingPublicKey: string) => { + await _sodium.ready + const sodium = _sodium + + return sodium.to_hex( + sodium.crypto_sign_ed25519_pk_to_curve25519(sodium.from_hex(signingPublicKey)) + ) +} + +export const getUserKxPrivateKey = async (signingPrivateKey: string) => { + await _sodium.ready + const sodium = _sodium + + return sodium.to_hex( + sodium.crypto_sign_ed25519_sk_to_curve25519(sodium.from_hex(signingPrivateKey)) + ) +} + +export const digest = async (input: string, salt: string) => { + await _sodium.ready + const sodium = _sodium + + const hash = await sodium.crypto_generichash(32, input, salt) + return sodium.to_hex(hash) +} diff --git a/frontend/utils/environments.ts b/frontend/utils/environments.ts new file mode 100644 index 00000000..f712d5b3 --- /dev/null +++ b/frontend/utils/environments.ts @@ -0,0 +1,433 @@ +import _sodium from 'libsodium-wrappers-sumo' +import { OrganisationKeyring, cryptoUtils } from '@/utils/auth' +import { splitSecret } from './keyshares' +import { + ApiEnvironmentEnvTypeChoices, + ApiOrganisationMemberRoleChoices, + EnvironmentKeyType, + EnvironmentType, + OrganisationMemberType, + SecretType, +} from '@/apollo/graphql' +import { + decryptAsymmetric, + encryptAsymmetric, + getUserKxPrivateKey, + getUserKxPublicKey, + randomKeyPair, +} from './crypto' + +export type EnvKeyring = { + publicKey: string + privateKey: string +} + +/** + * Create a random seed for a new env + * + * @returns {Promise} - hex encoded env seed + */ +export const newEnvSeed = async () => { + await _sodium.ready + const sodium = _sodium + + const seed = sodium.crypto_kdf_keygen() + return sodium.to_hex(seed) +} + +/** + * Create a random salt for a new env + * + * @returns {Promise} - hex encoded env salt + */ +export const newEnvSalt = async () => { + await _sodium.ready + const sodium = _sodium + + const seed = sodium.crypto_kdf_keygen() + return sodium.to_hex(seed) +} + +/** + * Create a random token for a new env + * + * @returns {Promise} - hex encoded env token + */ +export const newEnvToken = async () => { + await _sodium.ready + const sodium = _sodium + + const token = sodium.crypto_kdf_keygen() + return sodium.to_hex(token) +} + +/** + * Create a wrapping key for new env secret share + * + * @returns {Promise} - hex encoded wrapping key + */ +export const newEnvWrapKey = async () => { + await _sodium.ready + const sodium = _sodium + + const key = sodium.crypto_kdf_keygen() + return sodium.to_hex(key) +} + +/** + * Encrypts an env seed with the given key + * + * @param {string} seed - Env seed as a hex string + * @param {string} key - Encryption key as a hex string + * @returns {Promise} - hex encoded encrypted seed + */ +export const encryptedEnvSeed = async (seed: string, key: string) => { + await _sodium.ready + const sodium = _sodium + + const keyBytes = sodium.from_hex(key) + const encryptedSeed = await cryptoUtils.encryptRaw(seed, keyBytes) + return sodium.to_hex(encryptedSeed) +} + +/** + * Decrypts an env seed with the given key + * + * @param {string} encryptedSeed - Encrypted env seed as a hex string + * @param {string} key - Decryption key as a hex string + * @returns {Promise} - hex encoded plaintext app seed + */ +export const decryptedAppSeed = async (encryptedSeed: string, key: string) => { + await _sodium.ready + const sodium = _sodium + + const ciphertextBytes = sodium.from_hex(encryptedSeed) + const keyBytes = sodium.from_hex(key) + + const seedBytes = await cryptoUtils.decryptRaw(ciphertextBytes, keyBytes) + return sodium.to_string(seedBytes) +} + +/** + * Derives an env keyring from the given seed + * + * @param {string} envSeed - Env seed as a hex string + * @returns {Promise} + */ +export const envKeyring = async (envSeed: string): Promise => { + await _sodium.ready + const sodium = _sodium + + const seedBytes = sodium.from_hex(envSeed) + const envKeypair = sodium.crypto_kx_seed_keypair(seedBytes) + + const { publicKey, privateKey } = envKeypair + + return { publicKey: sodium.to_hex(publicKey), privateKey: sodium.to_hex(privateKey) } +} + +export const newServiceTokenKeys = async () => { + await _sodium.ready + const sodium = _sodium + + const { publicKey, privateKey } = await randomKeyPair() + + return { + publicKey: sodium.to_hex(publicKey), + privateKey: sodium.to_hex(privateKey), + } +} + +/** + * Generates an environment token. + * + * @param {EnvironmentType} environment - The environment for which the token is generated. + * @param {EnvironmentKeyType} key - The key associated with the environment. + * @param {{ publicKey: string; privateKey: string }} userKeyring - The user's keyring. + * @returns {Promise<{ pssEnv: string; mutationPayload: object }>} - An object containing the environment token and mutation payload. + */ +export const generateEnvironmentToken = async ( + environment: EnvironmentType, + key: EnvironmentKeyType, + userKeyring: { publicKey: string; privateKey: string } +) => { + const wrapKey = await newEnvWrapKey() + const token = await newEnvToken() + + const envSeed = await decryptAsymmetric( + key.wrappedSeed, + userKeyring.privateKey, + userKeyring.publicKey + ) + + const envKeys = await envKeyring(envSeed) + + const keyShares = await splitSecret(envKeys.privateKey) + const wrappedKeyShare = await cryptoUtils.wrappedKeyShare(keyShares[1], wrapKey) + + const envSalt = await decryptAsymmetric( + key.wrappedSalt, + userKeyring.privateKey, + userKeyring.publicKey + ) + + const pssEnv = `pss_env:v1:${token}:${envKeys.publicKey}:${envSalt}:${keyShares[0]}:${wrapKey}` + const mutationPayload = { + envId: environment.id, + name: 'testEnvToken', + identityKey: environment.identityKey, + token, + wrappedKeyShare, + } + + return { + pssEnv, + mutationPayload, + } +} + +/** + * Generates a user token. + * + * @param {string} orgId - The organization ID. + * @param {{ publicKey: string; privateKey: string }} userKeyring - The user's keyring. + * @returns {Promise<{ pssUser: string; mutationPayload: object }>} - An object containing the user token and mutation payload. + */ +export const generateUserToken = async ( + orgId: string, + userKeyring: { publicKey: string; privateKey: string }, + name: string, + expiry: number | null +) => { + const wrapKey = await newEnvWrapKey() + const token = await newEnvToken() + + const keyShares = await splitSecret(userKeyring.privateKey) + const wrappedKeyShare = await cryptoUtils.wrappedKeyShare(keyShares[1], wrapKey) + + const pssUser = `pss_user:v1:${token}:${userKeyring.publicKey}:${keyShares[0]}:${wrapKey}` + const mutationPayload = { + orgId, + name, + identityKey: userKeyring.publicKey, + token, + wrappedKeyShare, + expiry, + } + + return { + pssUser, + mutationPayload, + } +} + +/** + * Wraps environment secrets for a user. + * + * @param {{ seed: string; salt: string }} envSecrets - The environment secrets to be wrapped. + * @param {OrganisationMemberType} user - The user for whom the secrets are wrapped. + * @returns {Promise<{ user: OrganisationMemberType; wrappedSeed: string; wrappedSalt: string }>} - An object containing the wrapped environment secrets and user information. + */ +export const wrapEnvSecretsForUser = async ( + envSecrets: { seed: string; salt: string }, + user: OrganisationMemberType +) => { + const userPubKey = await getUserKxPublicKey(user.identityKey!) + const wrappedSeed = await encryptAsymmetric(envSecrets.seed, userPubKey) + const wrappedSalt = await encryptAsymmetric(envSecrets.salt, userPubKey) + + return { + user, + wrappedSeed, + wrappedSalt, + } +} + +/** + * Wraps environment secrets for a service token. + * + * @param {{ seed: string; salt: string }} envSecrets - The environment secrets to be wrapped. + * @param {string} publicKey - The public key of the service token. + * @returns {Promise<{ wrappedSeed: string; wrappedSalt: string }>} - An object containing the wrapped environment secrets. + */ +export const wrapEnvSecretsForServiceToken = async ( + envSecrets: { seed: string; salt: string }, + publicKey: string +) => { + //const servicePubKey = await getUserKxPublicKey(publicKey) + const wrappedSeed = await encryptAsymmetric(envSecrets.seed, publicKey) + const wrappedSalt = await encryptAsymmetric(envSecrets.salt, publicKey) + + return { + wrappedSeed, + wrappedSalt, + } +} + +/** + * Unwraps environment secrets for a user. + * + * @param {string} wrappedSeed - The wrapped environment seed. + * @param {string} wrappedSalt - The wrapped environment salt. + * @param {OrganisationKeyring} keyring - The keyring of the user. + * @returns {Promise<{ publicKey: string; privateKey: string; salt: string }>} - An object containing the unwrapped environment secrets. + */ +export const unwrapEnvSecretsForUser = async ( + wrappedSeed: string, + wrappedSalt: string, + keyring: OrganisationKeyring +) => { + const userKxKeys = { + publicKey: await getUserKxPublicKey(keyring!.publicKey), + privateKey: await getUserKxPrivateKey(keyring!.privateKey), + } + const seed = await decryptAsymmetric(wrappedSeed, userKxKeys.privateKey, userKxKeys.publicKey) + + const salt = await decryptAsymmetric(wrappedSalt, userKxKeys.privateKey, userKxKeys.publicKey) + + const { publicKey, privateKey } = await envKeyring(seed) + + return { + seed, + publicKey, + privateKey, + salt, + } +} + +/** + * Decrypts environment secret key and value pairs. + * + * @param {SecretType[]} encryptedSecrets - An array of encrypted secrets. + * @param {{ publicKey: string; privateKey: string }} envKeys - The environment keys for decryption. + * @returns {Promise} - An array of decrypted secrets. + */ +export const decryptEnvSecretKVs = async ( + encryptedSecrets: SecretType[], + envKeys: { publicKey: string; privateKey: string } +) => { + const decryptedSecrets = await Promise.all( + encryptedSecrets.map(async (secret: SecretType) => { + const decryptedSecret = structuredClone(secret) + decryptedSecret.key = await decryptAsymmetric( + secret.key, + envKeys?.privateKey, + envKeys?.publicKey + ) + + decryptedSecret.value = await decryptAsymmetric( + secret.value, + envKeys?.privateKey, + envKeys?.publicKey + ) + + return decryptedSecret + }) + ) + return decryptedSecrets +} + +/** + * Decrypts environment secrets. + * + * @param {SecretType[]} encryptedSecrets - An array of encrypted secrets. + * @param {{ publicKey: string; privateKey: string }} envKeys - The environment keys for decryption. + * @returns {Promise} - An array of decrypted secrets. + */ +export const decryptEnvSecrets = async ( + encryptedSecrets: SecretType[], + envKeys: { publicKey: string; privateKey: string } +) => { + const decryptedSecrets = await Promise.all( + encryptedSecrets.map(async (secret: SecretType) => { + const decryptedSecret = structuredClone(secret) + decryptedSecret.key = await decryptAsymmetric( + secret.key, + envKeys?.privateKey, + envKeys?.publicKey + ) + decryptedSecret.value = await decryptAsymmetric( + secret.value, + envKeys.privateKey, + envKeys.publicKey + ) + return decryptedSecret + }) + ) + return decryptedSecrets +} + +/** + * Creates a new environment payload. + * + * @param {string} appId - The ID of the application. + * @param {string} name - The name of the environment. + * @param {ApiEnvironmentEnvTypeChoices} envType - The type of environment. + * @param {OrganisationMemberType} owner - The user for whom the environment is created. + * @returns {Promise} - An object containing the environment payload. + */ +export const createNewEnv = async ( + appId: string, + name: string, + envType: ApiEnvironmentEnvTypeChoices, + ownerAndAdmins: OrganisationMemberType[] +) => { + const seed = await newEnvSeed() + const keys = await envKeyring(seed) + + const salt = await newEnvSalt() + + const owner = ownerAndAdmins.find( + (user: OrganisationMemberType) => user.role === ApiOrganisationMemberRoleChoices.Owner + ) + + const ownerWrappedEnv = await wrapEnvSecretsForUser({ seed, salt }, owner!) + const adminWrappedEnvSecrets = await Promise.all( + ownerAndAdmins + .filter((user) => user.role !== ApiOrganisationMemberRoleChoices.Owner) + .map(async (admin) => { + const adminWrappedEnvSecret = await wrapEnvSecretsForUser({ seed, salt }, admin) + return adminWrappedEnvSecret + }) + ) + + return { + createEnvPayload: { + appId, + name, + envType, + wrappedSeed: ownerWrappedEnv.wrappedSeed, + wrappedSalt: ownerWrappedEnv.wrappedSalt, + identityKey: keys.publicKey, + }, + adminKeysPayload: adminWrappedEnvSecrets.map((wrappedSecrets) => { + const { wrappedSeed, wrappedSalt, user } = wrappedSecrets + return { + identityKey: keys.publicKey, + wrappedSeed, + wrappedSalt, + userId: user.id, + envId: '', + } + }), + } +} + +/** + * Compares two arrays for equality. + * + * @param {any[]} arr1 - The first array. + * @param {any[]} arr2 - The second array. + * @returns {boolean} - True if the arrays are equal, false otherwise. + */ +export const arraysEqual = (arr1: any[], arr2: any[]) => { + if (arr1.length !== arr2.length) { + return false + } + for (let i = 0; i < arr1.length; i++) { + if (arr1[i] !== arr2[i]) { + return false + } + } + return true +} diff --git a/frontend/utils/localStorage.ts b/frontend/utils/localStorage.ts index 11af28da..4e2f18a3 100644 --- a/frontend/utils/localStorage.ts +++ b/frontend/utils/localStorage.ts @@ -1,34 +1,44 @@ import { OrganisationType } from '@/apollo/graphql' -interface LocalOrganisation { +interface LocalKeyring { email: string org: OrganisationType keyring: string recovery?: string } -export const getLocalOrgs = () => { +export const getLocalKeyrings = () => { const localData = localStorage.getItem('phase-accounts') if (!localData) return undefined const orgs = JSON.parse(localData) - return orgs as LocalOrganisation[] + return orgs as LocalKeyring[] } -export const setLocalOrg = (org: LocalOrganisation) => { - let localOrgs: LocalOrganisation[] = getLocalOrgs() || [] - // Org already exists locally - if (localOrgs.find((orgData) => orgData.org.id === org.org.id)) return - // Add Org to local storage +export const setLocalKeyring = (keyringData: LocalKeyring) => { + let localKeyrings: LocalKeyring[] = getLocalKeyrings() || [] + + const existingKeyringIndex = localKeyrings.findIndex( + (orgData) => orgData.email === keyringData.email && orgData.org.id === keyringData.org.id + ) + + // Keyring for this org and user already exists locally, remove it + if (existingKeyringIndex !== -1) { + localKeyrings.splice(existingKeyringIndex, 1) // Remove the object from the array + } + + // Add Keyring to local storage else { - localOrgs.push(org) - localStorage.setItem('phase-accounts', JSON.stringify(localOrgs)) + localKeyrings.push(keyringData) + localStorage.setItem('phase-accounts', JSON.stringify(localKeyrings)) } } -export const getLocalKeyring = (orgId: string) => { - const localOrgs: LocalOrganisation[] | undefined = getLocalOrgs() - if (!localOrgs) return undefined - const org = localOrgs.find((org) => org.org.id === orgId) - if (org) return org.keyring +export const getLocalKeyring = (email: string, orgId: string) => { + const localKeyrings: LocalKeyring[] | undefined = getLocalKeyrings() + if (!localKeyrings) return undefined + const localKeyring = localKeyrings.find( + (keyring) => keyring.email === email && keyring.org.id === orgId + ) + if (localKeyring) return localKeyring as LocalKeyring else return undefined } diff --git a/frontend/utils/permissions.ts b/frontend/utils/permissions.ts new file mode 100644 index 00000000..5d96e1da --- /dev/null +++ b/frontend/utils/permissions.ts @@ -0,0 +1,2 @@ +export const userIsAdmin = (role: string) => + ['admin', 'owner'].includes(role.toLowerCase()) ?? false diff --git a/frontend/utils/posthog.ts b/frontend/utils/posthog.ts new file mode 100644 index 00000000..b8569c20 --- /dev/null +++ b/frontend/utils/posthog.ts @@ -0,0 +1,15 @@ +import posthog from 'posthog-js' + +export function initializePostHog() { + if (typeof window !== 'undefined' && process.env.NEXT_PUBLIC_POSTHOG_KEY) { + posthog.init(process.env.NEXT_PUBLIC_POSTHOG_KEY, { + api_host: process.env.NEXT_PUBLIC_POSTHOG_HOST || 'https://app.posthog.com', + capture_pageview: true, + session_recording: { + maskInputOptions: { + password: true, // Mask password inputs + }, + }, + }) + } +} diff --git a/frontend/utils/recovery.ts b/frontend/utils/recovery.ts new file mode 100644 index 00000000..0e47a878 --- /dev/null +++ b/frontend/utils/recovery.ts @@ -0,0 +1,166 @@ +import jsPDF from 'jspdf' +import { toast } from 'react-toastify' +import { copyToClipBoard } from './clipboard' + +const PHASE_LOGO = `data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAQAAAAEACAIAAADTED8xAAAezElEQVR4nOydUWwb15X3KZHSjBjRHJmORCmKzMSOrKafE36fk0aJLYTGtxsbdbBykYck1YPlBmgbA4s4u8A2XiwQ+yXOFouNUyBxUmzWFhZO4MUasbBJWmPTRoJdhEazW8luUVmtYcpSKEqRrKFpkTMUh1pE41weX5J3OCJnSGrO72lGuaFG9PzPPefcc8+ttSGIhUEBIJYGBYBYGhQAYmlQAIilQQEglgYFgFgaFABiaVAAiKVBASCWBgWAWBoUAGJpUACIpUEBIJYGBYBYGhQAYmlQAIilQQEglgYFgFgaFABiaVAAiKVBASCWBgWAWBoUAGJpUACIpUEBIJYGBYBYGhQAYmlQAIilQQEglgYFgFgaFABiaVAAiKVBASCWBgWAWBoUAGJpUACIpUEBIJYGBYBYGhQAYmlQAIilQQEglgYFgFgaFABiaVAAiKVBASCWBgWAWBoUAGJpUACIpUEBIJYGBYBYGke5H8AkHA7H+vXrnU5nfX29zWZLJBLxeDwajaZSqWI+1i3U9u1z9wQatvr5Dt/XX+blEfnSiPT+QOz8ULx0j48YRU25H8BwOI7buHGjy+XK/k/JZHJ+fj4cDsMf8jzf3d3t8/laWlp4nrfZbJFIZGJiIhgMiqIIRx467DnwUpNbyD2LnjoZPXpk/nqoKIEhRrPGBdDW1tba2soek0wmx8fHZVnmeX737t0PP/xwvpGjo6NDQ0OiKHb46j74sHWrn2d/8vXQ0vPfC18ekVf7+IjhrFkBcBy3adOmhoaGQgYnk0m32719+3bV5DMQRTEmf3Dk6Lp8hj+bH/dPvz8QK3AwYjJrUwAcx3V2dqruPiEWi4miKMuyoigej8flcnEcp/6nxx9//MEHH5ybm1MUhf3JTwQuPx74PfyJKKbfPnbjwlDiwnDCLdRufZh7/VjzQ36ODIiKynd3TuE8UJmsQQE4nc7Ozk673U5+IstyKBS6desWHFZfX9/a2nrPPfc89dRTqpukhgTLy8uiKF68eHFsbEwURZ7nu7q6nnzySUEQst/+t47deP3IjaiYpp7hlVfX//3hDeT2emjpuzsnMR6oQNaaALLf/oWFhYmJiZymnef5gwcP3nfffeQn8/Pzg4ODFy9ezB787on/93z/HRJ69ZWbb/xjJN+TUBo4PxTfs3NqVX8TYiD2cj9AKeE47oEHHqirqyM/CYfDk5OTy8vL2YN5nt+3b58gCHa7Xf1fYrHYJ598cunSpWQySQ0+cLDp5VcyHytLdR/8y1NfBBuvXLmS72EuDCeWbcs9Aad6u9FXF40qvw1KpfhDkZKxdgSQ7feHw+Hp6el841944QWv16t6PjzPLywsnD17NpFIuFwu1REiI3sCzhMfZFJJslT37yf//2ykyev1zszMzM3N5fsVF4YTOwING323lfNoN/+v70ZlKYcakXKxdgSwZcsWmMNhv/27d+/u6upSr5eXl7/88svPPvtMNfx2uz2dTpOAYSXj2eYWbn9Ropg+/k8Px2/dq962t7ePjo4yVtMuDCf6+t08X7My59TyfM2n53CBrIJYIwJoa2tramoitzMzM9TyFiQQCGzfvp3cRiKR9957L5VKNTY2qj9xOp1fffXV8vKyW6j99ecdHb6M87P/+fBHg5FHHnlEveV5PpVKTUxM5PtdUTHN8zXEEXq0u+HUQDQ7aEbKxVqoBWppaYGrXbFYbGoqb7ippnTIrSiKp0+fliRpenqaBMp2u725uVld64Vv/2uH5z4+uxiJRIaHh8kPu7u7BUFgPN7RI/MToSVye+hVz6r+SsQQql4AHMfBt1/NeOYbLAjCrl27yK0oigMDA2qBg6IocNJoaWn567/dcOClzKyiZjzVa1gWwfN8IBBgP+QrL39Frvv63WrVEFIJVL0L9OCDD5K0TyqVunLlSnYOh7Bv374NGzKpydOnT0cimTzm4uKix+NxOL5+O9vabT/9WV39N8tZE6GlHzwfIfFrKpWSZZlEEV6vd3R0VJLyZnj+NJaE0bDNZsNIoEKo7hmgra0Npn2mp6cZb38gEFDTPipDQ0PZc8Xs7Kx68ca78UbX7dddFNN7dk5SjvvIyAisjXvsscfYj3r0yDy57uvXUUmBGEoV/zO4XC7o/MzNzZHXNxufzwdd/7GxMejHE+bn5xVF6f+h7G3NJCuPHp7LuYgLP8Hv97PriC4MJUgk4Bbs0LlCyki1CsDhcPh8PnIryzIj6SkIQm9vL7kVRfHcuXM5RyqKUmOP7PthZho5dTJ6/E0x52A4CfA8rzkJvH/yJrk+cJAVNyOmUa0CaG1thc5PKBRiOz8wUTM4OEhV9kP+7T8yBaSRcO2bP00wHgMWTXR3d7MngbffXCB+lFuwP93byBiMmENVCsDlcqlpSpVwOEwVukH8fj8s8c/p+hOovOfJn9enFS/jSUZGRkjsq5bNMQZHxfSpk1Fy+yJOAhVAVQpAl/NDZf1zuv4qHb46mKT/5X/WnfuozuVywdI6CkmS4CTA2Eyj8tFgRqg9ASeGwmWn+v4BqMzP+Pg4YzDl/AwMDDAGf/JZO7mOhGsHfn47CQpnm2xGRkbItW8FxuALQwm4VxhD4bJTZQKglr1mZmYYrr/X66WcH4brf+BgE3R+jh9LRaZv14p7PKy1W1EUYSnEli1b2H/Cx2cXwS9FL6jMVJkAOjs7yTXb+bHZbM8++yy51nR+DryUeRdPnYz+7J+vkcoIjuNy7qknBINBcu33+9l/AqwFcgv2HYGCNm0iBlFNAvB4PNSyF2MHI+X8nD59mvHJh17NxL4ToaWjR+YVRYnHM76K2+1m/O+hUAiGwmwvKCqmPzqbiQQwF1ReqkYAHMe1tbWR27m5ufn5+XyDqdh3ZGQEljxQPL23sa9/Hbl9+9iCuuwFpxdYQJGNJEmjo6PkFv7qnJwayOSCcFW4vFTNVw8T/6lUiu38wOo0tvNjs9lef+Nucj0RWiLLXvF4HNaHsr2gsbExcu31ejVXhaEXtBXsoEdMpjoEwHEcjERnZ2cZsS+V+B8eHmbEvlTif8/OSXKtKArc7aXpBcFVYe1IACwI9O1bxx6MGEd1CEBX7As9kFAoBNOUFB2+OvjynToZpWp+otHMa8r2gtS2WeRaMxcEFwSe3tuIXlC5qILvnYp9JycnGYOzqx4Yg7NjX2pALBYr3AuiFgTQC6oKKl0ADoeDin2hVaagYt/sbp6Qr80/iH2PHs7dx7NwL4haEEAvqCqodAE0NzdTqU/GYCr2zdnehwDXfSdCS+8P3Mw5TJcXBKuM9HpB7MGIQVS0AKh133A4zIh9fT5f4bFvX/86GPu+cjDvRgLTvCBcESsLFS0AarMvY7+LzWajKv4ZsS+1M/3UyejHg4uMwVBIxnlBuCJWFipXAE6nE6Y+2eu+fr+/8KI3KvWZHftSwBU3dgOIYrwgGJAgplG5Ati0aRO5lmWZse7L8zy17qsR+4KI861jNzR71sIVMY7j2C3XoQA0V8Quj8joBZWXChUAlfq8evUqYzDVmYe97kulPvNtd4RQdUHsSYBaEYPb8LOJiulLI5leEj1POjUfBiktFSoAKvWZSOTdl0ilPtk1zwWmPrOBn8mOg202G+yYqxkGwLMzcAYwn0oUQEtLi0GpTxj7MlKf2UAHjL1HjKoL0g4DzmYEgHvEzKfivm6O4+AOrLm5OUbqUxAEKvXJ6E7VE3BS5r/wp1IUJRaLwd/LGKy3OhruEcMVMZOpOAHAlS/Nsp/du3eTa83U5/ETLeT6/FC8cPOvAgWgywvSnAQuDGUcvB0BDANMpbIEQJn/+fl5dtUnfLfYsS+18vXi/rzbA/IBG0/oSoayZ4Cv1TicmQF6MAwwl8oSALXyVXjV59jYmK6Vr1Uc16VrSZjaHsAWDC4Jl5EKEgBV9M9++6mVr3yd3lQo86+58pWPwpeEJUmCS8LsfkEroTBukiwPFSQAyvwzVr4o889e+SqJ+VfRFQboygVdGMYwoDxUigBcLteqzb/GypfOwgcGUGZOp5OdDNW1JAyToQ/5OUyGmkalfNHUES+6NrwXXvjwWp4+zwWiKxkaiURgMlRzSRiToWWhIgSgnttObhnHe1HmX3PDO1X4oDf1mY0uL0jXJkmYDMUNYqZREQKgCh8YnW51m/9VFT4wWHUyVDMOhslQ3B9jGuUXQHa7K8ZgXf1OVl34wEBXMhQKQFiBMRiToWWh/AKgzL+uwgfGx2ab/1I8rI0KhcnJqjkpJhmKlaHmUGYBFGP+C1/5KpX5V4Gl0bqSoRs3bmQPvjMZijOAGZRZAFVn/rNLo9nJUNiSUbMmAitDzaecX3E1mn+bzZZMJmVZJrdOJ8tX0VsZCsP0HegFGU/ZBEA1u2V3fNBl/ospey6QwmsiqMpQTS/ozkkAvSDDKZsAoPnXLHzYu3cvudZl/ldR9lwIsFlQaStDsVmQyZRHAFTdG7vs2efzQcPJ7nbYE3DC8NEI80/FwRzHFb5BTLNZ0OWRjHPV4avr2Ogo+mERFuURgC7zD73/0AqMwXDXy0dnb8G8SgnRVRMhSVLhoTBVE9GDhXEGUwYBUP3epqen2bteCjf/dL+3l1mNtIpEV00EXA3QToYOYTLUPMoggMovey4EXTUR0AvCmoiKwmwBFLPrRdemx2LKnguBqolgd8uClaF6ayKwMM5QzBbA2jD/KoUvCUuSNDMzQ261JwEYBjyJXpCBmCqANWP+VaAgdSVD2XsDqDBgD3pBRmKqANaS+afCAPZ6cDFNcx9CF8hIzBPAGjP/eo+RpGoi2JPA9dASlkabg3kCWGPmX6Xw0mgqGVpAYRyWRpuBSQJYe+ZfRVdptC4vCEujzcEkAaxJ86+3TwTVLYv9yTARhH0ijMOMr3Wtmn+qNNput7NDYVEUCy+Nvh5aImLG1QDjMEMAa9X8q5hTGo3t4gzCcAGsYfOvAg/v0JUM1YyDL49mSqRwBjAIwwWwts2/3h2Sq24XhzskDcLY73TV5l+z5UmFmH+1NLrwHZLiCup1Ie3icIek0RgrgDVv/lVWHQboPEAJk6Glx0ABUOafvZElEAhA8w+bCmYDzf9EaKmM5l8F1kRorgbAzTHaRUHYNdpgDBQA1e+W3fAQ7nmvLvNPbY7RtRqguUMSVwOMxqgvlDL/uvrd6jL/Rux514uug4SL2SGJydCSY5QArGP+VeAkwN4co3eHJNwmj8nQkmOIAKjTLkpo/uFpFxVi/lVW3ShFu2EoNkoxEkMEYJD5p067qBzzr7dRCtU1WlejFAwDSkvpv03XCuR2bXv/BL2NUgrvGo1hgKGUXgAWNP8qcBLQDANgHNzS0sIeDCcBLI0uLSUWgDXNv4quMEBXoxQYBmCrrNJSYgFY1vxTOyQ5joONr7PR1Sjl8ohMdkhiv8TSUkoBWNn8Z68G6GqUorkacGlEIrc4CZSQUgpg1eY/GAxWu/lX0dUvUVdpNPZLNIiSCaAY83/x4kXG4Kow/ypQ85p75KEANJfDYL9EnAFKSMkEsGrzr33YUZWYfwwDqpHSCKAY888+7aKKzP8qioIK75dIhQG4JFwqSiMANP8Eqk8Ee7CuPhEfn10k11gUVCpKIAA0/xC4RVhzNQAuh2lujrk0ijNA6SmBAND8Q6i26ewwgOqXqKttOoYBJaFYAaD5z6bwMMBms+lqm45hQMkpVgBo/rOh+kSwB+s6Sh6GAbhDsiQUJQA0/znRtUVY13LYnasBuBxWAmqK+Z+Xl5dL9yRIQdyyPbJsu73efJdtsNam4TVZgZqa1b/GuLuiyrDbvkOuUzbWCjpSCCiAKuNOAXxa1mdZC6AAqgy77TFynbaNlfVZ1gJF5ZKL8b3WPH6/n+wMHh8fh4Wi2fzkJz8hO4MHBgbYTcQmFzarO4OXbTd7djphoWhF4fF4SFgfi8XGx8cZg/1+f29vr3odCoUGBgYYg/v61x0/cXvhHO4XXQU4AxiFrtOTdLZNr47Tk+AaCFwbyQlcA4EL5DnZAzZGw/2iqwAFYBQG7g2ohtOTOI6DAmC3hRUEAVaCsHdHdfjq4CLgqeJS5CgAo9DVNl1Xv8SqaJsOJz1ZlmGJVDZQ86IosmcAuAAyEVrCGaBCsXijlLa2NnLNbotPNQZnlwhQrQGPHi62LzIKwEAKF4DuMyQr+yh5l8sFqwDZCQCfzwe/HHYCoCfghOdCXBguKgJGARgLVRNReLs47a7Rd9ZEVJoXBBtjzs3NJZNJxmC/30+uNTvDfr/UFWKV9cWtMajSaPb+GF1HyVOl0RW1P4ZqDK4Z/sISSc3wt68fCqAEFWIoAGOZm5sj15qHx8B/fk0v6NTJTB+uigoDYIFwPB5nFAirB6OQ60gkwvZ/oPd/aUSG2bBVgwIwFtgubsOGDezBMBcEHYOcwHZx0C6WF8r8z87OMgZT5p/dHIQy/28fWyj6YW0oAMOBfSLsdjt7QQD2idA8SRv2iXAL9gpZENB1Khw0/5oV8tD8l7BCHgVgLIqiFH6EniRJhR+hFxXTcEm4ErwgXYeC6t4gBcx/8dlPAgrAcKAASusFfVxhXlDVmX8UgBlQuSC2F0Tlgthe0PmheOV4QdVo/lEAZkAtCWt6QYWviFXUknA1mn8UgEnAZIimFwSTIZpe0NtvZpIhZfSCqtT8owBMYs3ngqrU/KMATEJRFLgiBo1lNpIkwRUx9iQQFdNwRQz2kjGN6jX/KADzoA5QKrw6esuWLRrV0Xeeo2p+XVBnZye51jT/+/btI9ea5v+Tz9rJtUHdcVAAJkHlgtiTAJULYk8CVF2QyZOAx+OBhZ/sWgbYG8pmsw0ODjIGw95QNpvtxX6NbWKrAwVgHrALomZ1NAyFNeuCYF2AmdXRDocD1v3Pzc0xKn94nod1/yMjIwy1uIVa6P2fOhktSeVPNigA84C5IKqpXjbBYJBc+1ZgDIa5oJ6A07RQuLm5GZp/tvff3d0NZc/2/g8cbILm/+iR0nv/KigA89C7IAD3iFXgggDHcTD5Ew6HGXX/giBA8z80NMQ+FQ6a/9cOzxnXGRYFYCrUggA7FIaTgN/vZ4fCbx/LvE99/etMCIVh9wpZltmFn3v37iXXmqfCvXMisxdiIrR0/E3WFpkiQQGYioGh8HDczFDY4/FAF256epr8Xdn4/X6oluHhYfJ3ZdPXvw66cEcPz5O/ywhQAKaiKErhobAkSYWHwlExbVoozHEcFfsyUp+U8zOyQr7BlPNz6mTU6MbgKACzWQOhcGtrK4l9U6kUO/YNBAJE55IksWPfQ696SOwrimnjYl8CCsBsqB0CMI7MRpIkuCgGTWk21A4BaEpLiGcFcjs7O8uIff1+f+Enovf1r7tzz9cNE05FQQGUAV2TAPSCyj4JUM6PLMsM8085P6IoMsw/5fxMhJZeP3KjRE/NAgVQBmIrkFv2JBAKhWA+lD0JXBhKwHxoyScB6PyoTX8Zg6Hzozb9ZQyGzs/XMczOyaIftiBQAOUBGk7NSWBoaIhca04C0G8u7STQ0tICnR924r+7uxs6P+zE/4GDTdD5MTTxT4ECKA9VNwlwHNfenilNi8fjbOdn165d5DYUCrGdn9ffuJvcnh+Km+P8qKAAykYVTQIOh4Mq+bx69Wq+wTzPUyWfjKI3t1BLlXy+uN+Qord8oADKRhVNApTrPz09zXB+KNd/eHiY4fwcOnyH63/08LzJ5+GiAMpJVUwCbW1tzc3N5HZmZoax7BUIBB57LHOIUzAYZCx7HTrsOfBSE7l969gN88/DRQGUE2oSgE52NtQkAJ3sbKhJADrZunA6ndR2R4br7/V6C897bvVzZcl7UrCKsRATSCaTJLVSV1enKMri4mK+waIokoqgxsZGWZanpqbyDb4+sdTXf7vgtMXriEaV3wbzVuDkhOO4Bx54gFTspVKpK1eupFK5XRRBEPr6+kjFniRJ7733Xr7tAR2+ug9/cY9bsH/zd6X/4vHrs5G81UTGgQIoM8lksr6+njSObmxsXFhYyFdYJoqiIAikcXR7e/sf/vCHfIVl10OpDl/dQ980jn60mz9zOlZ4YZnD4ejq6oKu/7Vr1/KJk+f5F154Abr+Z86cySdOt1D76887oOu///nwF8GiDnpZNegClZ+pqSlYIsp2hM6dOwdLRNmO0Csvz8IS0dffaC78qe6//3749ofDYUYs+9xzz8G3f2hoCFZwUHzw4T3w7X/t8NzHZ/NOekaDM0D5WV5eTqfTZH8Mz/OJRCKfXU+lUoqibN68Wb3dsGHDzMwMbDkBkaVlSU7/5e671NvOrvpLo9KfxpY0H+nee+9dv349uZ2ZmQmHw/kG7969+9vf/ja5DQaDv/rVr/INfv3Y3c88l4n13zp248ghwyveGOAMUBHMzs7CaNjn83Fc3jMvgsEgjIZ7e3sZZdXHj4kwGn7nhLfDp3E4NJX2Ya95UWmfSCTCCHyptM+lEaksgS8EBVAphEIh6AixTws+e/YsdITIEdM5eXH/DHSEjp9gnT3T1tZGpX2uXr2aLyYJBAJU2uf06dP55q5Dhz1U2uf73wsbutmlENAFqhQURYGOEMdxDofj5s3ceXFJkqAjJAhCQ0PDn//855yDo2IaOkIbfXVuofbTczmOl8t++8fHx/OteWW//QMDA/nihOy3f8/OSZPXvHKCAqggFhcXXS4XcX7uuuuuZDKZ74Tdqamp++67jzg/7e3t0Wg03wm7XwSlHYGGjd+Eno92N0yEkpdH73izLfj2owAqjlgsJgiCw3HbTXe5XDdv3lxayh22hkKhrq4uknr3+XxXr17Nl3q/MJzYs7dR+Cb13hNwfnouTlLvPp8P+v3st7+3t7e7u5vcst/+4ye90O+vqLffZrPVlPsBEBqn0/mtb32L3CqK8sc//lGWc6fJvV7vj370I3IrSdK7776b713c6ud+87tMaBEVle3/d2Jm2r5p06aGhkytBOPtFwTh2WefhSdYMt7+Dl/dBx+2bvVnmllU2tuPM0AlsrS0pCgKCQZqa2sFQYhGozkj0Vu3bsmyTIIBdfXqypUrOSPR2YgiRhUSDPB87dN7G8cut6SWCnr7vV5vX18fbO/OePu3+rkPf3FPZ1cml1WBbz8KoEJRF1xJbZzaQCWfLzQ1NVVTU0Nq49QGKvl8oS+C0rJtuSdwe+HZLdj37F3+7eeOG/M17Le/u7u7t7e3sTHTbILx9h842PTOCW+LN5Nvrcy3HwVQuaivL9FAbW3t3Xffna9SKBQKQQ04HI5HHnkkX6XQheGE3W7f/uRtz6Ses/3VM0uLsZqLn8fHx8ezS30EQXjuuee2bdtGIhP1Nw4MDGRrbMXtuecHP3bzfMa7Pj8U37NzqiylPpqgACoXSgNqN0WO4xKJRLY7RGnAZrNt3ry5qakJHrehasPr9X553VtTW+vflvmQ7zyh3OWKjv4uARPzPM/v2LGjt7eXOtUmGAyeOXOGkopbqP2bV9a/c8Lb2VUPf/7WsRs/eD4iS8vFfRlGgQKoaLI14HQ6BUFQFCU7PZqtAa/X29XVJcuymh71eDz333+/IAi1tbWj/2232WqgBh7y80/vbYyK6cujstrR5Jlnnunq6oKGX5KkM2fOZDc27Otfd+KD1qf3NkLDL4rp/c+Hj78ZLelXUmIwC1QFeDyee++9l2okmkwmw+FwNBqlLLHf79+1axdsJFpTU5NKpcbHx6empqhs0hOBG3/3D3aqkej10NL/BB+dCm2WpTr481AoNDg4eMexx0LtgYNNB15qym5Fen4o/uL+SAU6/RQogOqgvr6+s7Mzu0BI7Tgdi8USK6hi8Hg8+/fvV4+uUFEHq9tZwuHw/Pz8V199de3atdnZ2c4u1yeftTd7aZ9KluomQy2TobtnI+snQw3nfvmbixcvuoVaQbBv9XM7nmx4yM/n3Ggmiumjh+cM7WhbQlAA1URrayvsS8Vm2wrZP0+n07du3VpcXFxezvjlTwQuPx74fZGPJ4rpt4/dOP6mWPYKn8JBAVQZ9fX1ra2tmmetqjQ2Nm7bto101c356hPWCYuPBy7/H/+1VTxVNb76KiiAqkSVASwcYtDY2NjV1bVhw4a6urqcrz5knbB4f9d/be66udFXV8iTnB+Kf3R28f2BaNW9+ioogOqmoaFBbSeh+vpqxkbdNJNIJGRZjsfjZBXZ6/Wq7STcbrcgCGqgLK0wMzOzsLAwMzMzNjampk1XHH1nT6Chw1fX4asTVsJcUUxHReXyiHw9tHRpRP548FaVvvcIgthwQwxidVAAiKVBASCWBgWAWBoUAGJpUACIpUEBIJYGBYBYGhQAYmlQAIilQQEglgYFgFgaFABiaVAAiKVBASCWBgWAWBoUAGJpUACIpUEBIJYGBYBYGhQAYmlQAIilQQEglgYFgFgaFABiaVAAiKVBASCWBgWAWBoUAGJpUACIpUEBIJYGBYBYGhQAYmlQAIilQQEglgYFgFgaFABiaVAAiKVBASCWBgWAWBoUAGJpUACIpUEBIJYGBYBYGhQAYmlQAIilQQEglgYFgFgaFABiaf43AAD//ylBd5yP00qCAAAAAElFTkSuQmCC` + +export const generateRecoveryPdf = async ( + mnemonic: string, + email: string, + organisation: string, + name?: string +) => { + const title = 'Phase Recovery Kit' + const subtitle = `This is a recovery kit for your Phase account. \nYou can use this to recover your account keys if you forget your sudo password.` + const hostname = `${window.location.protocol}//${window.location.host}` + + // Create a new jsPDF instance + const pdf = new jsPDF() + + // Draw the black rectangle for the header + pdf.setFillColor(0, 0, 0) + pdf.rect(0, 0, pdf.internal.pageSize.getWidth(), 60, 'F') + + // Set the title + pdf.setTextColor(255, 255, 255) + pdf.setFontSize(20) + pdf.setFont('helvetica', 'bold') + pdf.text(title, 10, 25) + + // Set the subtitle + pdf.setTextColor(115, 115, 115) + pdf.setFont('helvetica', 'regular', '400') + pdf.setFontSize(11) + pdf.text(subtitle, 10, 35) + + // Add the logo + const imgProps = pdf.getImageProperties(PHASE_LOGO) + const imgWidth = 30 + const imgHeight = (imgProps.height * imgWidth) / imgProps.width // scale the height to maintain aspect ratio + const pageWidth = pdf.internal.pageSize.getWidth() + pdf.addImage(PHASE_LOGO, 'PNG', pageWidth - imgWidth - 10, 10, imgWidth, imgHeight) + + const lineSpace = 6 + const paragraphSpace = 12 + + // Define cursor x and y starting positions + let xPosition = 10 + let yPosition = 80 + + //Name + if (name) { + pdf.setTextColor(115, 115, 115) + pdf.setFont('helvetica', 'normal') + pdf.setFontSize(11) + pdf.text('Name', xPosition, yPosition) + yPosition += lineSpace + + pdf.setTextColor(23, 23, 23) + pdf.setFont('helvetica', 'bold') + pdf.setFontSize(14) + pdf.text(name, xPosition, yPosition) + yPosition += paragraphSpace + } + + //Email + pdf.setTextColor(115, 115, 115) + pdf.setFont('helvetica', 'normal') + pdf.setFontSize(11) + pdf.text('Email', xPosition, yPosition) + yPosition += lineSpace + + pdf.setTextColor(23, 23, 23) + pdf.setFont('helvetica', 'bold') + pdf.setFontSize(14) + pdf.text(email, xPosition, yPosition) + yPosition += paragraphSpace + + //Org + pdf.setTextColor(115, 115, 115) + pdf.setFont('helvetica', 'normal') + pdf.setFontSize(11) + pdf.text('Organisation', xPosition, yPosition) + yPosition += lineSpace + + pdf.setTextColor(23, 23, 23) + pdf.setFont('helvetica', 'bold') + pdf.setFontSize(14) + pdf.text(organisation, xPosition, yPosition) + yPosition += paragraphSpace + + //Phase instance host + pdf.setTextColor(115, 115, 115) + pdf.setFont('helvetica', 'normal') + pdf.setFontSize(11) + pdf.text('Login URL', xPosition, yPosition) + yPosition += lineSpace + + pdf.setTextColor(23, 23, 23) + pdf.setFont('helvetica', 'bold') + pdf.setFontSize(14) + pdf.text(hostname, xPosition, yPosition) + yPosition += paragraphSpace * 2 + + //Mnemonic + pdf.setTextColor(115, 115, 115) + pdf.setFont('helvetica', 'normal') + pdf.setFontSize(11) + pdf.text('Recovery phrase', xPosition, yPosition) + yPosition += lineSpace + + // Define the size of the grid cells + const cellWidth = pdf.internal.pageSize.getWidth() / 4 + const cellHeight = 10 + + // Split the mnemonic into words + const words = mnemonic.split(' ') + + // Loop over each word and place it in the PDF + words.forEach((word, index) => { + // Add the word number before the word + pdf.setFontSize(14) + pdf.setTextColor(23, 23, 23) + pdf.setFont('helvetica', 'bold') + pdf.text(word, xPosition, yPosition) + + // Increment the x position to the next column + xPosition += cellWidth + + // If we've reached the end of a row, reset x and increment y + if ((index + 1) % 4 === 0) { + xPosition = 10 + yPosition += cellHeight + } + }) + + yPosition += 10 + pdf.setTextColor(23, 23, 23) + pdf.setFont('helvetica', 'normal') + pdf.setFontSize(10) + pdf.text(`Generated on ${new Date().toDateString()}`, 10, 280) + + // Save the PDF + pdf.save(`phase-recovery-kit--${organisation}.pdf`) +} + +export const copyRecoveryKit = async ( + mnemonic: string, + email: string, + organisation: string, + name?: string +) => { + const hostname = `${window.location.protocol}//${window.location.host}` + + const recoveryKit = ` + Phase Recovery Kit\n\n + ${name ? `Name: ${name}` : ''}\n + Email: ${email}\n + Organsation: ${organisation}\n + LoginUrl: ${hostname}\n + Recovery phrase: ${mnemonic}\n + Generated on ${new Date().toDateString()} + ` + + const copied = await copyToClipBoard(recoveryKit) + copied ? toast.info('Copied to clipboard', { autoClose: 2000 }) : toast.error('Failed to copy') +} diff --git a/frontend/utils/tags.ts b/frontend/utils/tags.ts new file mode 100644 index 00000000..49cdc639 --- /dev/null +++ b/frontend/utils/tags.ts @@ -0,0 +1,14 @@ +import { SecretTagType } from '@/apollo/graphql' + +export const isTagSame = (tag1: SecretTagType, tag2: SecretTagType) => { + return tag1.color === tag2.color && tag1.name === tag2.name +} + +export const areTagsAreSame = (tags1: SecretTagType[], tags2: SecretTagType[]) => { + if (tags1.length !== tags2.length) return false + + const sortedTags1 = [...tags1].sort((a, b) => a.id.localeCompare(b.id)) + const sortedTags2 = [...tags2].sort((a, b) => a.id.localeCompare(b.id)) + + return sortedTags1.every((tag, index) => isTagSame(tag, sortedTags2[index])) +} diff --git a/frontend/utils/time.ts b/frontend/utils/time.ts index 7212e784..9808492a 100644 --- a/frontend/utils/time.ts +++ b/frontend/utils/time.ts @@ -31,3 +31,39 @@ export const relativeTimeFromDates = (relative: Date | null, pivot: Date = new D const elapsed = relative.getTime() - pivot.getTime() return relativeTimeFromElapsed(elapsed) } + +/** + * Get a Unix timestamp for a future time, specified in days, hours, and minutes. + * + * @param {number} [days=0] - The number of days in the future. + * @param {number} [hours=0] - The number of hours in the future. + * @param {number} [minutes=0] - The number of minutes in the future. + * @returns {number} The Unix timestamp for the future time. + */ +export const getUnixTimeStampinFuture = ( + days: number = 0, + hours: number = 0, + minutes: number = 0 +): number => { + const millisecondsInADay = 86400000 + const millisecondsInAnHour = 3600000 + const millisecondsInAMinute = 60000 + + return ( + Date.now() + + days * millisecondsInADay + + hours * millisecondsInAnHour + + minutes * millisecondsInAMinute + ) +} + +/** + * Converts a datetime string from python to a unix timestamp + * + * @param {string} datetime string + * @returns {number} + */ +export const dateToUnixTimestamp = (dateString: string): number => { + const dateObj = new Date(dateString) + return Math.floor(dateObj.getTime()) +} diff --git a/frontend/yarn.lock b/frontend/yarn.lock index 23f066ab..f345e024 100644 --- a/frontend/yarn.lock +++ b/frontend/yarn.lock @@ -4,7 +4,8 @@ "@ampproject/remapping@^2.2.0": version "2.2.1" - resolved "https://registry.npmjs.org/@ampproject/remapping/-/remapping-2.2.1.tgz" + resolved "https://registry.yarnpkg.com/@ampproject/remapping/-/remapping-2.2.1.tgz#99e8e11851128b8702cd57c33684f1d0f260b630" + integrity sha512-lFMjJTrFL3j7L9yBxwYfCq2k6qqwHyzuUl/XBnif78PWTJYyL/dfowQHWE3sp6U6ZzqWiiIZnpTMO96zhkjwtg== dependencies: "@jridgewell/gen-mapping" "^0.3.0" "@jridgewell/trace-mapping" "^0.3.9" @@ -29,7 +30,8 @@ "@ardatan/relay-compiler@12.0.0": version "12.0.0" - resolved "https://registry.npmjs.org/@ardatan/relay-compiler/-/relay-compiler-12.0.0.tgz" + resolved "https://registry.yarnpkg.com/@ardatan/relay-compiler/-/relay-compiler-12.0.0.tgz#2e4cca43088e807adc63450e8cab037020e91106" + integrity sha512-9anThAaj1dQr6IGmzBMcfzOQKTa5artjuPmw8NYK/fiGEMjADbSguBY2FMDykt+QhilR3wc9VA/3yVju7JHg7Q== dependencies: "@babel/core" "^7.14.0" "@babel/generator" "^7.14.0" @@ -51,201 +53,225 @@ "@ardatan/sync-fetch@^0.0.1": version "0.0.1" - resolved "https://registry.npmjs.org/@ardatan/sync-fetch/-/sync-fetch-0.0.1.tgz" + resolved "https://registry.yarnpkg.com/@ardatan/sync-fetch/-/sync-fetch-0.0.1.tgz#3385d3feedceb60a896518a1db857ec1e945348f" + integrity sha512-xhlTqH0m31mnsG0tIP4ETgfSB6gXDaYYsUWTrlUV93fFQPI9dd8hE0Ot6MHLCtqgB32hwJAC3YZMWlXZw7AleA== dependencies: node-fetch "^2.6.1" -"@babel/code-frame@^7.0.0", "@babel/code-frame@^7.18.6", "@babel/code-frame@^7.21.4": - version "7.21.4" - resolved "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.21.4.tgz" +"@babel/code-frame@^7.0.0", "@babel/code-frame@^7.22.10", "@babel/code-frame@^7.22.5": + version "7.22.10" + resolved "https://registry.yarnpkg.com/@babel/code-frame/-/code-frame-7.22.10.tgz#1c20e612b768fefa75f6e90d6ecb86329247f0a3" + integrity sha512-/KKIMG4UEL35WmI9OlvMhurwtytjvXoFcGNrOvyG9zIzA8YmPjVtIZUf7b05+TPO7G7/GEmLHDaoCgACHl9hhA== dependencies: - "@babel/highlight" "^7.18.6" + "@babel/highlight" "^7.22.10" + chalk "^2.4.2" -"@babel/compat-data@^7.20.5", "@babel/compat-data@^7.21.4": - version "7.21.4" - resolved "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.21.4.tgz" +"@babel/compat-data@^7.20.5", "@babel/compat-data@^7.22.9": + version "7.22.9" + resolved "https://registry.yarnpkg.com/@babel/compat-data/-/compat-data-7.22.9.tgz#71cdb00a1ce3a329ce4cbec3a44f9fef35669730" + integrity sha512-5UamI7xkUcJ3i9qVDS+KFDEK8/7oJ55/sJMB1Ge7IEapr7KfdfV/HErR+koZwOfd+SgtFKOKRhRakdg++DcJpQ== "@babel/core@^7.14.0": - version "7.21.4" - resolved "https://registry.npmjs.org/@babel/core/-/core-7.21.4.tgz" + version "7.22.10" + resolved "https://registry.yarnpkg.com/@babel/core/-/core-7.22.10.tgz#aad442c7bcd1582252cb4576747ace35bc122f35" + integrity sha512-fTmqbbUBAwCcre6zPzNngvsI0aNrPZe77AeqvDxWM9Nm+04RrJ3CAmGHA9f7lJQY6ZMhRztNemy4uslDxTX4Qw== dependencies: "@ampproject/remapping" "^2.2.0" - "@babel/code-frame" "^7.21.4" - "@babel/generator" "^7.21.4" - "@babel/helper-compilation-targets" "^7.21.4" - "@babel/helper-module-transforms" "^7.21.2" - "@babel/helpers" "^7.21.0" - "@babel/parser" "^7.21.4" - "@babel/template" "^7.20.7" - "@babel/traverse" "^7.21.4" - "@babel/types" "^7.21.4" + "@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.10" + "@babel/parser" "^7.22.10" + "@babel/template" "^7.22.5" + "@babel/traverse" "^7.22.10" + "@babel/types" "^7.22.10" convert-source-map "^1.7.0" debug "^4.1.0" gensync "^1.0.0-beta.2" json5 "^2.2.2" - semver "^6.3.0" + semver "^6.3.1" -"@babel/generator@^7.14.0", "@babel/generator@^7.18.13", "@babel/generator@^7.21.4": - version "7.21.4" - resolved "https://registry.npmjs.org/@babel/generator/-/generator-7.21.4.tgz" +"@babel/generator@^7.14.0", "@babel/generator@^7.18.13", "@babel/generator@^7.22.10": + version "7.22.10" + resolved "https://registry.yarnpkg.com/@babel/generator/-/generator-7.22.10.tgz#c92254361f398e160645ac58831069707382b722" + integrity sha512-79KIf7YiWjjdZ81JnLujDRApWtl7BxTqWD88+FFdQEIOG8LJ0etDOM7CXuIgGJa55sGOwZVwuEsaLEm0PJ5/+A== dependencies: - "@babel/types" "^7.21.4" + "@babel/types" "^7.22.10" "@jridgewell/gen-mapping" "^0.3.2" "@jridgewell/trace-mapping" "^0.3.17" jsesc "^2.5.1" -"@babel/helper-annotate-as-pure@^7.18.6": - version "7.18.6" - resolved "https://registry.npmjs.org/@babel/helper-annotate-as-pure/-/helper-annotate-as-pure-7.18.6.tgz" +"@babel/helper-annotate-as-pure@^7.22.5": + version "7.22.5" + resolved "https://registry.yarnpkg.com/@babel/helper-annotate-as-pure/-/helper-annotate-as-pure-7.22.5.tgz#e7f06737b197d580a01edf75d97e2c8be99d3882" + integrity sha512-LvBTxu8bQSQkcyKOU+a1btnNFQ1dMAd0R6PyW3arXes06F6QLWLIrd681bxRPIXlrMGR3XYnW9JyML7dP3qgxg== dependencies: - "@babel/types" "^7.18.6" + "@babel/types" "^7.22.5" -"@babel/helper-compilation-targets@^7.18.9", "@babel/helper-compilation-targets@^7.20.7", "@babel/helper-compilation-targets@^7.21.4": - version "7.21.4" - resolved "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.21.4.tgz" +"@babel/helper-compilation-targets@^7.20.7", "@babel/helper-compilation-targets@^7.22.10", "@babel/helper-compilation-targets@^7.22.5", "@babel/helper-compilation-targets@^7.22.6": + version "7.22.10" + resolved "https://registry.yarnpkg.com/@babel/helper-compilation-targets/-/helper-compilation-targets-7.22.10.tgz#01d648bbc25dd88f513d862ee0df27b7d4e67024" + integrity sha512-JMSwHD4J7SLod0idLq5PKgI+6g/hLD/iuWBq08ZX49xE14VpVEojJ5rHWptpirV2j020MvypRLAXAO50igCJ5Q== dependencies: - "@babel/compat-data" "^7.21.4" - "@babel/helper-validator-option" "^7.21.0" - browserslist "^4.21.3" + "@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.0" + semver "^6.3.1" "@babel/helper-create-class-features-plugin@^7.18.6": - version "7.21.4" - resolved "https://registry.npmjs.org/@babel/helper-create-class-features-plugin/-/helper-create-class-features-plugin-7.21.4.tgz" - dependencies: - "@babel/helper-annotate-as-pure" "^7.18.6" - "@babel/helper-environment-visitor" "^7.18.9" - "@babel/helper-function-name" "^7.21.0" - "@babel/helper-member-expression-to-functions" "^7.21.0" - "@babel/helper-optimise-call-expression" "^7.18.6" - "@babel/helper-replace-supers" "^7.20.7" - "@babel/helper-skip-transparent-expression-wrappers" "^7.20.0" - "@babel/helper-split-export-declaration" "^7.18.6" - -"@babel/helper-environment-visitor@^7.18.9": - version "7.18.9" - resolved "https://registry.npmjs.org/@babel/helper-environment-visitor/-/helper-environment-visitor-7.18.9.tgz" - -"@babel/helper-function-name@^7.18.9", "@babel/helper-function-name@^7.21.0": - version "7.21.0" - resolved "https://registry.npmjs.org/@babel/helper-function-name/-/helper-function-name-7.21.0.tgz" - dependencies: - "@babel/template" "^7.20.7" - "@babel/types" "^7.21.0" - -"@babel/helper-hoist-variables@^7.18.6": - version "7.18.6" - resolved "https://registry.npmjs.org/@babel/helper-hoist-variables/-/helper-hoist-variables-7.18.6.tgz" - dependencies: - "@babel/types" "^7.18.6" - -"@babel/helper-member-expression-to-functions@^7.20.7", "@babel/helper-member-expression-to-functions@^7.21.0": - version "7.21.0" - resolved "https://registry.npmjs.org/@babel/helper-member-expression-to-functions/-/helper-member-expression-to-functions-7.21.0.tgz" - dependencies: - "@babel/types" "^7.21.0" - -"@babel/helper-module-imports@^7.18.6": - version "7.21.4" - resolved "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.21.4.tgz" - dependencies: - "@babel/types" "^7.21.4" - -"@babel/helper-module-transforms@^7.21.2": - version "7.21.2" - resolved "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.21.2.tgz" - dependencies: - "@babel/helper-environment-visitor" "^7.18.9" - "@babel/helper-module-imports" "^7.18.6" - "@babel/helper-simple-access" "^7.20.2" - "@babel/helper-split-export-declaration" "^7.18.6" - "@babel/helper-validator-identifier" "^7.19.1" - "@babel/template" "^7.20.7" - "@babel/traverse" "^7.21.2" - "@babel/types" "^7.21.2" - -"@babel/helper-optimise-call-expression@^7.18.6": - version "7.18.6" - resolved "https://registry.npmjs.org/@babel/helper-optimise-call-expression/-/helper-optimise-call-expression-7.18.6.tgz" - dependencies: - "@babel/types" "^7.18.6" - -"@babel/helper-plugin-utils@^7.12.13", "@babel/helper-plugin-utils@^7.18.6", "@babel/helper-plugin-utils@^7.18.9", "@babel/helper-plugin-utils@^7.19.0", "@babel/helper-plugin-utils@^7.20.2", "@babel/helper-plugin-utils@^7.8.0": - version "7.20.2" - resolved "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.20.2.tgz" - -"@babel/helper-replace-supers@^7.18.6", "@babel/helper-replace-supers@^7.20.7": - version "7.20.7" - resolved "https://registry.npmjs.org/@babel/helper-replace-supers/-/helper-replace-supers-7.20.7.tgz" - dependencies: - "@babel/helper-environment-visitor" "^7.18.9" - "@babel/helper-member-expression-to-functions" "^7.20.7" - "@babel/helper-optimise-call-expression" "^7.18.6" - "@babel/template" "^7.20.7" - "@babel/traverse" "^7.20.7" - "@babel/types" "^7.20.7" - -"@babel/helper-simple-access@^7.20.2": - version "7.20.2" - resolved "https://registry.npmjs.org/@babel/helper-simple-access/-/helper-simple-access-7.20.2.tgz" + version "7.22.10" + resolved "https://registry.yarnpkg.com/@babel/helper-create-class-features-plugin/-/helper-create-class-features-plugin-7.22.10.tgz#dd2612d59eac45588021ac3d6fa976d08f4e95a3" + integrity sha512-5IBb77txKYQPpOEdUdIhBx8VrZyDCQ+H82H0+5dX1TmuscP5vJKEE3cKurjtIw/vFwzbVH48VweE78kVDBrqjA== + 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" + +"@babel/helper-environment-visitor@^7.22.5": + version "7.22.5" + resolved "https://registry.yarnpkg.com/@babel/helper-environment-visitor/-/helper-environment-visitor-7.22.5.tgz#f06dd41b7c1f44e1f8da6c4055b41ab3a09a7e98" + integrity sha512-XGmhECfVA/5sAt+H+xpSg0mfrHq6FzNr9Oxh7PSEBBRUb/mL7Kz3NICXb194rCqAEdxkhPT1a88teizAFyvk8Q== + +"@babel/helper-function-name@^7.22.5": + version "7.22.5" + resolved "https://registry.yarnpkg.com/@babel/helper-function-name/-/helper-function-name-7.22.5.tgz#ede300828905bb15e582c037162f99d5183af1be" + integrity sha512-wtHSq6jMRE3uF2otvfuD3DIvVhOsSNshQl0Qrd7qC9oQJzHvOL4qQXlQn2916+CXGywIjpGuIkoyZRRxHPiNQQ== + dependencies: + "@babel/template" "^7.22.5" + "@babel/types" "^7.22.5" + +"@babel/helper-hoist-variables@^7.22.5": + version "7.22.5" + resolved "https://registry.yarnpkg.com/@babel/helper-hoist-variables/-/helper-hoist-variables-7.22.5.tgz#c01a007dac05c085914e8fb652b339db50d823bb" + integrity sha512-wGjk9QZVzvknA6yKIUURb8zY3grXCcOZt+/7Wcy8O2uctxhplmUPkOdlgoNhmdVee2c92JXbf1xpMtVNbfoxRw== + dependencies: + "@babel/types" "^7.22.5" + +"@babel/helper-member-expression-to-functions@^7.22.5": + version "7.22.5" + resolved "https://registry.yarnpkg.com/@babel/helper-member-expression-to-functions/-/helper-member-expression-to-functions-7.22.5.tgz#0a7c56117cad3372fbf8d2fb4bf8f8d64a1e76b2" + integrity sha512-aBiH1NKMG0H2cGZqspNvsaBe6wNGjbJjuLy29aU+eDZjSbbN53BaxlpB02xm9v34pLTZ1nIQPFYn2qMZoa5BQQ== + dependencies: + "@babel/types" "^7.22.5" + +"@babel/helper-module-imports@^7.22.5": + version "7.22.5" + resolved "https://registry.yarnpkg.com/@babel/helper-module-imports/-/helper-module-imports-7.22.5.tgz#1a8f4c9f4027d23f520bd76b364d44434a72660c" + integrity sha512-8Dl6+HD/cKifutF5qGd/8ZJi84QeAKh+CEe1sBzz8UayBBGg1dAIJrdHOcOM5b2MpzWL2yuotJTtGjETq0qjXg== + dependencies: + "@babel/types" "^7.22.5" + +"@babel/helper-module-transforms@^7.22.5", "@babel/helper-module-transforms@^7.22.9": + version "7.22.9" + resolved "https://registry.yarnpkg.com/@babel/helper-module-transforms/-/helper-module-transforms-7.22.9.tgz#92dfcb1fbbb2bc62529024f72d942a8c97142129" + integrity sha512-t+WA2Xn5K+rTeGtC8jCsdAH52bjggG5TKRuRrAGNM/mjIbO4GxvlLMFOEz9wXY5I2XQ60PMFsAG2WIcG82dQMQ== + 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" + +"@babel/helper-optimise-call-expression@^7.22.5": + version "7.22.5" + resolved "https://registry.yarnpkg.com/@babel/helper-optimise-call-expression/-/helper-optimise-call-expression-7.22.5.tgz#f21531a9ccbff644fdd156b4077c16ff0c3f609e" + integrity sha512-HBwaojN0xFRx4yIvpwGqxiV2tUfl7401jlok564NgB9EHS1y6QT17FmKWm4ztqjeVdXLuC4fSvHc5ePpQjoTbw== + dependencies: + "@babel/types" "^7.22.5" + +"@babel/helper-plugin-utils@^7.12.13", "@babel/helper-plugin-utils@^7.18.6", "@babel/helper-plugin-utils@^7.20.2", "@babel/helper-plugin-utils@^7.22.5", "@babel/helper-plugin-utils@^7.8.0": + version "7.22.5" + resolved "https://registry.yarnpkg.com/@babel/helper-plugin-utils/-/helper-plugin-utils-7.22.5.tgz#dd7ee3735e8a313b9f7b05a773d892e88e6d7295" + integrity sha512-uLls06UVKgFG9QD4OeFYLEGteMIAa5kpTPcFL28yuCIIzsf6ZyKZMllKVOCZFhiZ5ptnwX4mtKdWCBE/uT4amg== + +"@babel/helper-replace-supers@^7.22.5", "@babel/helper-replace-supers@^7.22.9": + version "7.22.9" + resolved "https://registry.yarnpkg.com/@babel/helper-replace-supers/-/helper-replace-supers-7.22.9.tgz#cbdc27d6d8d18cd22c81ae4293765a5d9afd0779" + integrity sha512-LJIKvvpgPOPUThdYqcX6IXRuIcTkcAub0IaDRGCZH0p5GPUp7PhRU9QVgFcDDd51BaPkk77ZjqFwh6DZTAEmGg== + 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" + +"@babel/helper-simple-access@^7.22.5": + version "7.22.5" + resolved "https://registry.yarnpkg.com/@babel/helper-simple-access/-/helper-simple-access-7.22.5.tgz#4938357dc7d782b80ed6dbb03a0fba3d22b1d5de" + integrity sha512-n0H99E/K+Bika3++WNL17POvo4rKWZ7lZEp1Q+fStVbUi8nxPQEBOlTmCOxW/0JsS56SKKQ+ojAe2pHKJHN35w== + dependencies: + "@babel/types" "^7.22.5" + +"@babel/helper-skip-transparent-expression-wrappers@^7.22.5": + version "7.22.5" + resolved "https://registry.yarnpkg.com/@babel/helper-skip-transparent-expression-wrappers/-/helper-skip-transparent-expression-wrappers-7.22.5.tgz#007f15240b5751c537c40e77abb4e89eeaaa8847" + integrity sha512-tK14r66JZKiC43p8Ki33yLBVJKlQDFoA8GYN67lWCDCqoL6EMMSuM9b+Iff2jHaM/RRFYl7K+iiru7hbRqNx8Q== + dependencies: + "@babel/types" "^7.22.5" + +"@babel/helper-split-export-declaration@^7.22.6": + version "7.22.6" + resolved "https://registry.yarnpkg.com/@babel/helper-split-export-declaration/-/helper-split-export-declaration-7.22.6.tgz#322c61b7310c0997fe4c323955667f18fcefb91c" + integrity sha512-AsUnxuLhRYsisFiaJwvp1QF+I3KjD5FOxut14q/GzovUe6orHLesW2C7d754kRm53h5gqrz6sFl6sxc4BVtE/g== + dependencies: + "@babel/types" "^7.22.5" + +"@babel/helper-string-parser@^7.22.5": + version "7.22.5" + resolved "https://registry.yarnpkg.com/@babel/helper-string-parser/-/helper-string-parser-7.22.5.tgz#533f36457a25814cf1df6488523ad547d784a99f" + integrity sha512-mM4COjgZox8U+JcXQwPijIZLElkgEpO5rsERVDJTc2qfCDfERyob6k5WegS14SX18IIjv+XD+GrqNumY5JRCDw== + +"@babel/helper-validator-identifier@^7.22.5": + version "7.22.5" + resolved "https://registry.yarnpkg.com/@babel/helper-validator-identifier/-/helper-validator-identifier-7.22.5.tgz#9544ef6a33999343c8740fa51350f30eeaaaf193" + integrity sha512-aJXu+6lErq8ltp+JhkJUfk1MTGyuA4v7f3pA+BJ5HLfNC6nAQ0Cpi9uOquUj8Hehg0aUiHzWQbOVJGao6ztBAQ== + +"@babel/helper-validator-option@^7.22.5": + version "7.22.5" + resolved "https://registry.yarnpkg.com/@babel/helper-validator-option/-/helper-validator-option-7.22.5.tgz#de52000a15a177413c8234fa3a8af4ee8102d0ac" + integrity sha512-R3oB6xlIVKUnxNUxbmgq7pKjxpru24zlimpE8WK47fACIlM0II/Hm1RS8IaOI7NgCr6LNS+jl5l75m20npAziw== + +"@babel/helpers@^7.22.10": + version "7.22.10" + resolved "https://registry.yarnpkg.com/@babel/helpers/-/helpers-7.22.10.tgz#ae6005c539dfbcb5cd71fb51bfc8a52ba63bc37a" + integrity sha512-a41J4NW8HyZa1I1vAndrraTlPZ/eZoga2ZgS7fEr0tZJGVU4xqdE80CEm0CcNjha5EZ8fTBYLKHF0kqDUuAwQw== + dependencies: + "@babel/template" "^7.22.5" + "@babel/traverse" "^7.22.10" + "@babel/types" "^7.22.10" + +"@babel/highlight@^7.22.10": + version "7.22.10" + resolved "https://registry.yarnpkg.com/@babel/highlight/-/highlight-7.22.10.tgz#02a3f6d8c1cb4521b2fd0ab0da8f4739936137d7" + integrity sha512-78aUtVcT7MUscr0K5mIEnkwxPE0MaxkR5RxRwuHaQ+JuU5AmTPhY+do2mdzVTnIJJpyBglql2pehuBIWHug+WQ== dependencies: - "@babel/types" "^7.20.2" - -"@babel/helper-skip-transparent-expression-wrappers@^7.20.0": - version "7.20.0" - resolved "https://registry.npmjs.org/@babel/helper-skip-transparent-expression-wrappers/-/helper-skip-transparent-expression-wrappers-7.20.0.tgz" - dependencies: - "@babel/types" "^7.20.0" - -"@babel/helper-split-export-declaration@^7.18.6": - version "7.18.6" - resolved "https://registry.npmjs.org/@babel/helper-split-export-declaration/-/helper-split-export-declaration-7.18.6.tgz" - dependencies: - "@babel/types" "^7.18.6" - -"@babel/helper-string-parser@^7.19.4": - version "7.19.4" - resolved "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.19.4.tgz" - -"@babel/helper-validator-identifier@^7.18.6", "@babel/helper-validator-identifier@^7.19.1": - version "7.19.1" - resolved "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.19.1.tgz" - -"@babel/helper-validator-option@^7.21.0": - version "7.21.0" - resolved "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.21.0.tgz" - -"@babel/helpers@^7.21.0": - version "7.21.0" - resolved "https://registry.npmjs.org/@babel/helpers/-/helpers-7.21.0.tgz" - dependencies: - "@babel/template" "^7.20.7" - "@babel/traverse" "^7.21.0" - "@babel/types" "^7.21.0" - -"@babel/highlight@^7.18.6": - version "7.18.6" - resolved "https://registry.npmjs.org/@babel/highlight/-/highlight-7.18.6.tgz" - dependencies: - "@babel/helper-validator-identifier" "^7.18.6" - chalk "^2.0.0" + "@babel/helper-validator-identifier" "^7.22.5" + chalk "^2.4.2" js-tokens "^4.0.0" -"@babel/parser@^7.14.0", "@babel/parser@^7.16.8", "@babel/parser@^7.20.7", "@babel/parser@^7.21.4": - version "7.21.4" - resolved "https://registry.npmjs.org/@babel/parser/-/parser-7.21.4.tgz" +"@babel/parser@^7.14.0", "@babel/parser@^7.16.8", "@babel/parser@^7.22.10", "@babel/parser@^7.22.5": + version "7.22.10" + resolved "https://registry.yarnpkg.com/@babel/parser/-/parser-7.22.10.tgz#e37634f9a12a1716136c44624ef54283cabd3f55" + integrity sha512-lNbdGsQb9ekfsnjFGhEiF4hfFqGgfOP3H3d27re3n+CGhNuTSUEQdfWk556sTLNTloczcdM5TYF2LhzmDQKyvQ== "@babel/plugin-proposal-class-properties@^7.0.0": version "7.18.6" - resolved "https://registry.npmjs.org/@babel/plugin-proposal-class-properties/-/plugin-proposal-class-properties-7.18.6.tgz" + resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-class-properties/-/plugin-proposal-class-properties-7.18.6.tgz#b110f59741895f7ec21a6fff696ec46265c446a3" + integrity sha512-cumfXOF0+nzZrrN8Rf0t7M+tF6sZc7vhQwYQck9q1/5w2OExlD+b4v4RpMJFaV1Z7WcDRgO6FqvxqxGlwo+RHQ== dependencies: "@babel/helper-create-class-features-plugin" "^7.18.6" "@babel/helper-plugin-utils" "^7.18.6" "@babel/plugin-proposal-object-rest-spread@^7.0.0": version "7.20.7" - resolved "https://registry.npmjs.org/@babel/plugin-proposal-object-rest-spread/-/plugin-proposal-object-rest-spread-7.20.7.tgz" + resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-object-rest-spread/-/plugin-proposal-object-rest-spread-7.20.7.tgz#aa662940ef425779c75534a5c41e9d936edc390a" + integrity sha512-d2S98yCiLxDVmBmE8UjGcfPvNEUbA1U5q5WxaWFUGRzJSVAZqm5W6MbPct0jxnegUZ0niLeNX+IOzEs7wYg9Dg== dependencies: "@babel/compat-data" "^7.20.5" "@babel/helper-compilation-targets" "^7.20.7" @@ -255,209 +281,251 @@ "@babel/plugin-syntax-class-properties@^7.0.0": version "7.12.13" - resolved "https://registry.npmjs.org/@babel/plugin-syntax-class-properties/-/plugin-syntax-class-properties-7.12.13.tgz" + resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-class-properties/-/plugin-syntax-class-properties-7.12.13.tgz#b5c987274c4a3a82b89714796931a6b53544ae10" + integrity sha512-fm4idjKla0YahUNgFNLCB0qySdsoPiZP3iQE3rky0mBUtMZ23yDJ9SJdg6dXTSDnulOVqiF3Hgr9nbXvXTQZYA== dependencies: "@babel/helper-plugin-utils" "^7.12.13" -"@babel/plugin-syntax-flow@^7.0.0", "@babel/plugin-syntax-flow@^7.18.6": - version "7.21.4" - resolved "https://registry.npmjs.org/@babel/plugin-syntax-flow/-/plugin-syntax-flow-7.21.4.tgz" +"@babel/plugin-syntax-flow@^7.0.0", "@babel/plugin-syntax-flow@^7.22.5": + version "7.22.5" + resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-flow/-/plugin-syntax-flow-7.22.5.tgz#163b820b9e7696ce134df3ee716d9c0c98035859" + integrity sha512-9RdCl0i+q0QExayk2nOS7853w08yLucnnPML6EN9S8fgMPVtdLDCdx/cOQ/i44Lb9UeQX9A35yaqBBOMMZxPxQ== dependencies: - "@babel/helper-plugin-utils" "^7.20.2" + "@babel/helper-plugin-utils" "^7.22.5" -"@babel/plugin-syntax-import-assertions@7.20.0": - version "7.20.0" - resolved "https://registry.npmjs.org/@babel/plugin-syntax-import-assertions/-/plugin-syntax-import-assertions-7.20.0.tgz" +"@babel/plugin-syntax-import-assertions@^7.20.0": + version "7.22.5" + resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-import-assertions/-/plugin-syntax-import-assertions-7.22.5.tgz#07d252e2aa0bc6125567f742cd58619cb14dce98" + integrity sha512-rdV97N7KqsRzeNGoWUOK6yUsWarLjE5Su/Snk9IYPU9CwkWHs4t+rTGOvffTR8XGkJMTAdLfO0xVnXm8wugIJg== dependencies: - "@babel/helper-plugin-utils" "^7.19.0" + "@babel/helper-plugin-utils" "^7.22.5" -"@babel/plugin-syntax-jsx@^7.0.0", "@babel/plugin-syntax-jsx@^7.18.6": - version "7.21.4" - resolved "https://registry.npmjs.org/@babel/plugin-syntax-jsx/-/plugin-syntax-jsx-7.21.4.tgz" +"@babel/plugin-syntax-jsx@^7.0.0", "@babel/plugin-syntax-jsx@^7.22.5": + version "7.22.5" + resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-jsx/-/plugin-syntax-jsx-7.22.5.tgz#a6b68e84fb76e759fc3b93e901876ffabbe1d918" + integrity sha512-gvyP4hZrgrs/wWMaocvxZ44Hw0b3W8Pe+cMxc8V1ULQ07oh8VNbIRaoD1LRZVTvD+0nieDKjfgKg89sD7rrKrg== dependencies: - "@babel/helper-plugin-utils" "^7.20.2" + "@babel/helper-plugin-utils" "^7.22.5" "@babel/plugin-syntax-object-rest-spread@^7.0.0", "@babel/plugin-syntax-object-rest-spread@^7.8.3": version "7.8.3" - resolved "https://registry.npmjs.org/@babel/plugin-syntax-object-rest-spread/-/plugin-syntax-object-rest-spread-7.8.3.tgz" + resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-object-rest-spread/-/plugin-syntax-object-rest-spread-7.8.3.tgz#60e225edcbd98a640332a2e72dd3e66f1af55871" + integrity sha512-XoqMijGZb9y3y2XskN+P1wUGiVwWZ5JmoDRwx5+3GmEplNyVM2s2Dg8ILFQm8rWM48orGy5YpI5Bl8U1y7ydlA== dependencies: "@babel/helper-plugin-utils" "^7.8.0" "@babel/plugin-transform-arrow-functions@^7.0.0": - version "7.20.7" - resolved "https://registry.npmjs.org/@babel/plugin-transform-arrow-functions/-/plugin-transform-arrow-functions-7.20.7.tgz" + version "7.22.5" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-arrow-functions/-/plugin-transform-arrow-functions-7.22.5.tgz#e5ba566d0c58a5b2ba2a8b795450641950b71958" + integrity sha512-26lTNXoVRdAnsaDXPpvCNUq+OVWEVC6bx7Vvz9rC53F2bagUWW4u4ii2+h8Fejfh7RYqPxn+libeFBBck9muEw== dependencies: - "@babel/helper-plugin-utils" "^7.20.2" + "@babel/helper-plugin-utils" "^7.22.5" "@babel/plugin-transform-block-scoped-functions@^7.0.0": - version "7.18.6" - resolved "https://registry.npmjs.org/@babel/plugin-transform-block-scoped-functions/-/plugin-transform-block-scoped-functions-7.18.6.tgz" + version "7.22.5" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-block-scoped-functions/-/plugin-transform-block-scoped-functions-7.22.5.tgz#27978075bfaeb9fa586d3cb63a3d30c1de580024" + integrity sha512-tdXZ2UdknEKQWKJP1KMNmuF5Lx3MymtMN/pvA+p/VEkhK8jVcQ1fzSy8KM9qRYhAf2/lV33hoMPKI/xaI9sADA== dependencies: - "@babel/helper-plugin-utils" "^7.18.6" + "@babel/helper-plugin-utils" "^7.22.5" "@babel/plugin-transform-block-scoping@^7.0.0": - version "7.21.0" - resolved "https://registry.npmjs.org/@babel/plugin-transform-block-scoping/-/plugin-transform-block-scoping-7.21.0.tgz" + version "7.22.10" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-block-scoping/-/plugin-transform-block-scoping-7.22.10.tgz#88a1dccc3383899eb5e660534a76a22ecee64faa" + integrity sha512-1+kVpGAOOI1Albt6Vse7c8pHzcZQdQKW+wJH+g8mCaszOdDVwRXa/slHPqIw+oJAJANTKDMuM2cBdV0Dg618Vg== dependencies: - "@babel/helper-plugin-utils" "^7.20.2" + "@babel/helper-plugin-utils" "^7.22.5" "@babel/plugin-transform-classes@^7.0.0": - version "7.21.0" - resolved "https://registry.npmjs.org/@babel/plugin-transform-classes/-/plugin-transform-classes-7.21.0.tgz" - dependencies: - "@babel/helper-annotate-as-pure" "^7.18.6" - "@babel/helper-compilation-targets" "^7.20.7" - "@babel/helper-environment-visitor" "^7.18.9" - "@babel/helper-function-name" "^7.21.0" - "@babel/helper-optimise-call-expression" "^7.18.6" - "@babel/helper-plugin-utils" "^7.20.2" - "@babel/helper-replace-supers" "^7.20.7" - "@babel/helper-split-export-declaration" "^7.18.6" + version "7.22.6" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-classes/-/plugin-transform-classes-7.22.6.tgz#e04d7d804ed5b8501311293d1a0e6d43e94c3363" + integrity sha512-58EgM6nuPNG6Py4Z3zSuu0xWu2VfodiMi72Jt5Kj2FECmaYk1RrTXA45z6KBFsu9tRgwQDwIiY4FXTt+YsSFAQ== + dependencies: + "@babel/helper-annotate-as-pure" "^7.22.5" + "@babel/helper-compilation-targets" "^7.22.6" + "@babel/helper-environment-visitor" "^7.22.5" + "@babel/helper-function-name" "^7.22.5" + "@babel/helper-optimise-call-expression" "^7.22.5" + "@babel/helper-plugin-utils" "^7.22.5" + "@babel/helper-replace-supers" "^7.22.5" + "@babel/helper-split-export-declaration" "^7.22.6" globals "^11.1.0" "@babel/plugin-transform-computed-properties@^7.0.0": - version "7.20.7" - resolved "https://registry.npmjs.org/@babel/plugin-transform-computed-properties/-/plugin-transform-computed-properties-7.20.7.tgz" + version "7.22.5" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-computed-properties/-/plugin-transform-computed-properties-7.22.5.tgz#cd1e994bf9f316bd1c2dafcd02063ec261bb3869" + integrity sha512-4GHWBgRf0krxPX+AaPtgBAlTgTeZmqDynokHOX7aqqAB4tHs3U2Y02zH6ETFdLZGcg9UQSD1WCmkVrE9ErHeOg== dependencies: - "@babel/helper-plugin-utils" "^7.20.2" - "@babel/template" "^7.20.7" + "@babel/helper-plugin-utils" "^7.22.5" + "@babel/template" "^7.22.5" "@babel/plugin-transform-destructuring@^7.0.0": - version "7.21.3" - resolved "https://registry.npmjs.org/@babel/plugin-transform-destructuring/-/plugin-transform-destructuring-7.21.3.tgz" + version "7.22.10" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-destructuring/-/plugin-transform-destructuring-7.22.10.tgz#38e2273814a58c810b6c34ea293be4973c4eb5e2" + integrity sha512-dPJrL0VOyxqLM9sritNbMSGx/teueHF/htMKrPT7DNxccXxRDPYqlgPFFdr8u+F+qUZOkZoXue/6rL5O5GduEw== dependencies: - "@babel/helper-plugin-utils" "^7.20.2" + "@babel/helper-plugin-utils" "^7.22.5" "@babel/plugin-transform-flow-strip-types@^7.0.0": - version "7.21.0" - resolved "https://registry.npmjs.org/@babel/plugin-transform-flow-strip-types/-/plugin-transform-flow-strip-types-7.21.0.tgz" + version "7.22.5" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-flow-strip-types/-/plugin-transform-flow-strip-types-7.22.5.tgz#0bb17110c7bf5b35a60754b2f00c58302381dee2" + integrity sha512-tujNbZdxdG0/54g/oua8ISToaXTFBf8EnSb5PgQSciIXWOWKX3S4+JR7ZE9ol8FZwf9kxitzkGQ+QWeov/mCiA== dependencies: - "@babel/helper-plugin-utils" "^7.20.2" - "@babel/plugin-syntax-flow" "^7.18.6" + "@babel/helper-plugin-utils" "^7.22.5" + "@babel/plugin-syntax-flow" "^7.22.5" "@babel/plugin-transform-for-of@^7.0.0": - version "7.21.0" - resolved "https://registry.npmjs.org/@babel/plugin-transform-for-of/-/plugin-transform-for-of-7.21.0.tgz" + version "7.22.5" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-for-of/-/plugin-transform-for-of-7.22.5.tgz#ab1b8a200a8f990137aff9a084f8de4099ab173f" + integrity sha512-3kxQjX1dU9uudwSshyLeEipvrLjBCVthCgeTp6CzE/9JYrlAIaeekVxRpCWsDDfYTfRZRoCeZatCQvwo+wvK8A== dependencies: - "@babel/helper-plugin-utils" "^7.20.2" + "@babel/helper-plugin-utils" "^7.22.5" "@babel/plugin-transform-function-name@^7.0.0": - version "7.18.9" - resolved "https://registry.npmjs.org/@babel/plugin-transform-function-name/-/plugin-transform-function-name-7.18.9.tgz" + version "7.22.5" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-function-name/-/plugin-transform-function-name-7.22.5.tgz#935189af68b01898e0d6d99658db6b164205c143" + integrity sha512-UIzQNMS0p0HHiQm3oelztj+ECwFnj+ZRV4KnguvlsD2of1whUeM6o7wGNj6oLwcDoAXQ8gEqfgC24D+VdIcevg== dependencies: - "@babel/helper-compilation-targets" "^7.18.9" - "@babel/helper-function-name" "^7.18.9" - "@babel/helper-plugin-utils" "^7.18.9" + "@babel/helper-compilation-targets" "^7.22.5" + "@babel/helper-function-name" "^7.22.5" + "@babel/helper-plugin-utils" "^7.22.5" "@babel/plugin-transform-literals@^7.0.0": - version "7.18.9" - resolved "https://registry.npmjs.org/@babel/plugin-transform-literals/-/plugin-transform-literals-7.18.9.tgz" + version "7.22.5" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-literals/-/plugin-transform-literals-7.22.5.tgz#e9341f4b5a167952576e23db8d435849b1dd7920" + integrity sha512-fTLj4D79M+mepcw3dgFBTIDYpbcB9Sm0bpm4ppXPaO+U+PKFFyV9MGRvS0gvGw62sd10kT5lRMKXAADb9pWy8g== dependencies: - "@babel/helper-plugin-utils" "^7.18.9" + "@babel/helper-plugin-utils" "^7.22.5" "@babel/plugin-transform-member-expression-literals@^7.0.0": - version "7.18.6" - resolved "https://registry.npmjs.org/@babel/plugin-transform-member-expression-literals/-/plugin-transform-member-expression-literals-7.18.6.tgz" + version "7.22.5" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-member-expression-literals/-/plugin-transform-member-expression-literals-7.22.5.tgz#4fcc9050eded981a468347dd374539ed3e058def" + integrity sha512-RZEdkNtzzYCFl9SE9ATaUMTj2hqMb4StarOJLrZRbqqU4HSBE7UlBw9WBWQiDzrJZJdUWiMTVDI6Gv/8DPvfew== dependencies: - "@babel/helper-plugin-utils" "^7.18.6" + "@babel/helper-plugin-utils" "^7.22.5" "@babel/plugin-transform-modules-commonjs@^7.0.0": - version "7.21.2" - resolved "https://registry.npmjs.org/@babel/plugin-transform-modules-commonjs/-/plugin-transform-modules-commonjs-7.21.2.tgz" + version "7.22.5" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-modules-commonjs/-/plugin-transform-modules-commonjs-7.22.5.tgz#7d9875908d19b8c0536085af7b053fd5bd651bfa" + integrity sha512-B4pzOXj+ONRmuaQTg05b3y/4DuFz3WcCNAXPLb2Q0GT0TrGKGxNKV4jwsXts+StaM0LQczZbOpj8o1DLPDJIiA== dependencies: - "@babel/helper-module-transforms" "^7.21.2" - "@babel/helper-plugin-utils" "^7.20.2" - "@babel/helper-simple-access" "^7.20.2" + "@babel/helper-module-transforms" "^7.22.5" + "@babel/helper-plugin-utils" "^7.22.5" + "@babel/helper-simple-access" "^7.22.5" "@babel/plugin-transform-object-super@^7.0.0": - version "7.18.6" - resolved "https://registry.npmjs.org/@babel/plugin-transform-object-super/-/plugin-transform-object-super-7.18.6.tgz" + version "7.22.5" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-object-super/-/plugin-transform-object-super-7.22.5.tgz#794a8d2fcb5d0835af722173c1a9d704f44e218c" + integrity sha512-klXqyaT9trSjIUrcsYIfETAzmOEZL3cBYqOYLJxBHfMFFggmXOv+NYSX/Jbs9mzMVESw/WycLFPRx8ba/b2Ipw== dependencies: - "@babel/helper-plugin-utils" "^7.18.6" - "@babel/helper-replace-supers" "^7.18.6" + "@babel/helper-plugin-utils" "^7.22.5" + "@babel/helper-replace-supers" "^7.22.5" "@babel/plugin-transform-parameters@^7.0.0", "@babel/plugin-transform-parameters@^7.20.7": - version "7.21.3" - resolved "https://registry.npmjs.org/@babel/plugin-transform-parameters/-/plugin-transform-parameters-7.21.3.tgz" + version "7.22.5" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-parameters/-/plugin-transform-parameters-7.22.5.tgz#c3542dd3c39b42c8069936e48717a8d179d63a18" + integrity sha512-AVkFUBurORBREOmHRKo06FjHYgjrabpdqRSwq6+C7R5iTCZOsM4QbcB27St0a4U6fffyAOqh3s/qEfybAhfivg== dependencies: - "@babel/helper-plugin-utils" "^7.20.2" + "@babel/helper-plugin-utils" "^7.22.5" "@babel/plugin-transform-property-literals@^7.0.0": - version "7.18.6" - resolved "https://registry.npmjs.org/@babel/plugin-transform-property-literals/-/plugin-transform-property-literals-7.18.6.tgz" + version "7.22.5" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-property-literals/-/plugin-transform-property-literals-7.22.5.tgz#b5ddabd73a4f7f26cd0e20f5db48290b88732766" + integrity sha512-TiOArgddK3mK/x1Qwf5hay2pxI6wCZnvQqrFSqbtg1GLl2JcNMitVH/YnqjP+M31pLUeTfzY1HAXFDnUBV30rQ== dependencies: - "@babel/helper-plugin-utils" "^7.18.6" + "@babel/helper-plugin-utils" "^7.22.5" "@babel/plugin-transform-react-display-name@^7.0.0": - version "7.18.6" - resolved "https://registry.npmjs.org/@babel/plugin-transform-react-display-name/-/plugin-transform-react-display-name-7.18.6.tgz" + version "7.22.5" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-react-display-name/-/plugin-transform-react-display-name-7.22.5.tgz#3c4326f9fce31c7968d6cb9debcaf32d9e279a2b" + integrity sha512-PVk3WPYudRF5z4GKMEYUrLjPl38fJSKNaEOkFuoprioowGuWN6w2RKznuFNSlJx7pzzXXStPUnNSOEO0jL5EVw== dependencies: - "@babel/helper-plugin-utils" "^7.18.6" + "@babel/helper-plugin-utils" "^7.22.5" "@babel/plugin-transform-react-jsx@^7.0.0": - version "7.21.0" - resolved "https://registry.npmjs.org/@babel/plugin-transform-react-jsx/-/plugin-transform-react-jsx-7.21.0.tgz" + version "7.22.5" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-react-jsx/-/plugin-transform-react-jsx-7.22.5.tgz#932c291eb6dd1153359e2a90cb5e557dcf068416" + integrity sha512-rog5gZaVbUip5iWDMTYbVM15XQq+RkUKhET/IHR6oizR+JEoN6CAfTTuHcK4vwUyzca30qqHqEpzBOnaRMWYMA== dependencies: - "@babel/helper-annotate-as-pure" "^7.18.6" - "@babel/helper-module-imports" "^7.18.6" - "@babel/helper-plugin-utils" "^7.20.2" - "@babel/plugin-syntax-jsx" "^7.18.6" - "@babel/types" "^7.21.0" + "@babel/helper-annotate-as-pure" "^7.22.5" + "@babel/helper-module-imports" "^7.22.5" + "@babel/helper-plugin-utils" "^7.22.5" + "@babel/plugin-syntax-jsx" "^7.22.5" + "@babel/types" "^7.22.5" "@babel/plugin-transform-shorthand-properties@^7.0.0": - version "7.18.6" - resolved "https://registry.npmjs.org/@babel/plugin-transform-shorthand-properties/-/plugin-transform-shorthand-properties-7.18.6.tgz" + version "7.22.5" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-shorthand-properties/-/plugin-transform-shorthand-properties-7.22.5.tgz#6e277654be82b5559fc4b9f58088507c24f0c624" + integrity sha512-vM4fq9IXHscXVKzDv5itkO1X52SmdFBFcMIBZ2FRn2nqVYqw6dBexUgMvAjHW+KXpPPViD/Yo3GrDEBaRC0QYA== dependencies: - "@babel/helper-plugin-utils" "^7.18.6" + "@babel/helper-plugin-utils" "^7.22.5" "@babel/plugin-transform-spread@^7.0.0": - version "7.20.7" - resolved "https://registry.npmjs.org/@babel/plugin-transform-spread/-/plugin-transform-spread-7.20.7.tgz" + version "7.22.5" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-spread/-/plugin-transform-spread-7.22.5.tgz#6487fd29f229c95e284ba6c98d65eafb893fea6b" + integrity sha512-5ZzDQIGyvN4w8+dMmpohL6MBo+l2G7tfC/O2Dg7/hjpgeWvUx8FzfeOKxGog9IimPa4YekaQ9PlDqTLOljkcxg== dependencies: - "@babel/helper-plugin-utils" "^7.20.2" - "@babel/helper-skip-transparent-expression-wrappers" "^7.20.0" + "@babel/helper-plugin-utils" "^7.22.5" + "@babel/helper-skip-transparent-expression-wrappers" "^7.22.5" "@babel/plugin-transform-template-literals@^7.0.0": - version "7.18.9" - resolved "https://registry.npmjs.org/@babel/plugin-transform-template-literals/-/plugin-transform-template-literals-7.18.9.tgz" + version "7.22.5" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-template-literals/-/plugin-transform-template-literals-7.22.5.tgz#8f38cf291e5f7a8e60e9f733193f0bcc10909bff" + integrity sha512-5ciOehRNf+EyUeewo8NkbQiUs4d6ZxiHo6BcBcnFlgiJfu16q0bQUw9Jvo0b0gBKFG1SMhDSjeKXSYuJLeFSMA== dependencies: - "@babel/helper-plugin-utils" "^7.18.9" + "@babel/helper-plugin-utils" "^7.22.5" -"@babel/runtime@^7.0.0", "@babel/runtime@^7.20.13", "@babel/runtime@^7.20.7", "@babel/runtime@^7.21.0": +"@babel/runtime@^7.0.0": + version "7.22.10" + resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.22.10.tgz#ae3e9631fd947cb7e3610d3e9d8fef5f76696682" + integrity sha512-21t/fkKLMZI4pqP2wlmsQAWnYW1PDyKyyUV4vCi+B25ydmdaYTKXPwCj0BzSUnZf4seIiYvSA3jcZ3gdsMFkLQ== + dependencies: + regenerator-runtime "^0.14.0" + +"@babel/runtime@^7.12.5", "@babel/runtime@^7.14.0": + version "7.23.2" + resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.23.2.tgz#062b0ac103261d68a966c4c7baf2ae3e62ec3885" + integrity sha512-mM8eg4yl5D6i3lu2QKPuPH4FArvJ8KhTofbE7jwMUv9KX5mBvwPAqnV3MlyBNqdp9RyRKP6Yck8TrfYrPvX3bg== + dependencies: + regenerator-runtime "^0.14.0" + +"@babel/runtime@^7.20.13", "@babel/runtime@^7.20.7", "@babel/runtime@^7.21.0": version "7.21.0" resolved "https://registry.npmjs.org/@babel/runtime/-/runtime-7.21.0.tgz" dependencies: regenerator-runtime "^0.13.11" -"@babel/template@^7.18.10", "@babel/template@^7.20.7": - version "7.20.7" - resolved "https://registry.npmjs.org/@babel/template/-/template-7.20.7.tgz" - dependencies: - "@babel/code-frame" "^7.18.6" - "@babel/parser" "^7.20.7" - "@babel/types" "^7.20.7" - -"@babel/traverse@^7.14.0", "@babel/traverse@^7.16.8", "@babel/traverse@^7.20.7", "@babel/traverse@^7.21.0", "@babel/traverse@^7.21.2", "@babel/traverse@^7.21.4": - version "7.21.4" - resolved "https://registry.npmjs.org/@babel/traverse/-/traverse-7.21.4.tgz" - dependencies: - "@babel/code-frame" "^7.21.4" - "@babel/generator" "^7.21.4" - "@babel/helper-environment-visitor" "^7.18.9" - "@babel/helper-function-name" "^7.21.0" - "@babel/helper-hoist-variables" "^7.18.6" - "@babel/helper-split-export-declaration" "^7.18.6" - "@babel/parser" "^7.21.4" - "@babel/types" "^7.21.4" +"@babel/template@^7.18.10", "@babel/template@^7.20.7", "@babel/template@^7.22.5": + version "7.22.5" + resolved "https://registry.yarnpkg.com/@babel/template/-/template-7.22.5.tgz#0c8c4d944509875849bd0344ff0050756eefc6ec" + integrity sha512-X7yV7eiwAxdj9k94NEylvbVHLiVG1nvzCV2EAowhxLTwODV1jl9UzZ48leOC0sH7OnuHrIkllaBgneUykIcZaw== + dependencies: + "@babel/code-frame" "^7.22.5" + "@babel/parser" "^7.22.5" + "@babel/types" "^7.22.5" + +"@babel/traverse@^7.14.0", "@babel/traverse@^7.16.8", "@babel/traverse@^7.22.10": + version "7.22.10" + resolved "https://registry.yarnpkg.com/@babel/traverse/-/traverse-7.22.10.tgz#20252acb240e746d27c2e82b4484f199cf8141aa" + integrity sha512-Q/urqV4pRByiNNpb/f5OSv28ZlGJiFiiTh+GAHktbIrkPhPbl90+uW6SmpoLyZqutrg9AEaEf3Q/ZBRHBXgxig== + dependencies: + "@babel/code-frame" "^7.22.10" + "@babel/generator" "^7.22.10" + "@babel/helper-environment-visitor" "^7.22.5" + "@babel/helper-function-name" "^7.22.5" + "@babel/helper-hoist-variables" "^7.22.5" + "@babel/helper-split-export-declaration" "^7.22.6" + "@babel/parser" "^7.22.10" + "@babel/types" "^7.22.10" debug "^4.1.0" globals "^11.1.0" -"@babel/types@^7.0.0", "@babel/types@^7.16.8", "@babel/types@^7.18.13", "@babel/types@^7.18.6", "@babel/types@^7.20.0", "@babel/types@^7.20.2", "@babel/types@^7.20.7", "@babel/types@^7.21.0", "@babel/types@^7.21.2", "@babel/types@^7.21.4": - version "7.21.4" - resolved "https://registry.npmjs.org/@babel/types/-/types-7.21.4.tgz" +"@babel/types@^7.0.0", "@babel/types@^7.16.8", "@babel/types@^7.18.13", "@babel/types@^7.22.10", "@babel/types@^7.22.5": + version "7.22.10" + resolved "https://registry.yarnpkg.com/@babel/types/-/types-7.22.10.tgz#4a9e76446048f2c66982d1a989dd12b8a2d2dc03" + integrity sha512-obaoigiLrlDZ7TUQln/8m4mSqIW2QFeOrCQc9r+xsaHGNoplVNYlRVpsfE8Vj35GEm2ZH4ZhrNYogs/3fj85kg== dependencies: - "@babel/helper-string-parser" "^7.19.4" - "@babel/helper-validator-identifier" "^7.19.1" + "@babel/helper-string-parser" "^7.22.5" + "@babel/helper-validator-identifier" "^7.22.5" to-fast-properties "^2.0.0" "@colors/colors@1.5.0": @@ -504,14 +572,16 @@ "@graphql-codegen/add@^4.0.1": version "4.0.1" - resolved "https://registry.npmjs.org/@graphql-codegen/add/-/add-4.0.1.tgz" + resolved "https://registry.yarnpkg.com/@graphql-codegen/add/-/add-4.0.1.tgz#c187af820fdd2fc7a9c1c2453bc389cd4e16699e" + integrity sha512-A7k+9eRfrKyyNfhWEN/0eKz09R5cp4XXxUuNLQAVm/aohmVI2xdMV4lM02rTlM6Pyou3cU/v0iZnhgo6IRpqeg== dependencies: "@graphql-codegen/plugin-helpers" "^4.1.0" tslib "~2.5.0" "@graphql-codegen/cli@3.2.2": version "3.2.2" - resolved "https://registry.npmjs.org/@graphql-codegen/cli/-/cli-3.2.2.tgz" + resolved "https://registry.yarnpkg.com/@graphql-codegen/cli/-/cli-3.2.2.tgz#1a94bc1ff9cfb7d618859336017d523689ab7d15" + integrity sha512-u+dm/SW1heLnUL4Tyf5Uv0AxOFhTCmUPHKwRLq2yE8MPhv7+Ti4vxxUP/XGoaMNRuHlN37wLI7tpFLV1Hhm22Q== dependencies: "@babel/generator" "^7.18.13" "@babel/template" "^7.18.10" @@ -551,7 +621,8 @@ "@graphql-codegen/client-preset@2.1.1": version "2.1.1" - resolved "https://registry.npmjs.org/@graphql-codegen/client-preset/-/client-preset-2.1.1.tgz" + resolved "https://registry.yarnpkg.com/@graphql-codegen/client-preset/-/client-preset-2.1.1.tgz#acf065d9520cde7e34ad0a9adada615e7502b884" + integrity sha512-yDFiO2CimwjkH/YE5nwRcLkXoAVz31NC+WWvFQdyrR1UKQqe2tqV/bKLUifnSbBGS238ajHJ6AEzVlzIvI8OYQ== dependencies: "@babel/helper-plugin-utils" "^7.20.2" "@babel/template" "^7.20.7" @@ -569,7 +640,8 @@ "@graphql-codegen/core@^3.1.0": version "3.1.0" - resolved "https://registry.npmjs.org/@graphql-codegen/core/-/core-3.1.0.tgz" + resolved "https://registry.yarnpkg.com/@graphql-codegen/core/-/core-3.1.0.tgz#ad859d52d509a4eb2ebe5aabba6543a628fb181b" + integrity sha512-DH1/yaR7oJE6/B+c6ZF2Tbdh7LixF1K8L+8BoSubjNyQ8pNwR4a70mvc1sv6H7qgp6y1bPQ9tKE+aazRRshysw== dependencies: "@graphql-codegen/plugin-helpers" "^4.1.0" "@graphql-tools/schema" "^9.0.0" @@ -578,7 +650,8 @@ "@graphql-codegen/gql-tag-operations@2.0.2": version "2.0.2" - resolved "https://registry.npmjs.org/@graphql-codegen/gql-tag-operations/-/gql-tag-operations-2.0.2.tgz" + resolved "https://registry.yarnpkg.com/@graphql-codegen/gql-tag-operations/-/gql-tag-operations-2.0.2.tgz#6fb693ede76d9ac67c1d7755aa0fc01c42b2be82" + integrity sha512-FB4/Q0xP/lIjwnlxdeGAfGFAiL7AhzIJB9keNrosd4Xe9r8V8NuZ0+0/hGc7KdzHhojYF/ycmJD7V2JLWaf23Q== dependencies: "@graphql-codegen/plugin-helpers" "^4.1.0" "@graphql-codegen/visitor-plugin-common" "3.0.2" @@ -588,7 +661,8 @@ "@graphql-codegen/plugin-helpers@^2.7.2": version "2.7.2" - resolved "https://registry.npmjs.org/@graphql-codegen/plugin-helpers/-/plugin-helpers-2.7.2.tgz" + resolved "https://registry.yarnpkg.com/@graphql-codegen/plugin-helpers/-/plugin-helpers-2.7.2.tgz#6544f739d725441c826a8af6a49519f588ff9bed" + integrity sha512-kln2AZ12uii6U59OQXdjLk5nOlh1pHis1R98cDZGFnfaiAbX9V3fxcZ1MMJkB7qFUymTALzyjZoXXdyVmPMfRg== dependencies: "@graphql-tools/utils" "^8.8.0" change-case-all "1.0.14" @@ -599,7 +673,8 @@ "@graphql-codegen/plugin-helpers@^4.1.0", "@graphql-codegen/plugin-helpers@^4.2.0": version "4.2.0" - resolved "https://registry.npmjs.org/@graphql-codegen/plugin-helpers/-/plugin-helpers-4.2.0.tgz" + resolved "https://registry.yarnpkg.com/@graphql-codegen/plugin-helpers/-/plugin-helpers-4.2.0.tgz#8324914d0f99162a223cfa01796cdd6be972d2ae" + integrity sha512-THFTCfg+46PXlXobYJ/OoCX6pzjI+9woQqCjdyKtgoI0tn3Xq2HUUCiidndxUpEYVrXb5pRiRXb7b/ZbMQqD0A== dependencies: "@graphql-tools/utils" "^9.0.0" change-case-all "1.0.15" @@ -610,7 +685,8 @@ "@graphql-codegen/schema-ast@^3.0.1": version "3.0.1" - resolved "https://registry.npmjs.org/@graphql-codegen/schema-ast/-/schema-ast-3.0.1.tgz" + resolved "https://registry.yarnpkg.com/@graphql-codegen/schema-ast/-/schema-ast-3.0.1.tgz#37b458bb57b95715a9eb4259341c856dae2a461d" + integrity sha512-rTKTi4XiW4QFZnrEqetpiYEWVsOFNoiR/v3rY9mFSttXFbIwNXPme32EspTiGWmEEdHY8UuTDtZN3vEcs/31zw== dependencies: "@graphql-codegen/plugin-helpers" "^4.1.0" "@graphql-tools/utils" "^9.0.0" @@ -618,7 +694,8 @@ "@graphql-codegen/typed-document-node@^3.0.2": version "3.0.2" - resolved "https://registry.npmjs.org/@graphql-codegen/typed-document-node/-/typed-document-node-3.0.2.tgz" + resolved "https://registry.yarnpkg.com/@graphql-codegen/typed-document-node/-/typed-document-node-3.0.2.tgz#afdf039c38f1b02a946854b6f487af764429eec8" + integrity sha512-RqX46y0GoMAcCfXjkUabOWpeSQ7tazpS5WyzWJNakpzXxNACx8NACaghU8zTEM+gjqtIp6YbFY/S92HQ34HbRQ== dependencies: "@graphql-codegen/plugin-helpers" "^4.1.0" "@graphql-codegen/visitor-plugin-common" "3.0.2" @@ -628,7 +705,8 @@ "@graphql-codegen/typescript-operations@^3.0.2": version "3.0.4" - resolved "https://registry.npmjs.org/@graphql-codegen/typescript-operations/-/typescript-operations-3.0.4.tgz" + resolved "https://registry.yarnpkg.com/@graphql-codegen/typescript-operations/-/typescript-operations-3.0.4.tgz#60163c07f0ef73655779ece450d02c1172c44027" + integrity sha512-6yE2OL2+WJ1vd5MwFEGXpaxsFGzjAGUytPVHDML3Bi3TwP1F3lnQlIko4untwvHW0JhZEGQ7Ck30H9HjcxpdKA== dependencies: "@graphql-codegen/plugin-helpers" "^4.2.0" "@graphql-codegen/typescript" "^3.0.4" @@ -638,7 +716,8 @@ "@graphql-codegen/typescript-react-apollo@^3.3.7": version "3.3.7" - resolved "https://registry.npmjs.org/@graphql-codegen/typescript-react-apollo/-/typescript-react-apollo-3.3.7.tgz" + resolved "https://registry.yarnpkg.com/@graphql-codegen/typescript-react-apollo/-/typescript-react-apollo-3.3.7.tgz#e856caa22c5f7bc9a546c44f54e5f3bd5801ab67" + integrity sha512-9DUiGE8rcwwEkf/S1kpBT/Py/UUs9Qak14bOnTT1JHWs1MWhiDA7vml+A8opU7YFI1EVbSSaE5jjRv11WHoikQ== dependencies: "@graphql-codegen/plugin-helpers" "^2.7.2" "@graphql-codegen/visitor-plugin-common" "2.13.1" @@ -648,7 +727,8 @@ "@graphql-codegen/typescript@^3.0.2", "@graphql-codegen/typescript@^3.0.4": version "3.0.4" - resolved "https://registry.npmjs.org/@graphql-codegen/typescript/-/typescript-3.0.4.tgz" + resolved "https://registry.yarnpkg.com/@graphql-codegen/typescript/-/typescript-3.0.4.tgz#e12dc106a2722ebc7d18556980ccf47fa9d0805f" + integrity sha512-x4O47447DZrWNtE/l5CU9QzzW4m1RbmCEdijlA3s2flG/y1Ckqdemob4CWfilSm5/tZ3w1junVDY616RDTSvZw== dependencies: "@graphql-codegen/plugin-helpers" "^4.2.0" "@graphql-codegen/schema-ast" "^3.0.1" @@ -658,7 +738,8 @@ "@graphql-codegen/visitor-plugin-common@2.13.1": version "2.13.1" - resolved "https://registry.npmjs.org/@graphql-codegen/visitor-plugin-common/-/visitor-plugin-common-2.13.1.tgz" + resolved "https://registry.yarnpkg.com/@graphql-codegen/visitor-plugin-common/-/visitor-plugin-common-2.13.1.tgz#2228660f6692bcdb96b1f6d91a0661624266b76b" + integrity sha512-mD9ufZhDGhyrSaWQGrU1Q1c5f01TeWtSWy/cDwXYjJcHIj1Y/DG2x0tOflEfCvh5WcnmHNIw4lzDsg1W7iFJEg== dependencies: "@graphql-codegen/plugin-helpers" "^2.7.2" "@graphql-tools/optimize" "^1.3.0" @@ -673,7 +754,8 @@ "@graphql-codegen/visitor-plugin-common@3.0.2": version "3.0.2" - resolved "https://registry.npmjs.org/@graphql-codegen/visitor-plugin-common/-/visitor-plugin-common-3.0.2.tgz" + resolved "https://registry.yarnpkg.com/@graphql-codegen/visitor-plugin-common/-/visitor-plugin-common-3.0.2.tgz#784c0faaa7e0773072ea5de464fdcae8d7765564" + integrity sha512-dKblRFrB0Fdl3+nPlzlLBka+TN/EGwr/q09mwry0H58z3j6gXkMbsdPr+dc8MhgOV7w/8egRvSPIvd7m6eFCnw== dependencies: "@graphql-codegen/plugin-helpers" "^4.1.0" "@graphql-tools/optimize" "^1.3.0" @@ -688,7 +770,8 @@ "@graphql-codegen/visitor-plugin-common@3.1.1", "@graphql-codegen/visitor-plugin-common@^3.0.2": version "3.1.1" - resolved "https://registry.npmjs.org/@graphql-codegen/visitor-plugin-common/-/visitor-plugin-common-3.1.1.tgz" + resolved "https://registry.yarnpkg.com/@graphql-codegen/visitor-plugin-common/-/visitor-plugin-common-3.1.1.tgz#50c2aa3c537a805ce68d2f115d0a9811b151428c" + integrity sha512-uAfp+zu/009R3HUAuTK2AamR1bxIltM6rrYYI6EXSmkM3rFtFsLTuJhjUDj98HcUCszJZrADppz8KKLGRUVlNg== dependencies: "@graphql-codegen/plugin-helpers" "^4.2.0" "@graphql-tools/optimize" "^1.3.0" @@ -703,39 +786,43 @@ "@graphql-tools/apollo-engine-loader@^7.3.6": version "7.3.26" - resolved "https://registry.npmjs.org/@graphql-tools/apollo-engine-loader/-/apollo-engine-loader-7.3.26.tgz" + resolved "https://registry.yarnpkg.com/@graphql-tools/apollo-engine-loader/-/apollo-engine-loader-7.3.26.tgz#91e54460d5579933e42a2010b8688c3459c245d8" + integrity sha512-h1vfhdJFjnCYn9b5EY1Z91JTF0KB3hHVJNQIsiUV2mpQXZdeOXQoaWeYEKaiI5R6kwBw5PP9B0fv3jfUIG8LyQ== dependencies: "@ardatan/sync-fetch" "^0.0.1" "@graphql-tools/utils" "^9.2.1" "@whatwg-node/fetch" "^0.8.0" tslib "^2.4.0" -"@graphql-tools/batch-execute@^8.5.19": - version "8.5.19" - resolved "https://registry.npmjs.org/@graphql-tools/batch-execute/-/batch-execute-8.5.19.tgz" +"@graphql-tools/batch-execute@^8.5.22": + version "8.5.22" + resolved "https://registry.yarnpkg.com/@graphql-tools/batch-execute/-/batch-execute-8.5.22.tgz#a742aa9d138fe794e786d8fb6429665dc7df5455" + integrity sha512-hcV1JaY6NJQFQEwCKrYhpfLK8frSXDbtNMoTur98u10Cmecy1zrqNKSqhEyGetpgHxaJRqszGzKeI3RuroDN6A== dependencies: "@graphql-tools/utils" "^9.2.1" - dataloader "2.2.2" + dataloader "^2.2.2" tslib "^2.4.0" - value-or-promise "1.0.12" + value-or-promise "^1.0.12" "@graphql-tools/code-file-loader@^7.3.17": - version "7.3.22" - resolved "https://registry.npmjs.org/@graphql-tools/code-file-loader/-/code-file-loader-7.3.22.tgz" + version "7.3.23" + resolved "https://registry.yarnpkg.com/@graphql-tools/code-file-loader/-/code-file-loader-7.3.23.tgz#33793f9a1f8e74981f8ae6ec4ab7061f9713db15" + integrity sha512-8Wt1rTtyTEs0p47uzsPJ1vAtfAx0jmxPifiNdmo9EOCuUPyQGEbMaik/YkqZ7QUFIEYEQu+Vgfo8tElwOPtx5Q== dependencies: - "@graphql-tools/graphql-tag-pluck" "7.5.1" + "@graphql-tools/graphql-tag-pluck" "7.5.2" "@graphql-tools/utils" "^9.2.1" globby "^11.0.3" tslib "^2.4.0" unixify "^1.0.0" "@graphql-tools/delegate@^9.0.31": - version "9.0.32" - resolved "https://registry.npmjs.org/@graphql-tools/delegate/-/delegate-9.0.32.tgz" + version "9.0.35" + resolved "https://registry.yarnpkg.com/@graphql-tools/delegate/-/delegate-9.0.35.tgz#94683f4bcec63520b4a6c8b2abf2e2e9324ea4f1" + integrity sha512-jwPu8NJbzRRMqi4Vp/5QX1vIUeUPpWmlQpOkXQD2r1X45YsVceyUUBnktCrlJlDB4jPRVy7JQGwmYo3KFiOBMA== dependencies: - "@graphql-tools/batch-execute" "^8.5.19" - "@graphql-tools/executor" "^0.0.18" - "@graphql-tools/schema" "^9.0.18" + "@graphql-tools/batch-execute" "^8.5.22" + "@graphql-tools/executor" "^0.0.20" + "@graphql-tools/schema" "^9.0.19" "@graphql-tools/utils" "^9.2.1" dataloader "^2.2.2" tslib "^2.5.0" @@ -743,14 +830,16 @@ "@graphql-tools/documents@^0.1.0": version "0.1.0" - resolved "https://registry.npmjs.org/@graphql-tools/documents/-/documents-0.1.0.tgz" + resolved "https://registry.yarnpkg.com/@graphql-tools/documents/-/documents-0.1.0.tgz#9c27faea5a17ab271dbd99edd8d52eee0e43573e" + integrity sha512-1WQeovHv5S1M3xMzQxbSrG3yl6QOnsq2JUBnlg5/0aMM5R4GNMx6Ms+ROByez/dnuA81kstRuSK+2qpe+GaRIw== dependencies: lodash.sortby "^4.7.0" tslib "^2.4.0" "@graphql-tools/executor-graphql-ws@^0.0.14": version "0.0.14" - resolved "https://registry.npmjs.org/@graphql-tools/executor-graphql-ws/-/executor-graphql-ws-0.0.14.tgz" + resolved "https://registry.yarnpkg.com/@graphql-tools/executor-graphql-ws/-/executor-graphql-ws-0.0.14.tgz#e0f53fc4cfc8a06cc461b2bc1edb4bb9a8e837ed" + integrity sha512-P2nlkAsPZKLIXImFhj0YTtny5NQVGSsKnhi7PzXiaHSXc6KkzqbWZHKvikD4PObanqg+7IO58rKFpGXP7eeO+w== dependencies: "@graphql-tools/utils" "^9.2.1" "@repeaterjs/repeater" "3.0.4" @@ -761,8 +850,9 @@ ws "8.13.0" "@graphql-tools/executor-http@^0.1.7", "@graphql-tools/executor-http@^0.1.9": - version "0.1.9" - resolved "https://registry.npmjs.org/@graphql-tools/executor-http/-/executor-http-0.1.9.tgz" + version "0.1.10" + resolved "https://registry.yarnpkg.com/@graphql-tools/executor-http/-/executor-http-0.1.10.tgz#faf48e18e62a925796c9653c2f50cf2095bc8e6f" + integrity sha512-hnAfbKv0/lb9s31LhWzawQ5hghBfHS+gYWtqxME6Rl0Aufq9GltiiLBcl7OVVOnkLF0KhwgbYP1mB5VKmgTGpg== dependencies: "@graphql-tools/utils" "^9.2.1" "@repeaterjs/repeater" "^3.0.4" @@ -775,7 +865,8 @@ "@graphql-tools/executor-legacy-ws@^0.0.11": version "0.0.11" - resolved "https://registry.npmjs.org/@graphql-tools/executor-legacy-ws/-/executor-legacy-ws-0.0.11.tgz" + resolved "https://registry.yarnpkg.com/@graphql-tools/executor-legacy-ws/-/executor-legacy-ws-0.0.11.tgz#a1e12be8279e92a363a23d4105461a34cd9e389e" + integrity sha512-4ai+NnxlNfvIQ4c70hWFvOZlSUN8lt7yc+ZsrwtNFbFPH/EroIzFMapAxM9zwyv9bH38AdO3TQxZ5zNxgBdvUw== dependencies: "@graphql-tools/utils" "^9.2.1" "@types/ws" "^8.0.0" @@ -783,21 +874,23 @@ tslib "^2.4.0" ws "8.13.0" -"@graphql-tools/executor@^0.0.18": - version "0.0.18" - resolved "https://registry.npmjs.org/@graphql-tools/executor/-/executor-0.0.18.tgz" +"@graphql-tools/executor@^0.0.20": + version "0.0.20" + resolved "https://registry.yarnpkg.com/@graphql-tools/executor/-/executor-0.0.20.tgz#d51d159696e839522dd49d936636af251670e425" + integrity sha512-GdvNc4vszmfeGvUqlcaH1FjBoguvMYzxAfT6tDd4/LgwymepHhinqLNA5otqwVLW+JETcDaK7xGENzFomuE6TA== dependencies: "@graphql-tools/utils" "^9.2.1" "@graphql-typed-document-node/core" "3.2.0" - "@repeaterjs/repeater" "3.0.4" + "@repeaterjs/repeater" "^3.0.4" tslib "^2.4.0" - value-or-promise "1.0.12" + value-or-promise "^1.0.12" "@graphql-tools/git-loader@^7.2.13": - version "7.2.21" - resolved "https://registry.npmjs.org/@graphql-tools/git-loader/-/git-loader-7.2.21.tgz" + version "7.3.0" + resolved "https://registry.yarnpkg.com/@graphql-tools/git-loader/-/git-loader-7.3.0.tgz#ca10c17d4f58c4592432d9d2ac1c2b393aebad16" + integrity sha512-gcGAK+u16eHkwsMYqqghZbmDquh8QaO24Scsxq+cVR+vx1ekRlsEiXvu+yXVDbZdcJ6PBIbeLcQbEu+xhDLmvQ== dependencies: - "@graphql-tools/graphql-tag-pluck" "7.5.1" + "@graphql-tools/graphql-tag-pluck" "7.5.2" "@graphql-tools/utils" "^9.2.1" is-glob "4.0.3" micromatch "^4.0.4" @@ -806,7 +899,8 @@ "@graphql-tools/github-loader@^7.3.20": version "7.3.28" - resolved "https://registry.npmjs.org/@graphql-tools/github-loader/-/github-loader-7.3.28.tgz" + resolved "https://registry.yarnpkg.com/@graphql-tools/github-loader/-/github-loader-7.3.28.tgz#a7166b136e8442bd8b3ab943ad3b66c84bcabfcf" + integrity sha512-OK92Lf9pmxPQvjUNv05b3tnVhw0JRfPqOf15jZjyQ8BfdEUrJoP32b4dRQQem/wyRL24KY4wOfArJNqzpsbwCA== dependencies: "@ardatan/sync-fetch" "^0.0.1" "@graphql-tools/executor-http" "^0.1.9" @@ -818,7 +912,8 @@ "@graphql-tools/graphql-file-loader@^7.3.7", "@graphql-tools/graphql-file-loader@^7.5.0": version "7.5.17" - resolved "https://registry.npmjs.org/@graphql-tools/graphql-file-loader/-/graphql-file-loader-7.5.17.tgz" + resolved "https://registry.yarnpkg.com/@graphql-tools/graphql-file-loader/-/graphql-file-loader-7.5.17.tgz#7c281617ea3ab4db4d42a2bdb49850f2b937f0f9" + integrity sha512-hVwwxPf41zOYgm4gdaZILCYnKB9Zap7Ys9OhY1hbwuAuC4MMNY9GpUjoTU3CQc3zUiPoYStyRtUGkHSJZ3HxBw== dependencies: "@graphql-tools/import" "6.7.18" "@graphql-tools/utils" "^9.2.1" @@ -826,12 +921,13 @@ tslib "^2.4.0" unixify "^1.0.0" -"@graphql-tools/graphql-tag-pluck@7.5.1", "@graphql-tools/graphql-tag-pluck@^7.4.6": - version "7.5.1" - resolved "https://registry.npmjs.org/@graphql-tools/graphql-tag-pluck/-/graphql-tag-pluck-7.5.1.tgz" +"@graphql-tools/graphql-tag-pluck@7.5.2", "@graphql-tools/graphql-tag-pluck@^7.4.6": + version "7.5.2" + resolved "https://registry.yarnpkg.com/@graphql-tools/graphql-tag-pluck/-/graphql-tag-pluck-7.5.2.tgz#502f1e066e19d832ebdeba5f571d7636dc27572d" + integrity sha512-RW+H8FqOOLQw0BPXaahYepVSRjuOHw+7IL8Opaa5G5uYGOBxoXR7DceyQ7BcpMgktAOOmpDNQ2WtcboChOJSRA== dependencies: "@babel/parser" "^7.16.8" - "@babel/plugin-syntax-import-assertions" "7.20.0" + "@babel/plugin-syntax-import-assertions" "^7.20.0" "@babel/traverse" "^7.16.8" "@babel/types" "^7.16.8" "@graphql-tools/utils" "^9.2.1" @@ -839,7 +935,8 @@ "@graphql-tools/import@6.7.18": version "6.7.18" - resolved "https://registry.npmjs.org/@graphql-tools/import/-/import-6.7.18.tgz" + resolved "https://registry.yarnpkg.com/@graphql-tools/import/-/import-6.7.18.tgz#ad092d8a4546bb6ffc3e871e499eec7ac368680b" + integrity sha512-XQDdyZTp+FYmT7as3xRWH/x8dx0QZA2WZqfMF5EWb36a0PiH7WwlRQYIdyYXj8YCLpiWkeBXgBRHmMnwEYR8iQ== dependencies: "@graphql-tools/utils" "^9.2.1" resolve-from "5.0.0" @@ -847,7 +944,8 @@ "@graphql-tools/json-file-loader@^7.3.7", "@graphql-tools/json-file-loader@^7.4.1": version "7.4.18" - resolved "https://registry.npmjs.org/@graphql-tools/json-file-loader/-/json-file-loader-7.4.18.tgz" + resolved "https://registry.yarnpkg.com/@graphql-tools/json-file-loader/-/json-file-loader-7.4.18.tgz#d78ae40979bde51cfc59717757354afc9e35fba2" + integrity sha512-AJ1b6Y1wiVgkwsxT5dELXhIVUPs/u3VZ8/0/oOtpcoyO/vAeM5rOvvWegzicOOnQw8G45fgBRMkkRfeuwVt6+w== dependencies: "@graphql-tools/utils" "^9.2.1" globby "^11.0.3" @@ -856,7 +954,8 @@ "@graphql-tools/load@^7.5.5", "@graphql-tools/load@^7.8.0": version "7.8.14" - resolved "https://registry.npmjs.org/@graphql-tools/load/-/load-7.8.14.tgz" + resolved "https://registry.yarnpkg.com/@graphql-tools/load/-/load-7.8.14.tgz#f2356f9a5f658a42e33934ae036e4b2cadf2d1e9" + integrity sha512-ASQvP+snHMYm+FhIaLxxFgVdRaM0vrN9wW2BKInQpktwWTXVyk+yP5nQUCEGmn0RTdlPKrffBaigxepkEAJPrg== dependencies: "@graphql-tools/schema" "^9.0.18" "@graphql-tools/utils" "^9.2.1" @@ -864,21 +963,24 @@ tslib "^2.4.0" "@graphql-tools/merge@^8.2.6", "@graphql-tools/merge@^8.4.1": - version "8.4.1" - resolved "https://registry.npmjs.org/@graphql-tools/merge/-/merge-8.4.1.tgz" + version "8.4.2" + resolved "https://registry.yarnpkg.com/@graphql-tools/merge/-/merge-8.4.2.tgz#95778bbe26b635e8d2f60ce9856b388f11fe8288" + integrity sha512-XbrHAaj8yDuINph+sAfuq3QCZ/tKblrTLOpirK0+CAgNlZUCHs0Fa+xtMUURgwCVThLle1AF7svJCxFizygLsw== dependencies: "@graphql-tools/utils" "^9.2.1" tslib "^2.4.0" "@graphql-tools/optimize@^1.3.0": version "1.4.0" - resolved "https://registry.npmjs.org/@graphql-tools/optimize/-/optimize-1.4.0.tgz" + resolved "https://registry.yarnpkg.com/@graphql-tools/optimize/-/optimize-1.4.0.tgz#20d6a9efa185ef8fc4af4fd409963e0907c6e112" + integrity sha512-dJs/2XvZp+wgHH8T5J2TqptT9/6uVzIYvA6uFACha+ufvdMBedkfR4b4GbT8jAKLRARiqRTxy3dctnwkTM2tdw== dependencies: tslib "^2.4.0" "@graphql-tools/prisma-loader@^7.2.49": - version "7.2.71" - resolved "https://registry.npmjs.org/@graphql-tools/prisma-loader/-/prisma-loader-7.2.71.tgz" + version "7.2.72" + resolved "https://registry.yarnpkg.com/@graphql-tools/prisma-loader/-/prisma-loader-7.2.72.tgz#6304fc23600458396f3ede713d8e2371df7850e3" + integrity sha512-0a7uV7Fky6yDqd0tI9+XMuvgIo6GAqiVzzzFV4OSLry4AwiQlI3igYseBV7ZVOGhedOTqj/URxjpiv07hRcwag== dependencies: "@graphql-tools/url-loader" "^7.17.18" "@graphql-tools/utils" "^9.2.1" @@ -889,8 +991,8 @@ debug "^4.3.1" dotenv "^16.0.0" graphql-request "^6.0.0" - http-proxy-agent "^5.0.0" - https-proxy-agent "^5.0.0" + http-proxy-agent "^6.0.0" + https-proxy-agent "^6.0.0" jose "^4.11.4" js-yaml "^4.0.0" json-stable-stringify "^1.0.1" @@ -901,24 +1003,27 @@ "@graphql-tools/relay-operation-optimizer@^6.5.0": version "6.5.18" - resolved "https://registry.npmjs.org/@graphql-tools/relay-operation-optimizer/-/relay-operation-optimizer-6.5.18.tgz" + resolved "https://registry.yarnpkg.com/@graphql-tools/relay-operation-optimizer/-/relay-operation-optimizer-6.5.18.tgz#a1b74a8e0a5d0c795b8a4d19629b654cf66aa5ab" + integrity sha512-mc5VPyTeV+LwiM+DNvoDQfPqwQYhPV/cl5jOBjTgSniyaq8/86aODfMkrE2OduhQ5E00hqrkuL2Fdrgk0w1QJg== dependencies: "@ardatan/relay-compiler" "12.0.0" "@graphql-tools/utils" "^9.2.1" tslib "^2.4.0" -"@graphql-tools/schema@^9.0.0", "@graphql-tools/schema@^9.0.18": - version "9.0.18" - resolved "https://registry.npmjs.org/@graphql-tools/schema/-/schema-9.0.18.tgz" +"@graphql-tools/schema@^9.0.0", "@graphql-tools/schema@^9.0.18", "@graphql-tools/schema@^9.0.19": + version "9.0.19" + resolved "https://registry.yarnpkg.com/@graphql-tools/schema/-/schema-9.0.19.tgz#c4ad373b5e1b8a0cf365163435b7d236ebdd06e7" + integrity sha512-oBRPoNBtCkk0zbUsyP4GaIzCt8C0aCI4ycIRUL67KK5pOHljKLBBtGT+Jr6hkzA74C8Gco8bpZPe7aWFjiaK2w== dependencies: "@graphql-tools/merge" "^8.4.1" "@graphql-tools/utils" "^9.2.1" tslib "^2.4.0" - value-or-promise "1.0.12" + value-or-promise "^1.0.12" "@graphql-tools/url-loader@^7.13.2", "@graphql-tools/url-loader@^7.17.18", "@graphql-tools/url-loader@^7.9.7": version "7.17.18" - resolved "https://registry.npmjs.org/@graphql-tools/url-loader/-/url-loader-7.17.18.tgz" + resolved "https://registry.yarnpkg.com/@graphql-tools/url-loader/-/url-loader-7.17.18.tgz#3e253594d23483e4c0dd3a4c3dd2ad5cd0141192" + integrity sha512-ear0CiyTj04jCVAxi7TvgbnGDIN2HgqzXzwsfcqiVg9cvjT40NcMlZ2P1lZDgqMkZ9oyLTV8Bw6j+SyG6A+xPw== dependencies: "@ardatan/sync-fetch" "^0.0.1" "@graphql-tools/delegate" "^9.0.31" @@ -936,20 +1041,23 @@ "@graphql-tools/utils@^8.8.0": version "8.13.1" - resolved "https://registry.npmjs.org/@graphql-tools/utils/-/utils-8.13.1.tgz" + resolved "https://registry.yarnpkg.com/@graphql-tools/utils/-/utils-8.13.1.tgz#b247607e400365c2cd87ff54654d4ad25a7ac491" + integrity sha512-qIh9yYpdUFmctVqovwMdheVNJqFh+DQNWIhX87FJStfXYnmweBUDATok9fWPleKeFwxnW8IapKmY8m8toJEkAw== dependencies: tslib "^2.4.0" "@graphql-tools/utils@^9.0.0", "@graphql-tools/utils@^9.1.1", "@graphql-tools/utils@^9.2.1": version "9.2.1" - resolved "https://registry.npmjs.org/@graphql-tools/utils/-/utils-9.2.1.tgz" + resolved "https://registry.yarnpkg.com/@graphql-tools/utils/-/utils-9.2.1.tgz#1b3df0ef166cfa3eae706e3518b17d5922721c57" + integrity sha512-WUw506Ql6xzmOORlriNrD6Ugx+HjVgYxt9KCXD9mHAak+eaXSwuGGPyE60hy9xaDEoXKBsG7SkG69ybitaVl6A== dependencies: "@graphql-typed-document-node/core" "^3.1.1" tslib "^2.4.0" "@graphql-tools/wrap@^9.4.2": version "9.4.2" - resolved "https://registry.npmjs.org/@graphql-tools/wrap/-/wrap-9.4.2.tgz" + resolved "https://registry.yarnpkg.com/@graphql-tools/wrap/-/wrap-9.4.2.tgz#30835587c4c73be1780908a7cb077d8013aa2703" + integrity sha512-DFcd9r51lmcEKn0JW43CWkkI2D6T9XI1juW/Yo86i04v43O9w2/k4/nx2XTJv4Yv+iXwUw7Ok81PGltwGJSDSA== dependencies: "@graphql-tools/delegate" "^9.0.31" "@graphql-tools/schema" "^9.0.18" @@ -959,11 +1067,13 @@ "@graphql-typed-document-node/core@3.1.2": version "3.1.2" - resolved "https://registry.npmjs.org/@graphql-typed-document-node/core/-/core-3.1.2.tgz" + resolved "https://registry.yarnpkg.com/@graphql-typed-document-node/core/-/core-3.1.2.tgz#6fc464307cbe3c8ca5064549b806360d84457b04" + integrity sha512-9anpBMM9mEgZN4wr2v8wHJI2/u5TnnggewRN6OlvXTTnuVyoY19X6rOv9XTqKRw6dcGKwZsBi8n0kDE2I5i4VA== "@graphql-typed-document-node/core@3.2.0", "@graphql-typed-document-node/core@^3.1.1", "@graphql-typed-document-node/core@^3.2.0": version "3.2.0" - resolved "https://registry.npmjs.org/@graphql-typed-document-node/core/-/core-3.2.0.tgz" + resolved "https://registry.yarnpkg.com/@graphql-typed-document-node/core/-/core-3.2.0.tgz#5f3d96ec6b2354ad6d8a28bf216a1d97b5426861" + integrity sha512-mB9oAsNCm9aM3/SOv4YtBMqZbYj10R7dkq8byBqxGY/ncFwhf2oQzMV+LCRlWoDSEBJ3COiR1yeDvMtsoOsuFQ== "@headlessui/react@^1.7.13": version "1.7.14" @@ -997,34 +1107,35 @@ "@jridgewell/gen-mapping@^0.3.0", "@jridgewell/gen-mapping@^0.3.2": version "0.3.3" - resolved "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.3.tgz" + resolved "https://registry.yarnpkg.com/@jridgewell/gen-mapping/-/gen-mapping-0.3.3.tgz#7e02e6eb5df901aaedb08514203b096614024098" + integrity sha512-HLhSWOLRi875zjjMG/r+Nv0oCW8umGb0BgEhyX3dDX3egwZtB8PqLnjz3yedt8R5StBrzcg4aBpnh8UA9D1BoQ== dependencies: "@jridgewell/set-array" "^1.0.1" "@jridgewell/sourcemap-codec" "^1.4.10" "@jridgewell/trace-mapping" "^0.3.9" -"@jridgewell/resolve-uri@3.1.0": - version "3.1.0" - resolved "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.0.tgz" +"@jridgewell/resolve-uri@^3.1.0": + version "3.1.1" + resolved "https://registry.yarnpkg.com/@jridgewell/resolve-uri/-/resolve-uri-3.1.1.tgz#c08679063f279615a3326583ba3a90d1d82cc721" + integrity sha512-dSYZh7HhCDtCKm4QakX0xFpsRDqjjtZf/kjI/v3T3Nwt5r8/qz/M19F9ySyOqU94SXBmeG9ttTul+YnR4LOxFA== "@jridgewell/set-array@^1.0.1": version "1.1.2" - resolved "https://registry.npmjs.org/@jridgewell/set-array/-/set-array-1.1.2.tgz" + resolved "https://registry.yarnpkg.com/@jridgewell/set-array/-/set-array-1.1.2.tgz#7c6cf998d6d20b914c0a55a91ae928ff25965e72" + integrity sha512-xnkseuNADM0gt2bs+BvhO0p78Mk762YnZdsuzFV018NoG1Sj1SCQvpSqa7XUaTam5vAGasABV9qXASMKnFMwMw== -"@jridgewell/sourcemap-codec@1.4.14": - version "1.4.14" - resolved "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.4.14.tgz" - -"@jridgewell/sourcemap-codec@^1.4.10": +"@jridgewell/sourcemap-codec@^1.4.10", "@jridgewell/sourcemap-codec@^1.4.14": version "1.4.15" - resolved "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.4.15.tgz" + resolved "https://registry.yarnpkg.com/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.4.15.tgz#d7c6e6755c78567a951e04ab52ef0fd26de59f32" + integrity sha512-eF2rxCRulEKXHTRiDrDy6erMYWqNw4LPdQ8UQA4huuxaQsVeRPFl2oM8oDGxMFhJUWZf9McpLtJasDDZb/Bpeg== "@jridgewell/trace-mapping@^0.3.17", "@jridgewell/trace-mapping@^0.3.9": - version "0.3.18" - resolved "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.18.tgz" + version "0.3.19" + resolved "https://registry.yarnpkg.com/@jridgewell/trace-mapping/-/trace-mapping-0.3.19.tgz#f8a3249862f91be48d3127c3cfe992f79b4b8811" + integrity sha512-kf37QtfW+Hwx/buWGMPcR60iF9ziHa6r/CZJIHbmcm4+0qrXiVdxegAH0F6yddEVQ7zdkjcGCgCzUu+BcbhQxw== dependencies: - "@jridgewell/resolve-uri" "3.1.0" - "@jridgewell/sourcemap-codec" "1.4.14" + "@jridgewell/resolve-uri" "^3.1.0" + "@jridgewell/sourcemap-codec" "^1.4.14" "@motionone/animation@^10.15.1": version "10.15.1" @@ -1145,14 +1256,16 @@ "@nodelib/fs.scandir@2.1.5": version "2.1.5" - resolved "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz" + resolved "https://registry.yarnpkg.com/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz#7619c2eb21b25483f6d167548b4cfd5a7488c3d5" + integrity sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g== dependencies: "@nodelib/fs.stat" "2.0.5" run-parallel "^1.1.9" "@nodelib/fs.stat@2.0.5", "@nodelib/fs.stat@^2.0.2": version "2.0.5" - resolved "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz" + resolved "https://registry.yarnpkg.com/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz#5bd262af94e9d25bd1e71b05deed44876a222e8b" + integrity sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A== "@nodelib/fs.walk@^1.2.3", "@nodelib/fs.walk@^1.2.8": version "1.2.8" @@ -1315,18 +1428,81 @@ version "1.0.4" resolved "https://registry.npmjs.org/@panva/hkdf/-/hkdf-1.0.4.tgz" +"@parcel/watcher-android-arm64@2.2.0": + version "2.2.0" + resolved "https://registry.yarnpkg.com/@parcel/watcher-android-arm64/-/watcher-android-arm64-2.2.0.tgz#3d1a71f251ba829ab884dfe119cc4f4c49c7222b" + integrity sha512-nU2wh00CTQT9rr1TIKTjdQ9lAGYpmz6XuKw0nAwAN+S2A5YiD55BK1u+E5WMCT8YOIDe/n6gaj4o/Bi9294SSQ== + +"@parcel/watcher-darwin-arm64@2.2.0": + version "2.2.0" + resolved "https://registry.yarnpkg.com/@parcel/watcher-darwin-arm64/-/watcher-darwin-arm64-2.2.0.tgz#9ed47d9e4146d53a39f62577a993426d6ddec2d2" + integrity sha512-cJl0UZDcodciy3TDMomoK/Huxpjlkkim3SyMgWzjovHGOZKNce9guLz2dzuFwfObBFCjfznbFMIvAZ5syXotYw== + +"@parcel/watcher-darwin-x64@2.2.0": + version "2.2.0" + resolved "https://registry.yarnpkg.com/@parcel/watcher-darwin-x64/-/watcher-darwin-x64-2.2.0.tgz#9b553e61f4a01a0947b1c005619db7b7c9a286d1" + integrity sha512-QI77zxaGrCV1StKcoRYfsUfmUmvPMPfQrubkBBy5XujV2fwaLgZivQOTQMBgp5K2+E19u1ufpspKXAPqSzpbyg== + +"@parcel/watcher-linux-arm-glibc@2.2.0": + version "2.2.0" + resolved "https://registry.yarnpkg.com/@parcel/watcher-linux-arm-glibc/-/watcher-linux-arm-glibc-2.2.0.tgz#2644f6292b862c52d636978da8711bf2f7b14b24" + integrity sha512-I2GPBcAXazPzabCmfsa3HRRW+MGlqxYd8g8RIueJU+a4o5nyNZDz0CR1cu0INT0QSQXEZV7w6UE8Hz9CF8u3Pg== + +"@parcel/watcher-linux-arm64-glibc@2.2.0": + version "2.2.0" + resolved "https://registry.yarnpkg.com/@parcel/watcher-linux-arm64-glibc/-/watcher-linux-arm64-glibc-2.2.0.tgz#b82c1715a20e6725c89b2697a208d2ae488d22da" + integrity sha512-St5mlfp+2lS9AmgixUqfwJa/DwVmTCJxC1HcOubUTz6YFOKIlkHCeUa1Bxi4E/tR/HSez8+heXHL8HQkJ4Bd8g== + +"@parcel/watcher-linux-arm64-musl@2.2.0": + version "2.2.0" + resolved "https://registry.yarnpkg.com/@parcel/watcher-linux-arm64-musl/-/watcher-linux-arm64-musl-2.2.0.tgz#45511adc69c4c89b10965f0dd1519e56a00f512f" + integrity sha512-jS+qfhhoOBVWwMLP65MaG8xdInMK30pPW8wqTCg2AAuVJh5xepMbzkhHJ4zURqHiyY3EiIRuYu4ONJKCxt8iqA== + +"@parcel/watcher-linux-x64-glibc@2.2.0": + version "2.2.0" + resolved "https://registry.yarnpkg.com/@parcel/watcher-linux-x64-glibc/-/watcher-linux-x64-glibc-2.2.0.tgz#f6e878bf40874b1ce9530ec87e5c2e644e9ad5ac" + integrity sha512-xJvJ7R2wJdi47WZBFS691RDOWvP1j/IAs3EXaWVhDI8FFITbWrWaln7KoNcR0Y3T+ZwimFY/cfb0PNht1q895g== + +"@parcel/watcher-linux-x64-musl@2.2.0": + version "2.2.0" + resolved "https://registry.yarnpkg.com/@parcel/watcher-linux-x64-musl/-/watcher-linux-x64-musl-2.2.0.tgz#4a1b7a8f65b42f50b1819185ef56ed3c9e251425" + integrity sha512-D+NMpgr23a+RI5mu8ZPKWy7AqjBOkURFDgP5iIXXEf/K3hm0jJ3ogzi0Ed2237B/CdYREimCgXyeiAlE/FtwyA== + +"@parcel/watcher-win32-arm64@2.2.0": + version "2.2.0" + resolved "https://registry.yarnpkg.com/@parcel/watcher-win32-arm64/-/watcher-win32-arm64-2.2.0.tgz#4cdbaf8c1097038e838d12f897ac8595b18dfb1f" + integrity sha512-z225cPn3aygJsyVUOWwfyW+fY0Tvk7N3XCOl66qUPFxpbuXeZuiuuJemmtm8vxyqa3Ur7peU/qJxrpC64aeI7Q== + +"@parcel/watcher-win32-x64@2.2.0": + version "2.2.0" + resolved "https://registry.yarnpkg.com/@parcel/watcher-win32-x64/-/watcher-win32-x64-2.2.0.tgz#f2fcc255b7cadfd0afa7c51cde36413632075bcc" + integrity sha512-JqGW0RJ61BkKx+yYzIURt9s53P7xMVbv0uxYPzAXLBINGaFmkIKSuUPyBVfy8TMbvp93lvF4SPBNDzVRJfvgOw== + "@parcel/watcher@^2.1.0": - version "2.1.0" - resolved "https://registry.npmjs.org/@parcel/watcher/-/watcher-2.1.0.tgz" + version "2.2.0" + resolved "https://registry.yarnpkg.com/@parcel/watcher/-/watcher-2.2.0.tgz#92067954e591d239c3ecfa08add205f88f476068" + integrity sha512-71S4TF+IMyAn24PK4KSkdKtqJDR3zRzb0HE3yXpacItqTM7XfF2f5q9NEGLEVl0dAaBAGfNwDCjH120y25F6Tg== dependencies: + detect-libc "^1.0.3" is-glob "^4.0.3" micromatch "^4.0.5" - node-addon-api "^3.2.1" - node-gyp-build "^4.3.0" + node-addon-api "^7.0.0" + optionalDependencies: + "@parcel/watcher-android-arm64" "2.2.0" + "@parcel/watcher-darwin-arm64" "2.2.0" + "@parcel/watcher-darwin-x64" "2.2.0" + "@parcel/watcher-linux-arm-glibc" "2.2.0" + "@parcel/watcher-linux-arm64-glibc" "2.2.0" + "@parcel/watcher-linux-arm64-musl" "2.2.0" + "@parcel/watcher-linux-x64-glibc" "2.2.0" + "@parcel/watcher-linux-x64-musl" "2.2.0" + "@parcel/watcher-win32-arm64" "2.2.0" + "@parcel/watcher-win32-x64" "2.2.0" "@peculiar/asn1-schema@^2.3.6": version "2.3.6" - resolved "https://registry.npmjs.org/@peculiar/asn1-schema/-/asn1-schema-2.3.6.tgz" + resolved "https://registry.yarnpkg.com/@peculiar/asn1-schema/-/asn1-schema-2.3.6.tgz#3dd3c2ade7f702a9a94dfb395c192f5fa5d6b922" + integrity sha512-izNRxPoaeJeg/AyH8hER6s+H7p4itk+03QCa4sbxI3lNdseQYCuxzgsuNK8bTXChtLTjpJz6NmXKA73qLa3rCA== dependencies: asn1js "^3.0.5" pvtsutils "^1.3.2" @@ -1334,13 +1510,15 @@ "@peculiar/json-schema@^1.1.12": version "1.1.12" - resolved "https://registry.npmjs.org/@peculiar/json-schema/-/json-schema-1.1.12.tgz" + resolved "https://registry.yarnpkg.com/@peculiar/json-schema/-/json-schema-1.1.12.tgz#fe61e85259e3b5ba5ad566cb62ca75b3d3cd5339" + integrity sha512-coUfuoMeIB7B8/NMekxaDzLhaYmp0HZNPEjYRm9goRou8UZIC3z21s0sL9AWoCw4EG876QyO3kYrc61WNF9B/w== dependencies: tslib "^2.0.0" "@peculiar/webcrypto@^1.4.0": version "1.4.3" - resolved "https://registry.npmjs.org/@peculiar/webcrypto/-/webcrypto-1.4.3.tgz" + resolved "https://registry.yarnpkg.com/@peculiar/webcrypto/-/webcrypto-1.4.3.tgz#078b3e8f598e847b78683dc3ba65feb5029b93a7" + integrity sha512-VtaY4spKTdN5LjJ04im/d/joXuvLbQdgy5Z4DXF4MFZhQ+MTrejbNMkfZBp1Bs3O5+bFqnJgyGdPuZQflvIa5A== dependencies: "@peculiar/asn1-schema" "^2.3.6" "@peculiar/json-schema" "^1.1.12" @@ -1365,7 +1543,8 @@ "@repeaterjs/repeater@3.0.4", "@repeaterjs/repeater@^3.0.4": version "3.0.4" - resolved "https://registry.npmjs.org/@repeaterjs/repeater/-/repeater-3.0.4.tgz" + resolved "https://registry.yarnpkg.com/@repeaterjs/repeater/-/repeater-3.0.4.tgz#a04d63f4d1bf5540a41b01a921c9a7fddc3bd1ca" + integrity sha512-AW8PKd6iX3vAZ0vA43nOUOnbq/X5ihgU+mSXXqunMkeQADGiqw/PY0JNeYtD5sr0PAy51YPgAPbDoeapv9r8WA== "@rushstack/eslint-patch@^1.1.3": version "1.2.0" @@ -1405,7 +1584,8 @@ "@tootallnate/once@2": version "2.0.0" - resolved "https://registry.npmjs.org/@tootallnate/once/-/once-2.0.0.tgz" + resolved "https://registry.yarnpkg.com/@tootallnate/once/-/once-2.0.0.tgz#f544a148d3ab35801c1f633a7441fd87c2e484bf" + integrity sha512-XCuKFP5PS55gnMVu3dty8KPatLqUoy/ZYzDzAGCQ8JNFCkLXzmI7vNHCR+XpbZaMWQK/vQubr7PkYq8g470J/A== "@tufjs/canonical-json@1.0.0": version "1.0.0" @@ -1424,7 +1604,8 @@ "@types/js-yaml@^4.0.0": version "4.0.5" - resolved "https://registry.npmjs.org/@types/js-yaml/-/js-yaml-4.0.5.tgz" + resolved "https://registry.yarnpkg.com/@types/js-yaml/-/js-yaml-4.0.5.tgz#738dd390a6ecc5442f35e7f03fa1431353f7e138" + integrity sha512-FhpRzf927MNQdRZP0J5DLIdTXhjLYzeUTmLAu69mnVksLH9CJY3IuSeEgbKUki7GQZm0WqDkGzyxju2EZGD2wA== "@types/json-schema@^7.0.9": version "7.0.11" @@ -1432,7 +1613,8 @@ "@types/json-stable-stringify@^1.0.32": version "1.0.34" - resolved "https://registry.npmjs.org/@types/json-stable-stringify/-/json-stable-stringify-1.0.34.tgz" + resolved "https://registry.yarnpkg.com/@types/json-stable-stringify/-/json-stable-stringify-1.0.34.tgz#c0fb25e4d957e0ee2e497c1f553d7f8bb668fd75" + integrity sha512-s2cfwagOQAS8o06TcwKfr9Wx11dNGbH2E9vJz1cqV+a/LOyhWNLUNd6JSRYNzvB4d29UuJX2M0Dj9vE1T8fRXw== "@types/json5@^0.0.29": version "0.0.29" @@ -1454,18 +1636,29 @@ version "0.7.10" resolved "https://registry.npmjs.org/@types/libsodium-wrappers/-/libsodium-wrappers-0.7.10.tgz" -"@types/node@*", "@types/node@18.14.0": +"@types/node@*": + version "20.5.1" + resolved "https://registry.yarnpkg.com/@types/node/-/node-20.5.1.tgz#178d58ee7e4834152b0e8b4d30cbfab578b9bb30" + integrity sha512-4tT2UrL5LBqDwoed9wZ6N3umC4Yhz3W3FloMmiiG4JwmUJWpie0c7lcnUNd4gtMKuDEO4wRVS8B6Xa0uMRsMKg== + +"@types/node@18.14.0": version "18.14.0" resolved "https://registry.npmjs.org/@types/node/-/node-18.14.0.tgz" "@types/parse-json@^4.0.0": version "4.0.0" - resolved "https://registry.npmjs.org/@types/parse-json/-/parse-json-4.0.0.tgz" + resolved "https://registry.yarnpkg.com/@types/parse-json/-/parse-json-4.0.0.tgz#2f8bb441434d163b35fb8ffdccd7138927ffb8c0" + integrity sha512-//oorEZjL6sbPcKUaCdIGlIUeH26mgzimjBB77G6XRgnDl/L5wOnpyBGRe/Mmf5CVW3PwEBE1NjiMZ/ssFh4wA== "@types/prop-types@*": version "15.7.5" resolved "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.5.tgz" +"@types/raf@^3.4.0": + version "3.4.2" + resolved "https://registry.yarnpkg.com/@types/raf/-/raf-3.4.2.tgz#4e2ea094fcce9ed693cd236e530c0c88f2e09e68" + integrity sha512-sM4HyDVlDFl4goOXPF+g9nNHJFZQGot+HgySjM4cRjqXzjdatcEvYrtG4Ia8XumR9T6k8G2tW9B7hnUj51Uf0A== + "@types/react-dom@18.0.11": version "18.0.11" resolved "https://registry.npmjs.org/@types/react-dom/-/react-dom-18.0.11.tgz" @@ -1495,8 +1688,9 @@ resolved "https://registry.npmjs.org/@types/semver/-/semver-7.3.13.tgz" "@types/ws@^8.0.0": - version "8.5.4" - resolved "https://registry.npmjs.org/@types/ws/-/ws-8.5.4.tgz" + version "8.5.5" + resolved "https://registry.yarnpkg.com/@types/ws/-/ws-8.5.5.tgz#af587964aa06682702ee6dcbc7be41a80e4b28eb" + integrity sha512-lwhs8hktwxSjf9UaZ9tG5M03PGogvFaH8gUgLNbN9HKIg0dvv6q+gkSuJ8HN4/VbyxkuLzCjlN7GquQ0gUJfIg== dependencies: "@types/node" "*" @@ -1619,25 +1813,28 @@ d3-selection "^3.0.0" d3-transition "^3.0.1" -"@whatwg-node/events@^0.0.2": - version "0.0.2" - resolved "https://registry.npmjs.org/@whatwg-node/events/-/events-0.0.2.tgz" +"@whatwg-node/events@^0.0.3": + version "0.0.3" + resolved "https://registry.yarnpkg.com/@whatwg-node/events/-/events-0.0.3.tgz#13a65dd4f5893f55280f766e29ae48074927acad" + integrity sha512-IqnKIDWfXBJkvy/k6tzskWTc2NK3LcqHlb+KHGCrjOCH4jfQckRX0NAiIcC/vIqQkzLYw2r2CTSwAxcrtcD6lA== "@whatwg-node/fetch@^0.8.0", "@whatwg-node/fetch@^0.8.1", "@whatwg-node/fetch@^0.8.2": - version "0.8.5" - resolved "https://registry.npmjs.org/@whatwg-node/fetch/-/fetch-0.8.5.tgz" + version "0.8.8" + resolved "https://registry.yarnpkg.com/@whatwg-node/fetch/-/fetch-0.8.8.tgz#48c6ad0c6b7951a73e812f09dd22d75e9fa18cae" + integrity sha512-CdcjGC2vdKhc13KKxgsc6/616BQ7ooDIgPeTuAiE8qfCnS0mGzcfCOoZXypQSz73nxI+GWc7ZReIAVhxoE1KCg== dependencies: "@peculiar/webcrypto" "^1.4.0" - "@whatwg-node/node-fetch" "^0.3.3" + "@whatwg-node/node-fetch" "^0.3.6" busboy "^1.6.0" - urlpattern-polyfill "^7.0.0" + urlpattern-polyfill "^8.0.0" web-streams-polyfill "^3.2.1" -"@whatwg-node/node-fetch@^0.3.3": - version "0.3.5" - resolved "https://registry.npmjs.org/@whatwg-node/node-fetch/-/node-fetch-0.3.5.tgz" +"@whatwg-node/node-fetch@^0.3.6": + version "0.3.6" + resolved "https://registry.yarnpkg.com/@whatwg-node/node-fetch/-/node-fetch-0.3.6.tgz#e28816955f359916e2d830b68a64493124faa6d0" + integrity sha512-w9wKgDO4C95qnXZRwZTfCmLWqyRnooGjcIwG0wADWjw9/HN0p7dtvtgSvItZtUyNteEvgTrd8QojNEqV6DAGTA== dependencies: - "@whatwg-node/events" "^0.0.2" + "@whatwg-node/events" "^0.0.3" busboy "^1.6.0" fast-querystring "^1.1.1" fast-url-parser "^1.1.3" @@ -1689,6 +1886,13 @@ agent-base@6, agent-base@^6.0.2: dependencies: debug "4" +agent-base@^7.0.2, agent-base@^7.1.0: + version "7.1.0" + resolved "https://registry.yarnpkg.com/agent-base/-/agent-base-7.1.0.tgz#536802b76bc0b34aa50195eb2442276d613e3434" + integrity sha512-o/zjMZRhJxny7OyEF+Op8X+efiELC7k7yOjMzgfzVqOzXqkBkWI79YoTdOtsuWd5BWhAGAuOY/Xa6xpiaWXiNg== + dependencies: + debug "^4.3.4" + agentkeepalive@^4.2.1: version "4.3.0" resolved "https://registry.npmjs.org/agentkeepalive/-/agentkeepalive-4.3.0.tgz" @@ -1699,7 +1903,8 @@ agentkeepalive@^4.2.1: aggregate-error@^3.0.0: version "3.1.0" - resolved "https://registry.npmjs.org/aggregate-error/-/aggregate-error-3.1.0.tgz" + resolved "https://registry.yarnpkg.com/aggregate-error/-/aggregate-error-3.1.0.tgz#92670ff50f5359bdb7a3e0d40d0ec30c5737687a" + integrity sha512-4I7Td01quW/RpocfNayFdFVk1qSuoh0E7JrbRJ16nH01HhKFQ88INq9Sd+nd72zqRySlr9BmDA8xlEJ6vJMrYA== dependencies: clean-stack "^2.0.0" indent-string "^4.0.0" @@ -1727,7 +1932,8 @@ amdefine@>=0.0.4: ansi-escapes@^4.2.1, ansi-escapes@^4.3.0: version "4.3.2" - resolved "https://registry.npmjs.org/ansi-escapes/-/ansi-escapes-4.3.2.tgz" + resolved "https://registry.yarnpkg.com/ansi-escapes/-/ansi-escapes-4.3.2.tgz#6b2291d1db7d98b6521d5f1efa42d0f3a9feb65e" + integrity sha512-gKXj5ALrKWQLsYG9jlTRmR/xKluxHV+Z9QEwNIgCfM1/uwPMCuzVVnh5mwTd+OuBZcwSIMbqssNWRm1lE51QaQ== dependencies: type-fest "^0.21.3" @@ -1737,7 +1943,8 @@ ansi-regex@^2.0.0: ansi-regex@^5.0.1: version "5.0.1" - resolved "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz" + resolved "https://registry.yarnpkg.com/ansi-regex/-/ansi-regex-5.0.1.tgz#082cb2c89c9fe8659a311a53bd6a4dc5301db304" + integrity sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ== ansi-styles@^2.2.1: version "2.2.1" @@ -1745,13 +1952,15 @@ ansi-styles@^2.2.1: ansi-styles@^3.2.1: version "3.2.1" - resolved "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz" + resolved "https://registry.yarnpkg.com/ansi-styles/-/ansi-styles-3.2.1.tgz#41fbb20243e50b12be0f04b8dedbf07520ce841d" + integrity sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA== dependencies: color-convert "^1.9.0" ansi-styles@^4.0.0, ansi-styles@^4.1.0, ansi-styles@^4.3.0: version "4.3.0" - resolved "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz" + resolved "https://registry.yarnpkg.com/ansi-styles/-/ansi-styles-4.3.0.tgz#edd803628ae71c04c85ae7a0906edad34b648937" + integrity sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg== dependencies: color-convert "^2.0.1" @@ -1794,7 +2003,8 @@ arg@^5.0.2: argparse@^2.0.1: version "2.0.1" - resolved "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz" + resolved "https://registry.yarnpkg.com/argparse/-/argparse-2.0.1.tgz#246f50f3ca78a3240f6c997e8a9bd1eac49e4b38" + integrity sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q== aria-query@^5.1.3: version "5.1.3" @@ -1814,7 +2024,8 @@ array-includes@^3.1.5, array-includes@^3.1.6: array-union@^2.1.0: version "2.1.0" - resolved "https://registry.npmjs.org/array-union/-/array-union-2.1.0.tgz" + resolved "https://registry.yarnpkg.com/array-union/-/array-union-2.1.0.tgz#b798420adbeb1de828d84acd8a2e23d3efe85e8d" + integrity sha512-HGyxoOTYUyCM6stUe6EJgnd4EoewAI7zMdfqO+kGjnlZmBDz/cR5pf8r/cR4Wq60sL/p0IkcjUEEPwS3GFrIyw== array.prototype.flat@^1.3.1: version "1.3.1" @@ -1846,11 +2057,13 @@ array.prototype.tosorted@^1.1.1: asap@~2.0.3: version "2.0.6" - resolved "https://registry.npmjs.org/asap/-/asap-2.0.6.tgz" + resolved "https://registry.yarnpkg.com/asap/-/asap-2.0.6.tgz#e50347611d7e690943208bbdafebcbc2fb866d46" + integrity sha512-BSHWgDSAiKs50o2Re8ppvp3seVHXSRM44cdSsT9FfNEUUZLOGWVCsiWaRPWM1Znn+mqZ1OfVZ3z3DWEzSp7hRA== asn1js@^3.0.1, asn1js@^3.0.5: version "3.0.5" - resolved "https://registry.npmjs.org/asn1js/-/asn1js-3.0.5.tgz" + resolved "https://registry.yarnpkg.com/asn1js/-/asn1js-3.0.5.tgz#5ea36820443dbefb51cc7f88a2ebb5b462114f38" + integrity sha512-FVnvrKJwpt9LP2lAMl8qZswRNm3T4q9CON+bxldk2iwk3FFpuwhx2FfinyitizWHsVYyaY+y5JzDR0rCMV5yTQ== dependencies: pvtsutils "^1.3.2" pvutils "^1.1.3" @@ -1862,15 +2075,22 @@ ast-types-flow@^0.0.7: astral-regex@^2.0.0: version "2.0.0" - resolved "https://registry.npmjs.org/astral-regex/-/astral-regex-2.0.0.tgz" + resolved "https://registry.yarnpkg.com/astral-regex/-/astral-regex-2.0.0.tgz#483143c567aeed4785759c0865786dc77d7d2e31" + integrity sha512-Z7tMw1ytTXt5jqMcOP+OQteU1VuNK9Y02uuJtKQ1Sv69jXQKKg5cibLwGJow8yzZP+eAc18EmLGPal0bp36rvQ== asynckit@^0.4.0: version "0.4.0" resolved "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz" +atob@^2.1.2: + version "2.1.2" + resolved "https://registry.yarnpkg.com/atob/-/atob-2.1.2.tgz#6d9517eb9e030d2436666651e86bd9f6f13533c9" + integrity sha512-Wm6ukoaOGJi/73p/cl2GvLjTI5JM1k/O14isD73YML8StrH/7/lRFgmg8nICZgD3bZZvjwCGxtMOD3wWNAu8cg== + auto-bind@~4.0.0: version "4.0.0" - resolved "https://registry.npmjs.org/auto-bind/-/auto-bind-4.0.0.tgz" + resolved "https://registry.yarnpkg.com/auto-bind/-/auto-bind-4.0.0.tgz#e3589fc6c2da8f7ca43ba9f84fa52a744fc997fb" + integrity sha512-Hdw8qdNiqdJ8LqT0iK0sVzkFbzg6fhnQqqfWhBDxcHZvU75+B+ayzTy8x+k5Ix0Y92XOhOUlx74ps+bA6BeYMQ== autoprefixer@^10.4.13: version "10.4.14" @@ -1907,11 +2127,13 @@ axobject-query@^3.1.1: babel-plugin-syntax-trailing-function-commas@^7.0.0-beta.0: version "7.0.0-beta.0" - resolved "https://registry.npmjs.org/babel-plugin-syntax-trailing-function-commas/-/babel-plugin-syntax-trailing-function-commas-7.0.0-beta.0.tgz" + resolved "https://registry.yarnpkg.com/babel-plugin-syntax-trailing-function-commas/-/babel-plugin-syntax-trailing-function-commas-7.0.0-beta.0.tgz#aa213c1435e2bffeb6fca842287ef534ad05d5cf" + integrity sha512-Xj9XuRuz3nTSbaTXWv3itLOcxyF4oPD8douBBmj7U9BBC6nEBYfyOJYQMf/8PJAFotC62UY5dFfIGEPr7WswzQ== babel-preset-fbjs@^3.4.0: version "3.4.0" - resolved "https://registry.npmjs.org/babel-preset-fbjs/-/babel-preset-fbjs-3.4.0.tgz" + resolved "https://registry.yarnpkg.com/babel-preset-fbjs/-/babel-preset-fbjs-3.4.0.tgz#38a14e5a7a3b285a3f3a86552d650dca5cf6111c" + integrity sha512-9ywCsCvo1ojrw0b+XYk7aFvTH6D9064t0RIL1rtMf3nsa02Xw41MS7sZw216Im35xj/UY0PDBQsa1brUDDF1Ow== dependencies: "@babel/plugin-proposal-class-properties" "^7.0.0" "@babel/plugin-proposal-object-rest-spread" "^7.0.0" @@ -1943,11 +2165,18 @@ babel-preset-fbjs@^3.4.0: balanced-match@^1.0.0: version "1.0.2" - resolved "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz" + resolved "https://registry.yarnpkg.com/balanced-match/-/balanced-match-1.0.2.tgz#e83e3a7e3f300b34cb9d87f615fa0cbf357690ee" + integrity sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw== + +base64-arraybuffer@^1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/base64-arraybuffer/-/base64-arraybuffer-1.0.2.tgz#1c37589a7c4b0746e34bd1feb951da2df01c1bdc" + integrity sha512-I3yl4r9QB5ZRY3XuJVEPfc2XhZO6YweFPI+UovAzn+8/hb3oJ6lnysaFcjVpkCPfVWFUDvoZ8kmVDP7WyRtYtQ== base64-js@^1.3.1: version "1.5.1" - resolved "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz" + resolved "https://registry.yarnpkg.com/base64-js/-/base64-js-1.5.1.tgz#1b1b440160a5bf7ad40b650f095963481903930a" + integrity sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA== big-integer@1.6.49: version "1.6.49" @@ -1974,7 +2203,8 @@ bip39@^3.1.0: bl@^4.1.0: version "4.1.0" - resolved "https://registry.npmjs.org/bl/-/bl-4.1.0.tgz" + resolved "https://registry.yarnpkg.com/bl/-/bl-4.1.0.tgz#451535264182bec2fbbc83a62ab98cf11d9f7b3a" + integrity sha512-1W07cM9gS6DcLperZfFSj+bWLtaPGSOHWhPiGzXmvVJbRLdG82sH/Kn8EtW1VqWVA54AKf2h5k5BbnIbwF3h6w== dependencies: buffer "^5.5.0" inherits "^2.0.4" @@ -1986,7 +2216,8 @@ body-scroll-lock@^4.0.0-beta.0: brace-expansion@^1.1.7: version "1.1.11" - resolved "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz" + resolved "https://registry.yarnpkg.com/brace-expansion/-/brace-expansion-1.1.11.tgz#3c7fcbf529d87226f3d2f52b966ff5271eb441dd" + integrity sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA== dependencies: balanced-match "^1.0.0" concat-map "0.0.1" @@ -1999,11 +2230,12 @@ brace-expansion@^2.0.1: braces@^3.0.2, braces@~3.0.2: version "3.0.2" - resolved "https://registry.npmjs.org/braces/-/braces-3.0.2.tgz" + resolved "https://registry.yarnpkg.com/braces/-/braces-3.0.2.tgz#3454e1a462ee8d599e236df336cd9ea4f8afe107" + integrity sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A== dependencies: fill-range "^7.0.1" -browserslist@^4.21.3, browserslist@^4.21.5: +browserslist@^4.21.5: version "4.21.5" resolved "https://registry.npmjs.org/browserslist/-/browserslist-4.21.5.tgz" dependencies: @@ -2012,19 +2244,36 @@ browserslist@^4.21.3, browserslist@^4.21.5: node-releases "^2.0.8" update-browserslist-db "^1.0.10" +browserslist@^4.21.9: + version "4.21.10" + resolved "https://registry.yarnpkg.com/browserslist/-/browserslist-4.21.10.tgz#dbbac576628c13d3b2231332cb2ec5a46e015bb0" + integrity sha512-bipEBdZfVH5/pwrvqc+Ub0kUPVfGUhlKxbvfD+z1BDnPEO/X98ruXGA1WP5ASpAFKan7Qr6j736IacbZQuAlKQ== + dependencies: + caniuse-lite "^1.0.30001517" + electron-to-chromium "^1.4.477" + node-releases "^2.0.13" + update-browserslist-db "^1.0.11" + bser@2.1.1: version "2.1.1" - resolved "https://registry.npmjs.org/bser/-/bser-2.1.1.tgz" + resolved "https://registry.yarnpkg.com/bser/-/bser-2.1.1.tgz#e6787da20ece9d07998533cfd9de6f5c38f4bc05" + integrity sha512-gQxTNE/GAfIIrmHLUE3oJyp5FO6HRBfhjnw4/wMmA63ZGDJnWBmgY/lyQBpnDUkGmAhbSe39tx2d/iTOAfglwQ== dependencies: node-int64 "^0.4.0" +btoa@^1.2.1: + version "1.2.1" + resolved "https://registry.yarnpkg.com/btoa/-/btoa-1.2.1.tgz#01a9909f8b2c93f6bf680ba26131eb30f7fa3d73" + integrity sha512-SB4/MIGlsiVkMcHmT+pSmIPoNDoHg+7cMzmt3Uxt628MTz2487DKSqK/fuhFBrkuqrYv5UCEnACpF4dTFNKc/g== + buffer-equal-constant-time@1.0.1: version "1.0.1" resolved "https://registry.npmjs.org/buffer-equal-constant-time/-/buffer-equal-constant-time-1.0.1.tgz" buffer@^5.5.0: version "5.7.1" - resolved "https://registry.npmjs.org/buffer/-/buffer-5.7.1.tgz" + resolved "https://registry.yarnpkg.com/buffer/-/buffer-5.7.1.tgz#ba62e7c13133053582197160851a8f648e99eed0" + integrity sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ== dependencies: base64-js "^1.3.1" ieee754 "^1.1.13" @@ -2044,7 +2293,8 @@ builtins@^5.0.0: busboy@^1.6.0: version "1.6.0" - resolved "https://registry.npmjs.org/busboy/-/busboy-1.6.0.tgz" + resolved "https://registry.yarnpkg.com/busboy/-/busboy-1.6.0.tgz#966ea36a9502e43cdb9146962523b92f531f6893" + integrity sha512-8SFQbg/0hQ9xy3UNTB0YEnsNBbWfhf7RtnzpL7TkBiTBRfrQ9Fxcnz7VJsleJpyp6rVLvXiuORqjlHi5q+PYuA== dependencies: streamsearch "^1.1.0" @@ -2102,11 +2352,13 @@ call-bind@^1.0.0, call-bind@^1.0.2, call-bind@~1.0.2: callsites@^3.0.0: version "3.1.0" - resolved "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz" + resolved "https://registry.yarnpkg.com/callsites/-/callsites-3.1.0.tgz#b3630abd8943432f54b3f0519238e33cd7df2f73" + integrity sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ== camel-case@^4.1.2: version "4.1.2" - resolved "https://registry.npmjs.org/camel-case/-/camel-case-4.1.2.tgz" + resolved "https://registry.yarnpkg.com/camel-case/-/camel-case-4.1.2.tgz#9728072a954f805228225a6deea6b38461e1bd5a" + integrity sha512-gxGWBrTT1JuMx6R+o5PTXMmUnhnVzLQ9SNutD4YqKtI6ap897t3tKECYla6gCWEkplXnlNybEkZg9GEGxKFCgw== dependencies: pascal-case "^3.1.2" tslib "^2.0.3" @@ -2121,15 +2373,36 @@ camelcase@^1.0.2: camelcase@^5.0.0: version "5.3.1" - resolved "https://registry.npmjs.org/camelcase/-/camelcase-5.3.1.tgz" + resolved "https://registry.yarnpkg.com/camelcase/-/camelcase-5.3.1.tgz#e3c9b31569e106811df242f715725a1f4c494320" + integrity sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg== -caniuse-lite@^1.0.30001406, caniuse-lite@^1.0.30001449, caniuse-lite@^1.0.30001464: +caniuse-lite@^1.0.30001406, caniuse-lite@^1.0.30001464: version "1.0.30001481" resolved "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001481.tgz" +caniuse-lite@^1.0.30001449, caniuse-lite@^1.0.30001517: + version "1.0.30001522" + resolved "https://registry.yarnpkg.com/caniuse-lite/-/caniuse-lite-1.0.30001522.tgz#44b87a406c901269adcdb834713e23582dd71856" + integrity sha512-TKiyTVZxJGhsTszLuzb+6vUZSjVOAhClszBr2Ta2k9IwtNBT/4dzmL6aywt0HCgEZlmwJzXJd8yNiob6HgwTRg== + +canvg@^3.0.6: + version "3.0.10" + resolved "https://registry.yarnpkg.com/canvg/-/canvg-3.0.10.tgz#8e52a2d088b6ffa23ac78970b2a9eebfae0ef4b3" + integrity sha512-qwR2FRNO9NlzTeKIPIKpnTY6fqwuYSequ8Ru8c0YkYU7U0oW+hLUvWadLvAu1Rl72OMNiFhoLu4f8eUjQ7l/+Q== + dependencies: + "@babel/runtime" "^7.12.5" + "@types/raf" "^3.4.0" + core-js "^3.8.3" + raf "^3.4.1" + regenerator-runtime "^0.13.7" + rgbcolor "^1.0.1" + stackblur-canvas "^2.0.0" + svg-pathdata "^6.0.3" + capital-case@^1.0.4: version "1.0.4" - resolved "https://registry.npmjs.org/capital-case/-/capital-case-1.0.4.tgz" + resolved "https://registry.yarnpkg.com/capital-case/-/capital-case-1.0.4.tgz#9d130292353c9249f6b00fa5852bee38a717e669" + integrity sha512-ds37W8CytHgwnhGGTi88pcPyR15qoNkOpYwmMMfnWqqWgESapLqvDx6huFjQ5vqWSn2Z06173XNA7LtMOeUh1A== dependencies: no-case "^3.0.4" tslib "^2.0.3" @@ -2152,9 +2425,10 @@ chalk@^1.1.1: strip-ansi "^3.0.0" supports-color "^2.0.0" -chalk@^2.0.0: +chalk@^2.4.2: version "2.4.2" - resolved "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz" + resolved "https://registry.yarnpkg.com/chalk/-/chalk-2.4.2.tgz#cd42541677a54333cf541a49108c1432b44c9424" + integrity sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ== dependencies: ansi-styles "^3.2.1" escape-string-regexp "^1.0.5" @@ -2162,14 +2436,16 @@ chalk@^2.0.0: chalk@^4.0.0, chalk@^4.1.0, chalk@^4.1.1, chalk@^4.1.2: version "4.1.2" - resolved "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz" + resolved "https://registry.yarnpkg.com/chalk/-/chalk-4.1.2.tgz#aac4e2b7734a740867aeb16bf02aad556a1e7a01" + integrity sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA== dependencies: ansi-styles "^4.1.0" supports-color "^7.1.0" change-case-all@1.0.14: version "1.0.14" - resolved "https://registry.npmjs.org/change-case-all/-/change-case-all-1.0.14.tgz" + resolved "https://registry.yarnpkg.com/change-case-all/-/change-case-all-1.0.14.tgz#bac04da08ad143278d0ac3dda7eccd39280bfba1" + integrity sha512-CWVm2uT7dmSHdO/z1CXT/n47mWonyypzBbuCy5tN7uMg22BsfkhwT6oHmFCAk+gL1LOOxhdbB9SZz3J1KTY3gA== dependencies: change-case "^4.1.2" is-lower-case "^2.0.2" @@ -2184,7 +2460,8 @@ change-case-all@1.0.14: change-case-all@1.0.15: version "1.0.15" - resolved "https://registry.npmjs.org/change-case-all/-/change-case-all-1.0.15.tgz" + resolved "https://registry.yarnpkg.com/change-case-all/-/change-case-all-1.0.15.tgz#de29393167fc101d646cd76b0ef23e27d09756ad" + integrity sha512-3+GIFhk3sNuvFAJKU46o26OdzudQlPNBCu1ZQi3cMeMHhty1bhDxu2WrEilVNYaGvqUtR1VSigFcJOiS13dRhQ== dependencies: change-case "^4.1.2" is-lower-case "^2.0.2" @@ -2199,7 +2476,8 @@ change-case-all@1.0.15: change-case@^4.1.2: version "4.1.2" - resolved "https://registry.npmjs.org/change-case/-/change-case-4.1.2.tgz" + resolved "https://registry.yarnpkg.com/change-case/-/change-case-4.1.2.tgz#fedfc5f136045e2398c0410ee441f95704641e12" + integrity sha512-bSxY2ws9OtviILG1EiY5K7NNxkqg/JnRnFxLtKQ96JaviiIxi7djMrSd0ECT9AC+lttClmYwKw53BWpOMblo7A== dependencies: camel-case "^4.1.2" capital-case "^1.0.4" @@ -2216,7 +2494,8 @@ change-case@^4.1.2: chardet@^0.7.0: version "0.7.0" - resolved "https://registry.npmjs.org/chardet/-/chardet-0.7.0.tgz" + resolved "https://registry.yarnpkg.com/chardet/-/chardet-0.7.0.tgz#90094849f0937f2eedc2425d0d28a9e5f0cbad9e" + integrity sha512-mT8iDcrh03qDGRRmoA2hmBJnxpllMR+0/0qlzjqZES6NdiWDcZkCNAk4rPFZ9Q85r27unkiNNg8ZOiwZXBHwcA== "chokidar@>=3.0.0 <4.0.0", chokidar@^3.5.3: version "3.5.3" @@ -2256,7 +2535,8 @@ classnames@^2.3.1, classnames@^2.3.2: clean-stack@^2.0.0: version "2.2.0" - resolved "https://registry.npmjs.org/clean-stack/-/clean-stack-2.2.0.tgz" + resolved "https://registry.yarnpkg.com/clean-stack/-/clean-stack-2.2.0.tgz#ee8472dbb129e727b31e8a10a427dee9dfe4008b" + integrity sha512-4diC9HaTE+KRAMWhDhrGOECgWZxoevMc5TlkObMqNSsVU62PYzXZ/SMTjzyGAFF1YusgxGcSWTEXBhp0CPwQ1A== cli-columns@^4.0.0: version "4.0.0" @@ -2267,13 +2547,15 @@ cli-columns@^4.0.0: cli-cursor@^3.1.0: version "3.1.0" - resolved "https://registry.npmjs.org/cli-cursor/-/cli-cursor-3.1.0.tgz" + resolved "https://registry.yarnpkg.com/cli-cursor/-/cli-cursor-3.1.0.tgz#264305a7ae490d1d03bf0c9ba7c925d1753af307" + integrity sha512-I/zHAwsKf9FqGoXM4WWRACob9+SNukZTd94DWF57E4toouRulbCxcUh6RKUEOQlYTHJnzkPMySvPNaaSLNfLZw== dependencies: restore-cursor "^3.1.0" cli-spinners@^2.5.0: - version "2.8.0" - resolved "https://registry.npmjs.org/cli-spinners/-/cli-spinners-2.8.0.tgz" + version "2.9.0" + resolved "https://registry.yarnpkg.com/cli-spinners/-/cli-spinners-2.9.0.tgz#5881d0ad96381e117bbe07ad91f2008fe6ffd8db" + integrity sha512-4/aL9X3Wh0yiMQlE+eeRhWP6vclO3QRtw1JHKIT0FFUs5FjpFmESqtMvYZ0+lbzBw900b95mS0hohy+qn2VK/g== cli-table3@^0.6.3: version "0.6.3" @@ -2285,14 +2567,16 @@ cli-table3@^0.6.3: cli-truncate@^2.1.0: version "2.1.0" - resolved "https://registry.npmjs.org/cli-truncate/-/cli-truncate-2.1.0.tgz" + resolved "https://registry.yarnpkg.com/cli-truncate/-/cli-truncate-2.1.0.tgz#c39e28bf05edcde5be3b98992a22deed5a2b93c7" + integrity sha512-n8fOixwDD6b/ObinzTrp1ZKFzbgvKZvuz/TvejnLn1aQfC6r52XEx85FmuC+3HI+JM7coBRXUvNqEU2PHVrHpg== dependencies: slice-ansi "^3.0.0" string-width "^4.2.0" cli-width@^3.0.0: version "3.0.0" - resolved "https://registry.npmjs.org/cli-width/-/cli-width-3.0.0.tgz" + resolved "https://registry.yarnpkg.com/cli-width/-/cli-width-3.0.0.tgz#a2f48437a2caa9a22436e794bf071ec9e61cedf6" + integrity sha512-FxqpkPPwu1HjuN93Omfm4h8uIanXofW0RxVEW3k5RKx+mJJYSthzNhp32Kzxxy3YAEZ/Dc/EWN1vZRY0+kOhbw== client-only@0.0.1, client-only@^0.0.1: version "0.0.1" @@ -2308,7 +2592,8 @@ cliui@^2.1.0: cliui@^6.0.0: version "6.0.0" - resolved "https://registry.npmjs.org/cliui/-/cliui-6.0.0.tgz" + resolved "https://registry.yarnpkg.com/cliui/-/cliui-6.0.0.tgz#511d702c0c4e41ca156d7d0e96021f23e13225b1" + integrity sha512-t6wbgtoCXvAzst7QgXxJYqPt0usEfbgQdftEPbLL/cvv6HPE5VgvqCuAIDR0NgU52ds6rFwqrgakNLrHEjCbrQ== dependencies: string-width "^4.2.0" strip-ansi "^6.0.0" @@ -2316,7 +2601,8 @@ cliui@^6.0.0: cliui@^8.0.1: version "8.0.1" - resolved "https://registry.npmjs.org/cliui/-/cliui-8.0.1.tgz" + resolved "https://registry.yarnpkg.com/cliui/-/cliui-8.0.1.tgz#0c04b075db02cbfe60dc8e6cf2f5486b1a3608aa" + integrity sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ== dependencies: string-width "^4.2.0" strip-ansi "^6.0.1" @@ -2324,7 +2610,8 @@ cliui@^8.0.1: clone@^1.0.2: version "1.0.4" - resolved "https://registry.npmjs.org/clone/-/clone-1.0.4.tgz" + resolved "https://registry.yarnpkg.com/clone/-/clone-1.0.4.tgz#da309cc263df15994c688ca902179ca3c7cd7c7e" + integrity sha512-JQHZ2QMW6l3aH/j6xCqQThY/9OH4D/9ls34cgkUBiEeocRTU04tHfKPBsUK1PqZCUQM7GiA0IIXJSuXHI64Kbg== clsx@^1.1.1, clsx@^1.2.1: version "1.2.1" @@ -2336,23 +2623,27 @@ cmd-shim@^6.0.0: color-convert@^1.9.0: version "1.9.3" - resolved "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz" + resolved "https://registry.yarnpkg.com/color-convert/-/color-convert-1.9.3.tgz#bb71850690e1f136567de629d2d5471deda4c1e8" + integrity sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg== dependencies: color-name "1.1.3" color-convert@^2.0.1: version "2.0.1" - resolved "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz" + resolved "https://registry.yarnpkg.com/color-convert/-/color-convert-2.0.1.tgz#72d3a68d598c9bdb3af2ad1e84f21d896abd4de3" + integrity sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ== dependencies: color-name "~1.1.4" color-name@1.1.3: version "1.1.3" - resolved "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz" + resolved "https://registry.yarnpkg.com/color-name/-/color-name-1.1.3.tgz#a7d0558bd89c42f795dd42328f740831ca53bc25" + integrity sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw== color-name@^1.1.4, color-name@~1.1.4: version "1.1.4" - resolved "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz" + resolved "https://registry.yarnpkg.com/color-name/-/color-name-1.1.4.tgz#c2a09a87acbde69543de6f63fa3995c826c536a2" + integrity sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA== color-support@^1.1.3: version "1.1.3" @@ -2360,7 +2651,8 @@ color-support@^1.1.3: colorette@^2.0.16: version "2.0.20" - resolved "https://registry.npmjs.org/colorette/-/colorette-2.0.20.tgz" + resolved "https://registry.yarnpkg.com/colorette/-/colorette-2.0.20.tgz#9eb793e6833067f7235902fcd3b09917a000a95a" + integrity sha512-IfEDxwoWIjkeXL1eXcDiow4UbKjhLdq6/EuSVR9GMN7KVH3r9gQ83e73hsz1Nd1T3ijd5xv1wcWRYO+D6kCI2w== columnify@^1.6.0: version "1.6.0" @@ -2385,11 +2677,13 @@ common-ancestor-path@^1.0.1: common-tags@1.8.2: version "1.8.2" - resolved "https://registry.npmjs.org/common-tags/-/common-tags-1.8.2.tgz" + resolved "https://registry.yarnpkg.com/common-tags/-/common-tags-1.8.2.tgz#94ebb3c076d26032745fd54face7f688ef5ac9c6" + integrity sha512-gk/Z852D2Wtb//0I+kRFNKKE9dIIVirjoqPoA1wJU+XePVXZfGeBpk45+A1rKO4Q43prqWBNY/MiIeRLbPWUaA== concat-map@0.0.1: version "0.0.1" - resolved "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz" + resolved "https://registry.yarnpkg.com/concat-map/-/concat-map-0.0.1.tgz#d8a96bd77fd68df7793a73036a3ba0d5405d477b" + integrity sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg== console-control-strings@^1.1.0: version "1.1.0" @@ -2397,7 +2691,8 @@ console-control-strings@^1.1.0: constant-case@^3.0.4: version "3.0.4" - resolved "https://registry.npmjs.org/constant-case/-/constant-case-3.0.4.tgz" + resolved "https://registry.yarnpkg.com/constant-case/-/constant-case-3.0.4.tgz#3b84a9aeaf4cf31ec45e6bf5de91bdfb0589faf1" + integrity sha512-I2hSBi7Vvs7BEuJDr5dDHfzb/Ruj3FyvFyh7KLilAjNQw3Be+xgqUBA2W6scVEcL0hL1dwPRtIqEPVUCKkSsyQ== dependencies: no-case "^3.0.4" tslib "^2.0.3" @@ -2409,15 +2704,22 @@ contour_plot@^0.0.1: convert-source-map@^1.7.0: version "1.9.0" - resolved "https://registry.npmjs.org/convert-source-map/-/convert-source-map-1.9.0.tgz" + resolved "https://registry.yarnpkg.com/convert-source-map/-/convert-source-map-1.9.0.tgz#7faae62353fb4213366d0ca98358d22e8368b05f" + integrity sha512-ASFBup0Mz1uyiIjANan1jzLQami9z1PoYSZCiiYW2FczPbenXc45FZdBZLzOT+r6+iciuEModtmCti+hjaAk0A== cookie@^0.5.0: version "0.5.0" resolved "https://registry.npmjs.org/cookie/-/cookie-0.5.0.tgz" +core-js@^3.6.0, core-js@^3.8.3: + version "3.33.2" + resolved "https://registry.yarnpkg.com/core-js/-/core-js-3.33.2.tgz#312bbf6996a3a517c04c99b9909cdd27138d1ceb" + integrity sha512-XeBzWI6QL3nJQiHmdzbAOiMYqjrb7hwU7A39Qhvd/POSa/t9E1AeZyEZx3fNvp/vtM8zXwhoL0FsiS0hD0pruQ== + cosmiconfig@8.0.0: version "8.0.0" - resolved "https://registry.npmjs.org/cosmiconfig/-/cosmiconfig-8.0.0.tgz" + resolved "https://registry.yarnpkg.com/cosmiconfig/-/cosmiconfig-8.0.0.tgz#e9feae014eab580f858f8a0288f38997a7bebe97" + integrity sha512-da1EafcpH6b/TD8vDRaWV7xFINlHlF6zKsGwS1TsuVJTZRkquaS5HTMq7uq6h31619QjbsYl21gVDOm32KM1vQ== dependencies: import-fresh "^3.2.1" js-yaml "^4.1.0" @@ -2426,7 +2728,8 @@ cosmiconfig@8.0.0: cosmiconfig@^7.0.0: version "7.1.0" - resolved "https://registry.npmjs.org/cosmiconfig/-/cosmiconfig-7.1.0.tgz" + resolved "https://registry.yarnpkg.com/cosmiconfig/-/cosmiconfig-7.1.0.tgz#1443b9afa596b670082ea46cbd8f6a62b84635f6" + integrity sha512-AdmX6xUzdNASswsFtmwSt7Vj8po9IuqXm0UXz7QKPuEUmPB4XyjGfaAr2PSuELMwkRMVH1EpIkX5bTZGRB3eCA== dependencies: "@types/parse-json" "^4.0.0" import-fresh "^3.2.1" @@ -2460,6 +2763,13 @@ cross-spawn@^7.0.2, cross-spawn@^7.0.3: shebang-command "^2.0.0" which "^2.0.1" +css-line-break@^2.1.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/css-line-break/-/css-line-break-2.1.0.tgz#bfef660dfa6f5397ea54116bb3cb4873edbc4fa0" + integrity sha512-FHcKFCZcAha3LwfVBhCQbW2nCNbkZXn7KVUJcsT5/P8YmfsVja0FMPJr0B903j/E69HUphKiV9iQArX8SDYA4w== + dependencies: + utrie "^1.0.2" + cssesc@^3.0.0: version "3.0.0" resolved "https://registry.npmjs.org/cssesc/-/cssesc-3.0.0.tgz" @@ -2583,9 +2893,10 @@ damerau-levenshtein@^1.0.8: version "1.0.8" resolved "https://registry.npmjs.org/damerau-levenshtein/-/damerau-levenshtein-1.0.8.tgz" -dataloader@2.2.2, dataloader@^2.2.2: +dataloader@^2.2.2: version "2.2.2" - resolved "https://registry.npmjs.org/dataloader/-/dataloader-2.2.2.tgz" + resolved "https://registry.yarnpkg.com/dataloader/-/dataloader-2.2.2.tgz#216dc509b5abe39d43a9b9d97e6e5e473dfbe3e0" + integrity sha512-8YnDaaf7N3k/q5HnTJVuzSyLETjoZjVmHc4AeKAzOvKHEFQKcn64OKBfzHYtE9zGjctNM7V9I0MfnUVLpi7M5g== date-fns@^2.29.3: version "2.30.0" @@ -2595,7 +2906,8 @@ date-fns@^2.29.3: debounce@^1.2.0: version "1.2.1" - resolved "https://registry.npmjs.org/debounce/-/debounce-1.2.1.tgz" + resolved "https://registry.yarnpkg.com/debounce/-/debounce-1.2.1.tgz#38881d8f4166a5c5848020c11827b834bcb3e0a5" + integrity sha512-XRRe6Glud4rd/ZGQfiV1ruXSfbvfJedlV9Y6zOlP+2K04vBYiJEte6stfFkCP03aMnY5tsipamumUjL14fofug== debug@4, debug@^4.1.0, debug@^4.1.1, debug@^4.3.1, debug@^4.3.2, debug@^4.3.3, debug@^4.3.4: version "4.3.4" @@ -2652,7 +2964,8 @@ deep-is@^0.1.3: defaults@^1.0.3: version "1.0.4" - resolved "https://registry.npmjs.org/defaults/-/defaults-1.0.4.tgz" + resolved "https://registry.yarnpkg.com/defaults/-/defaults-1.0.4.tgz#b0b02062c1e2aa62ff5d9528f0f98baa90978d7a" + integrity sha512-eFuaLoy/Rxalv2kr+lqMlUnrDWV+3j4pljOIJgLIhI058IQfWJ7vXhyEIHu+HtC738klGALYxOKDO0bQP3tg8A== dependencies: clone "^1.0.2" @@ -2685,11 +2998,18 @@ depd@^2.0.0: dependency-graph@^0.11.0: version "0.11.0" - resolved "https://registry.npmjs.org/dependency-graph/-/dependency-graph-0.11.0.tgz" + resolved "https://registry.yarnpkg.com/dependency-graph/-/dependency-graph-0.11.0.tgz#ac0ce7ed68a54da22165a85e97a01d53f5eb2e27" + integrity sha512-JeMq7fEshyepOWDfcfHK06N3MhyPhz++vtqWhMT5O9A3K42rdsEDpfdVqjaqaAhsw6a+ZqeDvQVtD0hFHQWrzg== detect-indent@^6.0.0: version "6.1.0" - resolved "https://registry.npmjs.org/detect-indent/-/detect-indent-6.1.0.tgz" + resolved "https://registry.yarnpkg.com/detect-indent/-/detect-indent-6.1.0.tgz#592485ebbbf6b3b1ab2be175c8393d04ca0d57e6" + integrity sha512-reYkTUJAZb9gUuZ2RvVCNhVHdg62RHnJ7WJl8ftMi4diZ6NWlciOzQN88pUhSELEwflJht4oQDv0F0BMlwaYtA== + +detect-libc@^1.0.3: + version "1.0.3" + resolved "https://registry.yarnpkg.com/detect-libc/-/detect-libc-1.0.3.tgz#fa137c4bd698edf55cd5cd02ac559f91a4c4ba9b" + integrity sha512-pGjwhsmsp4kL2RTz08wcOlGN83otlqHeD/Z5T8GXZB+/YcpQ/dgo+lbU8ZsGxV0HIvqqxo9l7mqYwyYMD9bKDg== didyoumean@^1.2.2: version "1.2.2" @@ -2701,7 +3021,8 @@ diff@^5.1.0: dir-glob@^3.0.1: version "3.0.1" - resolved "https://registry.npmjs.org/dir-glob/-/dir-glob-3.0.1.tgz" + resolved "https://registry.yarnpkg.com/dir-glob/-/dir-glob-3.0.1.tgz#56dbf73d992a4a93ba1584f4534063fd2e41717f" + integrity sha512-WkrWp9GR4KXfKGYzOLmTuGVi1UWFfws377n9cc55/tb6DuqyF6pcQ5AbiHEshaDpY9v6oaSr2XCDidGmMwdzIA== dependencies: path-type "^4.0.0" @@ -2721,16 +3042,23 @@ doctrine@^3.0.0: dependencies: esutils "^2.0.2" +dompurify@^2.2.0: + version "2.4.7" + resolved "https://registry.yarnpkg.com/dompurify/-/dompurify-2.4.7.tgz#277adeb40a2c84be2d42a8bcd45f582bfa4d0cfc" + integrity sha512-kxxKlPEDa6Nc5WJi+qRgPbOAbgTpSULL+vI3NUXsZMlkJxTqYI9wg5ZTay2sFrdZRWHPWNi+EdAhcJf81WtoMQ== + dot-case@^3.0.4: version "3.0.4" - resolved "https://registry.npmjs.org/dot-case/-/dot-case-3.0.4.tgz" + resolved "https://registry.yarnpkg.com/dot-case/-/dot-case-3.0.4.tgz#9b2b670d00a431667a8a75ba29cd1b98809ce751" + integrity sha512-Kv5nKlh6yRrdrGvxeJ2e5y2eRUpkUosIW4A2AS38zwSz27zu7ufDwQPi5Jhs3XAlGNetl3bmnGhQsMtkKJnj3w== dependencies: no-case "^3.0.4" tslib "^2.0.3" dotenv@^16.0.0: - version "16.0.3" - resolved "https://registry.npmjs.org/dotenv/-/dotenv-16.0.3.tgz" + version "16.3.1" + resolved "https://registry.yarnpkg.com/dotenv/-/dotenv-16.3.1.tgz#369034de7d7e5b120972693352a3bf112172cc3e" + integrity sha512-IPzF4w4/Rd94bA9imS68tZBaYyBWSCE47V1RGuMrB94iyTOIEwRmVL2x/4An+6mETpLrKJ5hQkB8W4kFAadeIQ== dotignore@~0.1.2: version "0.1.2" @@ -2740,7 +3068,8 @@ dotignore@~0.1.2: dset@^3.1.2: version "3.1.2" - resolved "https://registry.npmjs.org/dset/-/dset-3.1.2.tgz" + resolved "https://registry.yarnpkg.com/dset/-/dset-3.1.2.tgz#89c436ca6450398396dc6538ea00abc0c54cd45a" + integrity sha512-g/M9sqy3oHe477Ar4voQxWtaPIFw1jTdKZuomOjhCcBx9nHUNn0pu6NopuFFrTh/TRZIKEj+76vLWFu9BNKk+Q== ecdsa-sig-formatter@1.0.11: version "1.0.11" @@ -2748,9 +3077,10 @@ ecdsa-sig-formatter@1.0.11: dependencies: safe-buffer "^5.0.1" -electron-to-chromium@^1.4.284: - version "1.4.369" - resolved "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.4.369.tgz" +electron-to-chromium@^1.4.284, electron-to-chromium@^1.4.477: + version "1.4.496" + resolved "https://registry.yarnpkg.com/electron-to-chromium/-/electron-to-chromium-1.4.496.tgz#a57534b70d2bdee7e1ad7dbd4c91e560cbd08db1" + integrity sha512-qeXC3Zbykq44RCrBa4kr8v/dWzYJA8rAwpyh9Qd+NKWoJfjG5vvJqy9XOJ9H4P/lqulZBCgUWAYi+FeK5AuJ8g== ellipsize@^0.2.0: version "0.2.0" @@ -2764,7 +3094,8 @@ ellipsize@^0.5.1: emoji-regex@^8.0.0: version "8.0.0" - resolved "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz" + resolved "https://registry.yarnpkg.com/emoji-regex/-/emoji-regex-8.0.0.tgz#e818fd69ce5ccfcb404594f842963bf53164cc37" + integrity sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A== emoji-regex@^9.2.2: version "9.2.2" @@ -2793,7 +3124,8 @@ err-code@^2.0.2: error-ex@^1.3.1: version "1.3.2" - resolved "https://registry.npmjs.org/error-ex/-/error-ex-1.3.2.tgz" + resolved "https://registry.yarnpkg.com/error-ex/-/error-ex-1.3.2.tgz#b4ac40648107fdcdcfae242f428bea8a14d4f1bf" + integrity sha512-7dFHNmqeFSEt2ZBsCriorKnn3Z2pj+fd9kmI6QoWw4//DL+icEBfc0U7qJCisqrTsKTjw4fNFy2pW9OqStD84g== dependencies: is-arrayish "^0.2.1" @@ -2873,7 +3205,8 @@ es-to-primitive@^1.2.1: escalade@^3.1.1: version "3.1.1" - resolved "https://registry.npmjs.org/escalade/-/escalade-3.1.1.tgz" + resolved "https://registry.yarnpkg.com/escalade/-/escalade-3.1.1.tgz#d8cfdc7000965c5a0174b4a82eaa5c0552742e40" + integrity sha512-k0er2gUkLf8O0zKJiAhmkTnJlTvINGv7ygDNPbeIsX/TJjGJZHuh9B2UxbsaEkmlEo9MfhrSzmhIlhRlI2GXnw== escape-string-regexp@^1.0.2, escape-string-regexp@^1.0.5: version "1.0.5" @@ -3112,7 +3445,8 @@ events@^3.3.0: external-editor@^3.0.3: version "3.1.0" - resolved "https://registry.npmjs.org/external-editor/-/external-editor-3.1.0.tgz" + resolved "https://registry.yarnpkg.com/external-editor/-/external-editor-3.1.0.tgz#cb03f740befae03ea4d283caed2741a83f335495" + integrity sha512-hMQ4CX1p1izmuLYyZqLMO/qGNw10wSv9QDCPfzXfyFrOaCSSoRfqE1Kf1s5an66J5JZC62NewG+mK49jOCtQew== dependencies: chardet "^0.7.0" iconv-lite "^0.4.24" @@ -3120,11 +3454,13 @@ external-editor@^3.0.3: extract-files@^11.0.0: version "11.0.0" - resolved "https://registry.npmjs.org/extract-files/-/extract-files-11.0.0.tgz" + resolved "https://registry.yarnpkg.com/extract-files/-/extract-files-11.0.0.tgz#b72d428712f787eef1f5193aff8ab5351ca8469a" + integrity sha512-FuoE1qtbJ4bBVvv94CC7s0oTnKUGvQs+Rjf1L2SJFfS+HTVVjhPFtehPdQ0JiGPqVNfSSZvL5yzHHQq2Z4WNhQ== fast-decode-uri-component@^1.0.1: version "1.0.1" - resolved "https://registry.npmjs.org/fast-decode-uri-component/-/fast-decode-uri-component-1.0.1.tgz" + resolved "https://registry.yarnpkg.com/fast-decode-uri-component/-/fast-decode-uri-component-1.0.1.tgz#46f8b6c22b30ff7a81357d4f59abfae938202543" + integrity sha512-WKgKWg5eUxvRZGwW8FvfbaH7AXSh2cL+3j5fMGzUMCxWBJ3dV3a7Wz8y2f/uQ0e3B6WmodD3oS54jTQ9HVTIIg== fast-deep-equal@^3.1.1, fast-deep-equal@^3.1.3: version "3.1.3" @@ -3134,7 +3470,7 @@ fast-diff@^1.1.2: version "1.2.0" resolved "https://registry.npmjs.org/fast-diff/-/fast-diff-1.2.0.tgz" -fast-glob@^3.2.11, fast-glob@^3.2.12, fast-glob@^3.2.9: +fast-glob@^3.2.11, fast-glob@^3.2.12: version "3.2.12" resolved "https://registry.npmjs.org/fast-glob/-/fast-glob-3.2.12.tgz" dependencies: @@ -3144,6 +3480,17 @@ fast-glob@^3.2.11, fast-glob@^3.2.12, fast-glob@^3.2.9: merge2 "^1.3.0" micromatch "^4.0.4" +fast-glob@^3.2.9: + version "3.3.1" + resolved "https://registry.yarnpkg.com/fast-glob/-/fast-glob-3.3.1.tgz#784b4e897340f3dbbef17413b3f11acf03c874c4" + integrity sha512-kNFPyjhh5cKjrUltxs+wFx+ZkbRaxxmZ+X0ZU31SOsxCEtP9VPgtq2teZw1DebupL5GmDaNQ6yKMMVcM41iqDg== + 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" + fast-json-stable-stringify@^2.0.0: version "2.1.0" resolved "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz" @@ -3153,14 +3500,16 @@ fast-levenshtein@^2.0.6: resolved "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz" fast-querystring@^1.1.1: - version "1.1.1" - resolved "https://registry.npmjs.org/fast-querystring/-/fast-querystring-1.1.1.tgz" + version "1.1.2" + resolved "https://registry.yarnpkg.com/fast-querystring/-/fast-querystring-1.1.2.tgz#a6d24937b4fc6f791b4ee31dcb6f53aeafb89f53" + integrity sha512-g6KuKWmFXc0fID8WWH0jit4g0AGBoJhCkJMb1RmbsSEUNvQ+ZC8D6CUZ+GtF8nMzSPXnhiePyyqqipzNNEnHjg== dependencies: fast-decode-uri-component "^1.0.1" fast-url-parser@^1.1.3: version "1.1.3" - resolved "https://registry.npmjs.org/fast-url-parser/-/fast-url-parser-1.1.3.tgz" + resolved "https://registry.yarnpkg.com/fast-url-parser/-/fast-url-parser-1.1.3.tgz#f4af3ea9f34d8a271cf58ad2b3759f431f0b318d" + integrity sha512-5jOCVXADYNuRkKFzNJ0dCCewsZiYo0dz8QNYljkOpFC6r2U4OBmKtvm/Tsuh4w1YYdDqDb31a8TVhBJ2OJKdqQ== dependencies: punycode "^1.3.2" @@ -3170,23 +3519,27 @@ fastest-levenshtein@^1.0.16: fastq@^1.6.0: version "1.15.0" - resolved "https://registry.npmjs.org/fastq/-/fastq-1.15.0.tgz" + resolved "https://registry.yarnpkg.com/fastq/-/fastq-1.15.0.tgz#d04d07c6a2a68fe4599fea8d2e103a937fae6b3a" + integrity sha512-wBrocU2LCXXa+lWBt8RoIRD89Fi8OdABODa/kEnyeyjS5aZO5/GNvI5sEINADqP/h8M29UHTHUb53sUu5Ihqdw== dependencies: reusify "^1.0.4" fb-watchman@^2.0.0: version "2.0.2" - resolved "https://registry.npmjs.org/fb-watchman/-/fb-watchman-2.0.2.tgz" + resolved "https://registry.yarnpkg.com/fb-watchman/-/fb-watchman-2.0.2.tgz#e9524ee6b5c77e9e5001af0f85f3adbb8623255c" + integrity sha512-p5161BqbuCaSnB8jIbzQHOlpgsPmK5rJVDfDKO91Axs5NC1uu3HRQm6wt9cd9/+GtQQIO53JdGXXoyDpTAsgYA== dependencies: bser "2.1.1" fbjs-css-vars@^1.0.0: version "1.0.2" - resolved "https://registry.npmjs.org/fbjs-css-vars/-/fbjs-css-vars-1.0.2.tgz" + resolved "https://registry.yarnpkg.com/fbjs-css-vars/-/fbjs-css-vars-1.0.2.tgz#216551136ae02fe255932c3ec8775f18e2c078b8" + integrity sha512-b2XGFAFdWZWg0phtAWLHCk836A1Xann+I+Dgd3Gk64MHKZO44FfoD1KxyvbSh0qZsIoXQGGlVztIY+oitJPpRQ== fbjs@^3.0.0: - version "3.0.4" - resolved "https://registry.npmjs.org/fbjs/-/fbjs-3.0.4.tgz" + version "3.0.5" + resolved "https://registry.yarnpkg.com/fbjs/-/fbjs-3.0.5.tgz#aa0edb7d5caa6340011790bd9249dbef8a81128d" + integrity sha512-ztsSx77JBtkuMrEypfhgc3cI0+0h+svqeie7xHbh1k/IKdcydnvadp/mUaGgjAOXQmQSxsqgaRhS3q9fy+1kxg== dependencies: cross-fetch "^3.1.5" fbjs-css-vars "^1.0.0" @@ -3194,11 +3547,17 @@ fbjs@^3.0.0: object-assign "^4.1.0" promise "^7.1.1" setimmediate "^1.0.5" - ua-parser-js "^0.7.30" + ua-parser-js "^1.0.35" + +fflate@^0.4.1, fflate@^0.4.8: + version "0.4.8" + resolved "https://registry.yarnpkg.com/fflate/-/fflate-0.4.8.tgz#f90b82aefbd8ac174213abb338bd7ef848f0f5ae" + integrity sha512-FJqqoDBR00Mdj9ppamLa/Y7vxm+PRmNWA67N846RvsoYVMKB4q3y/de5PA7gUmRMYK/8CMz2GDZQmCRN1wBcWA== figures@^3.0.0: version "3.2.0" - resolved "https://registry.npmjs.org/figures/-/figures-3.2.0.tgz" + resolved "https://registry.yarnpkg.com/figures/-/figures-3.2.0.tgz#625c18bd293c604dc4a8ddb2febf0c88341746af" + integrity sha512-yaduQFRKLXYOGgEn6AZau90j3ggSOyiqXU0F9JZfeXYhNa+Jk4X+s45A2zg5jns87GAFa34BBm2kXw4XpNcbdg== dependencies: escape-string-regexp "^1.0.5" @@ -3210,13 +3569,15 @@ file-entry-cache@^6.0.1: fill-range@^7.0.1: version "7.0.1" - resolved "https://registry.npmjs.org/fill-range/-/fill-range-7.0.1.tgz" + resolved "https://registry.yarnpkg.com/fill-range/-/fill-range-7.0.1.tgz#1919a6a7c75fe38b2c7c77e5198535da9acdda40" + integrity sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ== dependencies: to-regex-range "^5.0.1" find-up@^4.1.0: version "4.1.0" - resolved "https://registry.npmjs.org/find-up/-/find-up-4.1.0.tgz" + resolved "https://registry.yarnpkg.com/find-up/-/find-up-4.1.0.tgz#97afe7d6cdc0bc5928584b7c8d7b16e8a9aa5d19" + integrity sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw== dependencies: locate-path "^5.0.0" path-exists "^4.0.0" @@ -3315,7 +3676,8 @@ fs-minipass@^3.0.0, fs-minipass@^3.0.1: fs.realpath@^1.0.0: version "1.0.0" - resolved "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz" + resolved "https://registry.yarnpkg.com/fs.realpath/-/fs.realpath-1.0.0.tgz#1504ad2523158caa40db4a2787cb01411994ea4f" + integrity sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw== fsevents@~2.3.2: version "2.3.2" @@ -3370,11 +3732,13 @@ gauge@^5.0.0: gensync@^1.0.0-beta.2: version "1.0.0-beta.2" - resolved "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz" + resolved "https://registry.yarnpkg.com/gensync/-/gensync-1.0.0-beta.2.tgz#32a6ee76c3d7f52d46b2b1ae5d93fea8580a25e0" + integrity sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg== get-caller-file@^2.0.1, get-caller-file@^2.0.5: version "2.0.5" - resolved "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz" + resolved "https://registry.yarnpkg.com/get-caller-file/-/get-caller-file-2.0.5.tgz#4f94412a82db32f36e3b0b9741f8a97feb031f7e" + integrity sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg== get-intrinsic@^1.0.2, get-intrinsic@^1.1.1, get-intrinsic@^1.1.3, get-intrinsic@^1.2.0: version "1.2.0" @@ -3397,7 +3761,8 @@ get-tsconfig@^4.2.0: glob-parent@^5.1.2, glob-parent@~5.1.2: version "5.1.2" - resolved "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz" + resolved "https://registry.yarnpkg.com/glob-parent/-/glob-parent-5.1.2.tgz#869832c58034fe68a4093c17dc15e8340d8401c4" + integrity sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow== dependencies: is-glob "^4.0.1" @@ -3418,7 +3783,7 @@ glob@7.1.6: once "^1.3.0" path-is-absolute "^1.0.0" -glob@7.1.7, glob@^7.1.1, glob@^7.1.3: +glob@7.1.7, glob@^7.1.3: version "7.1.7" resolved "https://registry.npmjs.org/glob/-/glob-7.1.7.tgz" dependencies: @@ -3429,7 +3794,7 @@ glob@7.1.7, glob@^7.1.1, glob@^7.1.3: once "^1.3.0" path-is-absolute "^1.0.0" -glob@^7.1.4, glob@~7.2.3: +glob@^7.1.1, glob@^7.1.4, glob@~7.2.3: version "7.2.3" resolved "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz" dependencies: @@ -3461,7 +3826,8 @@ glob@^9.3.0, glob@^9.3.1, glob@^9.3.2: globals@^11.1.0: version "11.12.0" - resolved "https://registry.npmjs.org/globals/-/globals-11.12.0.tgz" + resolved "https://registry.yarnpkg.com/globals/-/globals-11.12.0.tgz#ab8795338868a0babd8525758018c2a7eb95c42e" + integrity sha512-WOBp/EEGUiIsJSp7wcv/y6MO+lV9UoncWqxuFfm8eBwzWNgyfBd6Gz+IeKQ9jCmyhoH99g15M3T+QaVHFjizVA== globals@^13.19.0: version "13.20.0" @@ -3524,7 +3890,8 @@ grapheme-splitter@^1.0.4: graphql-config@^4.5.0: version "4.5.0" - resolved "https://registry.npmjs.org/graphql-config/-/graphql-config-4.5.0.tgz" + resolved "https://registry.yarnpkg.com/graphql-config/-/graphql-config-4.5.0.tgz#257c2338950b8dce295a27f75c5f6c39f8f777b2" + integrity sha512-x6D0/cftpLUJ0Ch1e5sj1TZn6Wcxx4oMfmhaG9shM0DKajA9iR+j1z86GSTQ19fShbGvrSSvbIQsHku6aQ6BBw== dependencies: "@graphql-tools/graphql-file-loader" "^7.3.7" "@graphql-tools/json-file-loader" "^7.3.7" @@ -3539,8 +3906,9 @@ graphql-config@^4.5.0: tslib "^2.4.0" graphql-request@^6.0.0: - version "6.0.0" - resolved "https://registry.npmjs.org/graphql-request/-/graphql-request-6.0.0.tgz" + version "6.1.0" + resolved "https://registry.yarnpkg.com/graphql-request/-/graphql-request-6.1.0.tgz#f4eb2107967af3c7a5907eb3131c671eac89be4f" + integrity sha512-p+XPfS4q7aIpKVcgmnZKhMNqhltk20hfXtkaIkTfjjmiKMJ5xrt5c743cL03y/K7y1rg3WrIC49xGiEQ4mxdNw== dependencies: "@graphql-typed-document-node/core" "^3.2.0" cross-fetch "^3.1.5" @@ -3553,11 +3921,13 @@ graphql-tag@^2.11.0, graphql-tag@^2.12.6: graphql-ws@5.12.1: version "5.12.1" - resolved "https://registry.npmjs.org/graphql-ws/-/graphql-ws-5.12.1.tgz" + resolved "https://registry.yarnpkg.com/graphql-ws/-/graphql-ws-5.12.1.tgz#c62d5ac54dbd409cc6520b0b39de374b3d59d0dd" + integrity sha512-umt4f5NnMK46ChM2coO36PTFhHouBrK9stWWBczERguwYrGnPNxJ9dimU6IyOBfOkC6Izhkg4H8+F51W/8CYDg== -graphql@^16.6.0: - version "16.6.0" - resolved "https://registry.npmjs.org/graphql/-/graphql-16.6.0.tgz" +graphql@^16.8.1: + version "16.8.1" + resolved "https://registry.yarnpkg.com/graphql/-/graphql-16.8.1.tgz#1930a965bef1170603702acdb68aedd3f3cf6f07" + integrity sha512-59LZHPdGZVh695Ud9lRzPBVTtlX9ZCV150Er2W43ro37wVof0ctenSaskPPjN7lVTIN8mSZt8PHUNKZuNQUuxw== has-ansi@^2.0.0: version "2.0.0" @@ -3571,11 +3941,13 @@ has-bigints@^1.0.1, has-bigints@^1.0.2: has-flag@^3.0.0: version "3.0.0" - resolved "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz" + resolved "https://registry.yarnpkg.com/has-flag/-/has-flag-3.0.0.tgz#b5d454dc2199ae225699f3467e5a07f3b955bafd" + integrity sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw== has-flag@^4.0.0: version "4.0.0" - resolved "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz" + resolved "https://registry.yarnpkg.com/has-flag/-/has-flag-4.0.0.tgz#944771fd9c81c81265c4d6941860da06bb59479b" + integrity sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ== has-property-descriptors@^1.0.0: version "1.0.0" @@ -3609,7 +3981,8 @@ has@^1.0.3, has@~1.0.3: header-case@^2.0.4: version "2.0.4" - resolved "https://registry.npmjs.org/header-case/-/header-case-2.0.4.tgz" + resolved "https://registry.yarnpkg.com/header-case/-/header-case-2.0.4.tgz#5a42e63b55177349cf405beb8d775acabb92c063" + integrity sha512-H/vuk5TEEVZwrR0lp2zed9OCo1uAILMlx0JEMgC26rzyJJ3N1v6XkwHHXJQdR2doSjcGPM6OKPYoJgf0plJ11Q== dependencies: capital-case "^1.0.4" tslib "^2.0.3" @@ -3634,25 +4007,51 @@ hosted-git-info@^6.0.0, hosted-git-info@^6.1.1: dependencies: lru-cache "^7.5.1" +html2canvas@^1.0.0-rc.5: + version "1.4.1" + resolved "https://registry.yarnpkg.com/html2canvas/-/html2canvas-1.4.1.tgz#7cef1888311b5011d507794a066041b14669a543" + integrity sha512-fPU6BHNpsyIhr8yyMpTLLxAbkaK8ArIBcmZIRiBLiDhjeqvXolaEmDGmELFuX9I4xDcaKKcJl+TKZLqruBbmWA== + dependencies: + css-line-break "^2.1.0" + text-segmentation "^1.0.3" + http-cache-semantics@^4.1.0, http-cache-semantics@^4.1.1: version "4.1.1" resolved "https://registry.npmjs.org/http-cache-semantics/-/http-cache-semantics-4.1.1.tgz" http-proxy-agent@^5.0.0: version "5.0.0" - resolved "https://registry.npmjs.org/http-proxy-agent/-/http-proxy-agent-5.0.0.tgz" + resolved "https://registry.yarnpkg.com/http-proxy-agent/-/http-proxy-agent-5.0.0.tgz#5129800203520d434f142bc78ff3c170800f2b43" + integrity sha512-n2hY8YdoRE1i7r6M0w9DIw5GgZN0G25P8zLCRQ8rjXtTU3vsNFBI/vWK/UIeE6g5MUUz6avwAPXmL6Fy9D/90w== dependencies: "@tootallnate/once" "2" agent-base "6" debug "4" +http-proxy-agent@^6.0.0: + version "6.1.1" + resolved "https://registry.yarnpkg.com/http-proxy-agent/-/http-proxy-agent-6.1.1.tgz#dc04f1a84e09511740cfbd984a56f86cc42e4277" + integrity sha512-JRCz+4Whs6yrrIoIlrH+ZTmhrRwtMnmOHsHn8GFEn9O2sVfSE+DAZ3oyyGIKF8tjJEeSJmP89j7aTjVsSqsU0g== + dependencies: + agent-base "^7.1.0" + debug "^4.3.4" + https-proxy-agent@^5.0.0: version "5.0.1" - resolved "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-5.0.1.tgz" + resolved "https://registry.yarnpkg.com/https-proxy-agent/-/https-proxy-agent-5.0.1.tgz#c59ef224a04fe8b754f3db0063a25ea30d0005d6" + integrity sha512-dFcAjpTQFgoLMzC2VwU+C/CbS7uRL0lWmxDITmqm7C+7F0Odmj6s9l6alZc6AELXhrnggM2CeWSXHGOdX2YtwA== dependencies: agent-base "6" debug "4" +https-proxy-agent@^6.0.0: + version "6.2.1" + resolved "https://registry.yarnpkg.com/https-proxy-agent/-/https-proxy-agent-6.2.1.tgz#0965ab47371b3e531cf6794d1eb148710a992ba7" + integrity sha512-ONsE3+yfZF2caH5+bJlcddtWqNI3Gvs5A38+ngvljxaBiRXRswym2c7yf8UAeFpRFKjFNHIFEHqR/OLAWJzyiA== + dependencies: + agent-base "^7.0.2" + debug "4" + human-format@^0.11.0: version "0.11.0" resolved "https://registry.npmjs.org/human-format/-/human-format-0.11.0.tgz" @@ -3669,7 +4068,8 @@ humanize-ms@^1.2.1: iconv-lite@^0.4.24: version "0.4.24" - resolved "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz" + resolved "https://registry.yarnpkg.com/iconv-lite/-/iconv-lite-0.4.24.tgz#2022b4b25fbddc21d2f524974a474aafe733908b" + integrity sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA== dependencies: safer-buffer ">= 2.1.2 < 3" @@ -3691,7 +4091,8 @@ ignore-walk@^6.0.0: ignore@^5.2.0: version "5.2.4" - resolved "https://registry.npmjs.org/ignore/-/ignore-5.2.4.tgz" + resolved "https://registry.yarnpkg.com/ignore/-/ignore-5.2.4.tgz#a291c0c6178ff1b960befe47fcdec301674a6324" + integrity sha512-MAb38BcSbH0eHNBxn7ql2NH/kX33OkB3lZ1BNdh7ENeRChHTYsTvWrMubiIAMNS2llXEEgZ1MUOBtXChP3kaFQ== immutable@^4.0.0: version "4.3.0" @@ -3699,7 +4100,8 @@ immutable@^4.0.0: immutable@~3.7.6: version "3.7.6" - resolved "https://registry.npmjs.org/immutable/-/immutable-3.7.6.tgz" + resolved "https://registry.yarnpkg.com/immutable/-/immutable-3.7.6.tgz#13b4d3cb12befa15482a26fe1b2ebae640071e4b" + integrity sha512-AizQPcaofEtO11RZhPPHBOJRdo/20MKQF9mBLnVkBoyHi1/zXK8fzVdnEpSV9gxqtnh6Qomfp3F0xT5qP/vThw== import-fresh@^3.0.0, import-fresh@^3.2.1: version "3.3.0" @@ -3710,7 +4112,8 @@ import-fresh@^3.0.0, import-fresh@^3.2.1: import-from@4.0.0: version "4.0.0" - resolved "https://registry.npmjs.org/import-from/-/import-from-4.0.0.tgz" + resolved "https://registry.yarnpkg.com/import-from/-/import-from-4.0.0.tgz#2710b8d66817d232e16f4166e319248d3d5492e2" + integrity sha512-P9J71vT5nLlDeV8FHs5nNxaLbrpfAV5cF5srvbZfpwpcJoM/xZR3hiv+q+SAnuSmuGbXMWud063iIMx/V/EWZQ== imurmurhash@^0.1.4: version "0.1.4" @@ -3718,7 +4121,8 @@ imurmurhash@^0.1.4: indent-string@^4.0.0: version "4.0.0" - resolved "https://registry.npmjs.org/indent-string/-/indent-string-4.0.0.tgz" + resolved "https://registry.yarnpkg.com/indent-string/-/indent-string-4.0.0.tgz#624f8f4497d619b2d9768531d58f4122854d7251" + integrity sha512-EdDDZu4A2OyIK7Lr/2zG+w5jmbuk1DVBnEwREQvBzspBJkCEbRa8GxU1lghYcaGJCnRWibjDXlq779X1/y5xwg== infer-owner@^1.0.4: version "1.0.4" @@ -3726,14 +4130,16 @@ infer-owner@^1.0.4: inflight@^1.0.4: version "1.0.6" - resolved "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz" + resolved "https://registry.yarnpkg.com/inflight/-/inflight-1.0.6.tgz#49bd6331d7d02d0c09bc910a1075ba8165b56df9" + integrity sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA== dependencies: once "^1.3.0" wrappy "1" inherits@2, inherits@^2.0.3, inherits@^2.0.4, inherits@~2.0.4: version "2.0.4" - resolved "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz" + resolved "https://registry.yarnpkg.com/inherits/-/inherits-2.0.4.tgz#0fa2c64f932917c3433a0ded55363aae37416b7c" + integrity sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ== ini@^4.1.0: version "4.1.0" @@ -3752,8 +4158,9 @@ init-package-json@^5.0.0: validate-npm-package-name "^5.0.0" inquirer@^8.0.0: - version "8.2.5" - resolved "https://registry.npmjs.org/inquirer/-/inquirer-8.2.5.tgz" + version "8.2.6" + resolved "https://registry.yarnpkg.com/inquirer/-/inquirer-8.2.6.tgz#733b74888195d8d400a67ac332011b5fae5ea562" + integrity sha512-M1WuAmb7pn9zdFRtQYk26ZBoY043Sse0wVDdk4Bppr+JOXyQYybdtvK+l9wUibhtjdjvtoiNy8tk+EgsYIUqKg== dependencies: ansi-escapes "^4.2.1" chalk "^4.1.1" @@ -3769,7 +4176,7 @@ inquirer@^8.0.0: string-width "^4.1.0" strip-ansi "^6.0.0" through "^2.3.6" - wrap-ansi "^7.0.0" + wrap-ansi "^6.0.1" install@^0.13.0: version "0.13.0" @@ -3793,7 +4200,8 @@ internmap@^1.0.0: invariant@^2.2.4: version "2.2.4" - resolved "https://registry.npmjs.org/invariant/-/invariant-2.2.4.tgz" + resolved "https://registry.yarnpkg.com/invariant/-/invariant-2.2.4.tgz#610f3c92c9359ce1db616e538008d23ff35158e6" + integrity sha512-phJfQVBuaJM5raOpJjSfkiD6BpbCE4Ns//LaXl6wGYtUBY83nWS6Rf9tXm2e8VaK60JEjYldbPif/A2B1C2gNA== dependencies: loose-envify "^1.0.0" @@ -3811,7 +4219,8 @@ ip@^2.0.0: is-absolute@^1.0.0: version "1.0.0" - resolved "https://registry.npmjs.org/is-absolute/-/is-absolute-1.0.0.tgz" + resolved "https://registry.yarnpkg.com/is-absolute/-/is-absolute-1.0.0.tgz#395e1ae84b11f26ad1795e73c17378e48a301576" + integrity sha512-dOWoqflvcydARa360Gvv18DZ/gRuHKi2NU/wU5X1ZFzdYfH29nkiNZsF3mp4OJ3H4yo9Mx8A/uAGNzpzPN3yBA== dependencies: is-relative "^1.0.0" is-windows "^1.0.1" @@ -3833,7 +4242,8 @@ is-array-buffer@^3.0.1: is-arrayish@^0.2.1: version "0.2.1" - resolved "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.2.1.tgz" + resolved "https://registry.yarnpkg.com/is-arrayish/-/is-arrayish-0.2.1.tgz#77c99840527aa8ecb1a8ba697b80645a7a926a9d" + integrity sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg== is-bigint@^1.0.1: version "1.0.4" @@ -3886,11 +4296,13 @@ is-docker@^2.0.0, is-docker@^2.1.1: is-extglob@^2.1.1: version "2.1.1" - resolved "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz" + resolved "https://registry.yarnpkg.com/is-extglob/-/is-extglob-2.1.1.tgz#a88c02535791f02ed37c76a1b9ea9773c833f8c2" + integrity sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ== is-fullwidth-code-point@^3.0.0: version "3.0.0" - resolved "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz" + resolved "https://registry.yarnpkg.com/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz#f116f8064fe90b3f7844a38997c0b75051269f1d" + integrity sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg== is-glob@4.0.3, is-glob@^4.0.0, is-glob@^4.0.1, is-glob@^4.0.3, is-glob@~4.0.1: version "4.0.3" @@ -3900,7 +4312,8 @@ is-glob@4.0.3, is-glob@^4.0.0, is-glob@^4.0.1, is-glob@^4.0.3, is-glob@~4.0.1: is-interactive@^1.0.0: version "1.0.0" - resolved "https://registry.npmjs.org/is-interactive/-/is-interactive-1.0.0.tgz" + resolved "https://registry.yarnpkg.com/is-interactive/-/is-interactive-1.0.0.tgz#cea6e6ae5c870a7b0a0004070b7b587e0252912e" + integrity sha512-2HvIEKRoqS62guEC+qBjpvRubdX910WCMuJTZ+I9yvqKU2/12eSL549HMwtabb4oupdj2sMP50k+XJfB/8JE6w== is-lambda@^1.0.1: version "1.0.1" @@ -3908,7 +4321,8 @@ is-lambda@^1.0.1: is-lower-case@^2.0.2: version "2.0.2" - resolved "https://registry.npmjs.org/is-lower-case/-/is-lower-case-2.0.2.tgz" + resolved "https://registry.yarnpkg.com/is-lower-case/-/is-lower-case-2.0.2.tgz#1c0884d3012c841556243483aa5d522f47396d2a" + integrity sha512-bVcMJy4X5Og6VZfdOZstSexlEy20Sr0k/p/b2IlQJlfdKAQuMpiv5w2Ccxb8sKdRUNAG1PnHVHjFSdRDVS6NlQ== dependencies: tslib "^2.0.3" @@ -3928,7 +4342,8 @@ is-number-object@^1.0.4: is-number@^7.0.0: version "7.0.0" - resolved "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz" + resolved "https://registry.yarnpkg.com/is-number/-/is-number-7.0.0.tgz#7535345b896734d5f80c4d06c50955527a14f12b" + integrity sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng== is-path-inside@^3.0.3: version "3.0.3" @@ -3943,7 +4358,8 @@ is-regex@^1.0.4, is-regex@^1.1.4, is-regex@~1.1.4: is-relative@^1.0.0: version "1.0.0" - resolved "https://registry.npmjs.org/is-relative/-/is-relative-1.0.0.tgz" + resolved "https://registry.yarnpkg.com/is-relative/-/is-relative-1.0.0.tgz#a1bb6935ce8c5dba1e8b9754b9b2dcc020e2260d" + integrity sha512-Kw/ReK0iqwKeu0MITLFuj0jbPAmEiOsIwyIXvvbfa6QfmN9pkD1M+8pdk7Rl/dTKbH34/XBFMbgD4iMJhLQbGA== dependencies: is-unc-path "^1.0.0" @@ -3981,17 +4397,20 @@ is-typed-array@^1.1.10, is-typed-array@^1.1.9: is-unc-path@^1.0.0: version "1.0.0" - resolved "https://registry.npmjs.org/is-unc-path/-/is-unc-path-1.0.0.tgz" + resolved "https://registry.yarnpkg.com/is-unc-path/-/is-unc-path-1.0.0.tgz#d731e8898ed090a12c352ad2eaed5095ad322c9d" + integrity sha512-mrGpVd0fs7WWLfVsStvgF6iEJnbjDFZh9/emhRDcGWTduTfNHd9CHeUwH3gYIjdbwo4On6hunkztwOaAw0yllQ== dependencies: unc-path-regex "^0.1.2" is-unicode-supported@^0.1.0: version "0.1.0" - resolved "https://registry.npmjs.org/is-unicode-supported/-/is-unicode-supported-0.1.0.tgz" + resolved "https://registry.yarnpkg.com/is-unicode-supported/-/is-unicode-supported-0.1.0.tgz#3f26c76a809593b52bfa2ecb5710ed2779b522a7" + integrity sha512-knxG2q4UC3u8stRGyAVJCOdxFmv5DZiRcdlIaAQXAbSfJya+OhopNotLQrstBhququ4ZpuKbDc/8S6mgXgPFPw== is-upper-case@^2.0.2: version "2.0.2" - resolved "https://registry.npmjs.org/is-upper-case/-/is-upper-case-2.0.2.tgz" + resolved "https://registry.yarnpkg.com/is-upper-case/-/is-upper-case-2.0.2.tgz#f1105ced1fe4de906a5f39553e7d3803fd804649" + integrity sha512-44pxmxAvnnAOwBg4tHPnkfvgjPwbc5QIsSstNU+YcJ1ovxVzCWpSGosPJOZh/a1tdl81fbgnLc9LLv+x2ywbPQ== dependencies: tslib "^2.0.3" @@ -4014,7 +4433,8 @@ is-weakset@^2.0.1: is-windows@^1.0.1: version "1.0.2" - resolved "https://registry.npmjs.org/is-windows/-/is-windows-1.0.2.tgz" + resolved "https://registry.yarnpkg.com/is-windows/-/is-windows-1.0.2.tgz#d1850eb9791ecd18e6182ce12a30f396634bb19d" + integrity sha512-eXK1UInq2bPmjyX6e3VHIzMLobc4J94i4AWn+Hpq3OU5KkrRC96OAcR3PRJ/pGu6m8TRnBHP9dkXQVsT/COVIA== is-wsl@^2.2.0: version "2.2.0" @@ -4032,17 +4452,29 @@ isexe@^2.0.0: isomorphic-ws@5.0.0, isomorphic-ws@^5.0.0: version "5.0.0" - resolved "https://registry.npmjs.org/isomorphic-ws/-/isomorphic-ws-5.0.0.tgz" + resolved "https://registry.yarnpkg.com/isomorphic-ws/-/isomorphic-ws-5.0.0.tgz#e5529148912ecb9b451b46ed44d53dae1ce04bbf" + integrity sha512-muId7Zzn9ywDsyXgTIafTry2sV3nySZeUDe6YedVd1Hvuuep5AsIlqK+XefWpYTyJG5e503F2xIuT2lcU6rCSw== jiti@1.17.1: version "1.17.1" - resolved "https://registry.npmjs.org/jiti/-/jiti-1.17.1.tgz" + resolved "https://registry.yarnpkg.com/jiti/-/jiti-1.17.1.tgz#264daa43ee89a03e8be28c3d712ccc4eb9f1e8ed" + integrity sha512-NZIITw8uZQFuzQimqjUxIrIcEdxYDFIe/0xYfIlVXTkiBjjyBEvgasj5bb0/cHtPRD/NziPbT312sFrkI5ALpw== -jiti@^1.17.1, jiti@^1.17.2: +jiti@^1.17.1: + version "1.19.3" + resolved "https://registry.yarnpkg.com/jiti/-/jiti-1.19.3.tgz#ef554f76465b3c2b222dc077834a71f0d4a37569" + integrity sha512-5eEbBDQT/jF1xg6l36P+mWGGoH9Spuy0PCdSr2dtWRDGC6ph/w9ZCL4lmESW8f8F7MwT3XKescfP0wnZWAKL9w== + +jiti@^1.17.2: version "1.18.2" resolved "https://registry.npmjs.org/jiti/-/jiti-1.18.2.tgz" -jose@^4.11.4, jose@^4.14.1: +jose@^4.11.4: + version "4.14.4" + resolved "https://registry.yarnpkg.com/jose/-/jose-4.14.4.tgz#59e09204e2670c3164ee24cbfe7115c6f8bff9ca" + integrity sha512-j8GhLiKmUAh+dsFXlX1aJCbt5KMibuKb+d7j1JaOJG6s2UjX1PQlW+OKB/sD4a/5ZYF4RcmYmLSndOoU3Lt/3g== + +jose@^4.14.1: version "4.14.1" resolved "https://registry.npmjs.org/jose/-/jose-4.14.1.tgz" @@ -4052,21 +4484,25 @@ js-sdsl@^4.1.4: "js-tokens@^3.0.0 || ^4.0.0", js-tokens@^4.0.0: version "4.0.0" - resolved "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz" + resolved "https://registry.yarnpkg.com/js-tokens/-/js-tokens-4.0.0.tgz#19203fb59991df98e3a287050d4647cdeaf32499" + integrity sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ== js-yaml@^4.0.0, js-yaml@^4.1.0: version "4.1.0" - resolved "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz" + resolved "https://registry.yarnpkg.com/js-yaml/-/js-yaml-4.1.0.tgz#c1fb65f8f5017901cdd2c951864ba18458a10602" + integrity sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA== dependencies: argparse "^2.0.1" jsesc@^2.5.1: version "2.5.2" - resolved "https://registry.npmjs.org/jsesc/-/jsesc-2.5.2.tgz" + resolved "https://registry.yarnpkg.com/jsesc/-/jsesc-2.5.2.tgz#80564d2e483dacf6e8ef209650a67df3f0c283a4" + integrity sha512-OYu7XEzjkCQ3C5Ps3QIZsQfNpqoJyZZA99wd9aWd05NCtC5pWOkShK2mkL6HXQR6/Cy2lbNdPlZBpuQHXE63gA== json-parse-even-better-errors@^2.3.0: version "2.3.1" - resolved "https://registry.npmjs.org/json-parse-even-better-errors/-/json-parse-even-better-errors-2.3.1.tgz" + resolved "https://registry.yarnpkg.com/json-parse-even-better-errors/-/json-parse-even-better-errors-2.3.1.tgz#7c47805a94319928e05777405dc12e1f7a4ee02d" + integrity sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w== json-parse-even-better-errors@^3.0.0: version "3.0.0" @@ -4082,7 +4518,8 @@ json-stable-stringify-without-jsonify@^1.0.1: json-stable-stringify@^1.0.1: version "1.0.2" - resolved "https://registry.npmjs.org/json-stable-stringify/-/json-stable-stringify-1.0.2.tgz" + resolved "https://registry.yarnpkg.com/json-stable-stringify/-/json-stable-stringify-1.0.2.tgz#e06f23128e0bbe342dc996ed5a19e28b57b580e0" + integrity sha512-eunSSaEnxV12z+Z73y/j5N37/In40GK4GmsSy+tEHJMxknvqnA7/djeYtAgW0GsWHUfg+847WJjKaEylk2y09g== dependencies: jsonify "^0.0.1" @@ -4092,7 +4529,8 @@ json-stringify-nice@^1.1.4: json-to-pretty-yaml@^1.2.2: version "1.2.2" - resolved "https://registry.npmjs.org/json-to-pretty-yaml/-/json-to-pretty-yaml-1.2.2.tgz" + resolved "https://registry.yarnpkg.com/json-to-pretty-yaml/-/json-to-pretty-yaml-1.2.2.tgz#f4cd0bd0a5e8fe1df25aaf5ba118b099fd992d5b" + integrity sha512-rvm6hunfCcqegwYaG5T4yKJWxc9FXFgBVrcTZ4XfSVRwa5HA/Xs+vB/Eo9treYYHCeNM0nrSUr82V/M31Urc7A== dependencies: remedial "^1.0.7" remove-trailing-spaces "^1.0.6" @@ -4111,11 +4549,13 @@ json5@^1.0.1: json5@^2.2.2: version "2.2.3" - resolved "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz" + resolved "https://registry.yarnpkg.com/json5/-/json5-2.2.3.tgz#78cd6f1a19bdc12b73db5ad0c61efd66c1e29283" + integrity sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg== jsonify@^0.0.1: version "0.0.1" - resolved "https://registry.npmjs.org/jsonify/-/jsonify-0.0.1.tgz" + resolved "https://registry.yarnpkg.com/jsonify/-/jsonify-0.0.1.tgz#2aa3111dae3d34a0f151c63f3a45d995d9420978" + integrity sha512-2/Ki0GcmuqSrgFyelQq9M05y7PS0mEwuIzrf3f1fPqkVDVRvZrPZtVSMHxdgo8Aq0sxAOb/cr2aqqA3LeWHVPg== jsonparse@^1.3.1: version "1.3.1" @@ -4130,6 +4570,21 @@ jsonwebtoken@^9.0.0: ms "^2.1.1" semver "^7.3.8" +jspdf@^2.5.1: + version "2.5.1" + resolved "https://registry.yarnpkg.com/jspdf/-/jspdf-2.5.1.tgz#00c85250abf5447a05f3b32ab9935ab4a56592cc" + integrity sha512-hXObxz7ZqoyhxET78+XR34Xu2qFGrJJ2I2bE5w4SM8eFaFEkW2xcGRVUss360fYelwRSid/jT078kbNvmoW0QA== + dependencies: + "@babel/runtime" "^7.14.0" + atob "^2.1.2" + btoa "^1.2.1" + fflate "^0.4.8" + optionalDependencies: + canvg "^3.0.6" + core-js "^3.6.0" + dompurify "^2.2.0" + html2canvas "^1.0.0-rc.5" + "jsx-ast-utils@^2.4.1 || ^3.0.0", jsx-ast-utils@^3.3.3: version "3.3.3" resolved "https://registry.npmjs.org/jsx-ast-utils/-/jsx-ast-utils-3.3.3.tgz" @@ -4306,11 +4761,13 @@ lilconfig@^2.0.5, lilconfig@^2.0.6: lines-and-columns@^1.1.6: version "1.2.4" - resolved "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-1.2.4.tgz" + resolved "https://registry.yarnpkg.com/lines-and-columns/-/lines-and-columns-1.2.4.tgz#eca284f75d2965079309dc0ad9255abb2ebc1632" + integrity sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg== listr2@^4.0.5: version "4.0.5" - resolved "https://registry.npmjs.org/listr2/-/listr2-4.0.5.tgz" + resolved "https://registry.yarnpkg.com/listr2/-/listr2-4.0.5.tgz#9dcc50221583e8b4c71c43f9c7dfd0ef546b75d5" + integrity sha512-juGHV1doQdpNT3GSTs9IUN43QJb7KHdF9uqg7Vufs/tG9VTzpFphqF4pm/ICdAABGQxsyNn9CiYA3StkI6jpwA== dependencies: cli-truncate "^2.1.0" colorette "^2.0.16" @@ -4323,7 +4780,8 @@ listr2@^4.0.5: locate-path@^5.0.0: version "5.0.0" - resolved "https://registry.npmjs.org/locate-path/-/locate-path-5.0.0.tgz" + resolved "https://registry.yarnpkg.com/locate-path/-/locate-path-5.0.0.tgz#1afba396afd676a6d42504d0a67a3a7eb9f62aa0" + integrity sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g== dependencies: p-locate "^4.1.0" @@ -4347,22 +4805,26 @@ lodash.merge@^4.6.2: lodash.sortby@^4.7.0: version "4.7.0" - resolved "https://registry.npmjs.org/lodash.sortby/-/lodash.sortby-4.7.0.tgz" + resolved "https://registry.yarnpkg.com/lodash.sortby/-/lodash.sortby-4.7.0.tgz#edd14c824e2cc9c1e0b0a1b42bb5210516a42438" + integrity sha512-HDWXG8isMntAyRF5vZ7xKuEvOhT4AhlRt/3czTSjvGUxjYCBVRQY48ViDHyfYz9VIoBkW4TMGQNapx+l3RUwdA== lodash@^4.17.20, lodash@^4.17.21, lodash@~4.17.0: version "4.17.21" - resolved "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz" + resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.21.tgz#679591c564c3bffaae8454cf0b3df370c3d6911c" + integrity sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg== log-symbols@^4.0.0, log-symbols@^4.1.0: version "4.1.0" - resolved "https://registry.npmjs.org/log-symbols/-/log-symbols-4.1.0.tgz" + resolved "https://registry.yarnpkg.com/log-symbols/-/log-symbols-4.1.0.tgz#3fbdbb95b4683ac9fc785111e792e558d4abd503" + integrity sha512-8XPvpAA8uyhfteu8pIvQxpJZ7SYYdpUivZpGy6sFsBuKRY/7rQGavedeB8aK+Zkyq6upMFVL/9AW6vOYzfRyLg== dependencies: chalk "^4.1.0" is-unicode-supported "^0.1.0" log-update@^4.0.0: version "4.0.0" - resolved "https://registry.npmjs.org/log-update/-/log-update-4.0.0.tgz" + resolved "https://registry.yarnpkg.com/log-update/-/log-update-4.0.0.tgz#589ecd352471f2a1c0c570287543a64dfd20e0a1" + integrity sha512-9fkkDevMefjg0mmzWFBW8YkFP91OrizzkW3diF7CpG+S2EYdy4+TVfGwz1zeF8x7hCx1ovSPTOE9Ngib74qqUg== dependencies: ansi-escapes "^4.3.0" cli-cursor "^3.1.0" @@ -4381,19 +4843,22 @@ loose-envify@^1.0.0, loose-envify@^1.1.0, loose-envify@^1.4.0: lower-case-first@^2.0.2: version "2.0.2" - resolved "https://registry.npmjs.org/lower-case-first/-/lower-case-first-2.0.2.tgz" + resolved "https://registry.yarnpkg.com/lower-case-first/-/lower-case-first-2.0.2.tgz#64c2324a2250bf7c37c5901e76a5b5309301160b" + integrity sha512-EVm/rR94FJTZi3zefZ82fLWab+GX14LJN4HrWBcuo6Evmsl9hEfnqxgcHCKb9q+mNf6EVdsjx/qucYFIIB84pg== dependencies: tslib "^2.0.3" lower-case@^2.0.2: version "2.0.2" - resolved "https://registry.npmjs.org/lower-case/-/lower-case-2.0.2.tgz" + resolved "https://registry.yarnpkg.com/lower-case/-/lower-case-2.0.2.tgz#6fa237c63dbdc4a82ca0fd882e4722dc5e634e28" + integrity sha512-7fm3l3NAF9WfN6W3JOmf5drwpVqX78JtoGJ3A6W0a6ZnldM41w2fV5D490psKFTpMds8TJse/eHLFFsNHHjHgg== dependencies: tslib "^2.0.3" lru-cache@^5.1.1: version "5.1.1" - resolved "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz" + resolved "https://registry.yarnpkg.com/lru-cache/-/lru-cache-5.1.1.tgz#1da27e6710271947695daf6848e847f01d84b920" + integrity sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w== dependencies: yallist "^3.0.2" @@ -4474,7 +4939,8 @@ make-fetch-happen@^11.1.1: map-cache@^0.2.0: version "0.2.2" - resolved "https://registry.npmjs.org/map-cache/-/map-cache-0.2.2.tgz" + resolved "https://registry.yarnpkg.com/map-cache/-/map-cache-0.2.2.tgz#c32abd0bd6525d9b051645bb4f26ac5dc98a0dbf" + integrity sha512-8y/eV9QQZCiyn1SprXSrCmqJN0yNRATe+PO8ztwqrvrbdRLA3eYJF0yaR0YayLWkMbsQSKWS9N2gPcGEc4UsZg== memoize-bind@^1.0.3: version "1.0.3" @@ -4496,15 +4962,18 @@ memoize-weak@^1.0.0: merge2@^1.3.0, merge2@^1.4.1: version "1.4.1" - resolved "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz" + resolved "https://registry.yarnpkg.com/merge2/-/merge2-1.4.1.tgz#4368892f885e907455a6fd7dc55c0c9d404990ae" + integrity sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg== meros@^1.2.1: - version "1.2.1" - resolved "https://registry.npmjs.org/meros/-/meros-1.2.1.tgz" + version "1.3.0" + resolved "https://registry.yarnpkg.com/meros/-/meros-1.3.0.tgz#c617d2092739d55286bf618129280f362e6242f2" + integrity sha512-2BNGOimxEz5hmjUG2FwoxCt5HN7BXdaWyFqEwxPTrJzVdABtrL4TiHTcsWSFAxPQ/tOnEaQEJh3qWq71QRMY+w== micromatch@^4.0.4, micromatch@^4.0.5: version "4.0.5" - resolved "https://registry.npmjs.org/micromatch/-/micromatch-4.0.5.tgz" + resolved "https://registry.yarnpkg.com/micromatch/-/micromatch-4.0.5.tgz#bc8999a7cbbf77cdc89f132f6e467051b49090c6" + integrity sha512-DMy+ERcEW2q8Z2Po+WNXuw3c5YaUSFjAO5GsJqfEl7UjvtIuFKO6ZrKvcItdy98dwFI2N1tg3zNIdKaQT+aNdA== dependencies: braces "^3.0.2" picomatch "^2.3.1" @@ -4521,11 +4990,13 @@ mime-types@^2.1.12: mimic-fn@^2.1.0: version "2.1.0" - resolved "https://registry.npmjs.org/mimic-fn/-/mimic-fn-2.1.0.tgz" + resolved "https://registry.yarnpkg.com/mimic-fn/-/mimic-fn-2.1.0.tgz#7ed2c2ccccaf84d3ffcb7a69b57711fc2083401b" + integrity sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg== minimatch@4.2.3: version "4.2.3" - resolved "https://registry.npmjs.org/minimatch/-/minimatch-4.2.3.tgz" + resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-4.2.3.tgz#b4dcece1d674dee104bb0fb833ebb85a78cbbca6" + integrity sha512-lIUdtK5hdofgCTu3aT0sOaHsYR37viUuIc0rwnnDXImbwFRcumyLMeZaM0t0I/fgxS6s6JMfu0rLD1Wz9pv1ng== dependencies: brace-expansion "^1.1.7" @@ -4645,7 +5116,8 @@ mkdirp@^1.0.3, mkdirp@^1.0.4: ms@2.1.2, ms@^2.1.1: version "2.1.2" - resolved "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz" + resolved "https://registry.yarnpkg.com/ms/-/ms-2.1.2.tgz#d09d1f357b443f493382a8eb3ccd183872ae6009" + integrity sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w== ms@^2.0.0, ms@^2.1.2: version "2.1.3" @@ -4653,7 +5125,8 @@ ms@^2.0.0, ms@^2.1.2: mute-stream@0.0.8: version "0.0.8" - resolved "https://registry.npmjs.org/mute-stream/-/mute-stream-0.0.8.tgz" + resolved "https://registry.yarnpkg.com/mute-stream/-/mute-stream-0.0.8.tgz#1630c42b2251ff81e2a283de96a5497ea92e5e0d" + integrity sha512-nnbWWOkoWyUsTjKrhgD0dcz22mdkSnpYqbEjIm2nhwhuxlSkpywJmBo8h0ZqJdkp73mb90SssHkN4rsRaBAfAA== mute-stream@~1.0.0: version "1.0.0" @@ -4723,14 +5196,16 @@ next@13.1.6: no-case@^3.0.4: version "3.0.4" - resolved "https://registry.npmjs.org/no-case/-/no-case-3.0.4.tgz" + resolved "https://registry.yarnpkg.com/no-case/-/no-case-3.0.4.tgz#d361fd5c9800f558551a8369fc0dcd4662b6124d" + integrity sha512-fgAN3jGAh+RoxUGZHTSOLJIqUc2wmoBwGR4tbpNAKmmovFoWq0OdRkb0VkldReO2a2iBT/OEulG9XSUc10r3zg== dependencies: lower-case "^2.0.2" tslib "^2.0.3" -node-addon-api@^3.2.1: - version "3.2.1" - resolved "https://registry.npmjs.org/node-addon-api/-/node-addon-api-3.2.1.tgz" +node-addon-api@^7.0.0: + version "7.0.0" + resolved "https://registry.yarnpkg.com/node-addon-api/-/node-addon-api-7.0.0.tgz#8136add2f510997b3b94814f4af1cce0b0e3962e" + integrity sha512-vgbBJTS4m5/KkE16t5Ly0WW9hz46swAstv0hYYwMtbG7AznRhNyfLRe8HZAiWIpcHzoO7HxhLuBQj9rJ/Ho0ZA== node-fetch@2.6.7: version "2.6.7" @@ -4739,15 +5214,12 @@ node-fetch@2.6.7: whatwg-url "^5.0.0" node-fetch@^2.6.1: - version "2.6.9" - resolved "https://registry.npmjs.org/node-fetch/-/node-fetch-2.6.9.tgz" + version "2.6.13" + resolved "https://registry.yarnpkg.com/node-fetch/-/node-fetch-2.6.13.tgz#a20acbbec73c2e09f9007de5cda17104122e0010" + integrity sha512-StxNAxh15zr77QvvkmveSQ8uCQ4+v5FkvNTj0OESmiHu+VRi/gXArXtkWMElOsOUNLtUEvI4yS+rdtOHZTwlQA== dependencies: whatwg-url "^5.0.0" -node-gyp-build@^4.3.0: - version "4.6.0" - resolved "https://registry.npmjs.org/node-gyp-build/-/node-gyp-build-4.6.0.tgz" - node-gyp@^9.0.0, node-gyp@^9.3.1: version "9.3.1" resolved "https://registry.npmjs.org/node-gyp/-/node-gyp-9.3.1.tgz" @@ -4765,11 +5237,13 @@ node-gyp@^9.0.0, node-gyp@^9.3.1: node-int64@^0.4.0: version "0.4.0" - resolved "https://registry.npmjs.org/node-int64/-/node-int64-0.4.0.tgz" + resolved "https://registry.yarnpkg.com/node-int64/-/node-int64-0.4.0.tgz#87a9065cdb355d3182d8f94ce11188b825c68a3b" + integrity sha512-O5lz91xSOeoXP6DulyHfllpq+Eg00MWitZIbtPfoSEvqIHdl5gfcY6hYzDWnj0qD5tz52PI08u9qUvSVeUBeHw== -node-releases@^2.0.8: - version "2.0.10" - resolved "https://registry.npmjs.org/node-releases/-/node-releases-2.0.10.tgz" +node-releases@^2.0.13, node-releases@^2.0.8: + version "2.0.13" + resolved "https://registry.yarnpkg.com/node-releases/-/node-releases-2.0.13.tgz#d5ed1627c23e3461e819b02e57b75e4899b1c81d" + integrity sha512-uYr7J37ae/ORWdZeQ1xxMJe3NtdmqMC/JZK+geofDrkLUApKRHPd18/TxtBOJ4A0/+uUIliorNrfYV6s1b02eQ== nopt@^6.0.0: version "6.0.0" @@ -4794,7 +5268,8 @@ normalize-package-data@^5.0.0: normalize-path@^2.1.1: version "2.1.1" - resolved "https://registry.npmjs.org/normalize-path/-/normalize-path-2.1.1.tgz" + resolved "https://registry.yarnpkg.com/normalize-path/-/normalize-path-2.1.1.tgz#1ab28b556e198363a8c1a6f7e6fa20137fe6aed9" + integrity sha512-3pKJwH184Xo/lnH6oyP1q2pMd7HcypqqmRs91/6/i2CGtWwIKGCkOOMTm/zXbgTEWHw1uNpNi/igc3ePOYHb6w== dependencies: remove-trailing-separator "^1.0.1" @@ -4966,7 +5441,8 @@ npmlog@^7.0.1: nullthrows@^1.1.1: version "1.1.1" - resolved "https://registry.npmjs.org/nullthrows/-/nullthrows-1.1.1.tgz" + resolved "https://registry.yarnpkg.com/nullthrows/-/nullthrows-1.1.1.tgz#7818258843856ae971eae4208ad7d7eb19a431b1" + integrity sha512-2vPPEi+Z7WqML2jZYddDIfy5Dqb0r2fze2zTxNNknZaFpVHU3mFB3R+DWeJWGVx0ecvttSGlJTI+WG+8Z4cDWw== oauth@^0.9.15: version "0.9.15" @@ -5045,13 +5521,15 @@ oidc-token-hash@^5.0.2: once@^1.3.0: version "1.4.0" - resolved "https://registry.npmjs.org/once/-/once-1.4.0.tgz" + resolved "https://registry.yarnpkg.com/once/-/once-1.4.0.tgz#583b1aa775961d4b113ac17d9c50baef9dd76bd1" + integrity sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w== dependencies: wrappy "1" onetime@^5.1.0: version "5.1.2" - resolved "https://registry.npmjs.org/onetime/-/onetime-5.1.2.tgz" + resolved "https://registry.yarnpkg.com/onetime/-/onetime-5.1.2.tgz#d0e96ebb56b07476df1dd9c4806e5237985ca45e" + integrity sha512-kbpaSSGJTWdAY5KPVeMOKXSrPtr8C8C7wodJbcsd51jRnmD+GZu8Y0VoU6Dm5Z4vWr0Ig/1NKuWRKf7j5aaYSg== dependencies: mimic-fn "^2.1.0" @@ -5092,7 +5570,8 @@ optionator@^0.9.1: ora@^5.4.1: version "5.4.1" - resolved "https://registry.npmjs.org/ora/-/ora-5.4.1.tgz" + resolved "https://registry.yarnpkg.com/ora/-/ora-5.4.1.tgz#1b2678426af4ac4a509008e5e4ac9e9959db9e18" + integrity sha512-5b6Y85tPxZZ7QytO+BQzysW31HJku27cRIlkbAXaNx+BdcVi+LlRFmVXzeF6a7JCwJpyw5c4b+YSVImQIrBpuQ== dependencies: bl "^4.1.0" chalk "^4.1.0" @@ -5106,7 +5585,8 @@ ora@^5.4.1: os-tmpdir@~1.0.2: version "1.0.2" - resolved "https://registry.npmjs.org/os-tmpdir/-/os-tmpdir-1.0.2.tgz" + resolved "https://registry.yarnpkg.com/os-tmpdir/-/os-tmpdir-1.0.2.tgz#bbe67406c79aa85c5cfec766fe5734555dfa1274" + integrity sha512-D2FR03Vir7FIu45XBY20mTb+/ZSWB00sjU9jdQXt83gDrI4Ztz5Fs7/yy74g2N5SVQY4xY1qDr4rNddwYRVX0g== p-limit@3.1.0, p-limit@^3.0.2: version "3.1.0" @@ -5116,13 +5596,15 @@ p-limit@3.1.0, p-limit@^3.0.2: p-limit@^2.2.0: version "2.3.0" - resolved "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz" + resolved "https://registry.yarnpkg.com/p-limit/-/p-limit-2.3.0.tgz#3dd33c647a214fdfffd835933eb086da0dc21db1" + integrity sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w== dependencies: p-try "^2.0.0" p-locate@^4.1.0: version "4.1.0" - resolved "https://registry.npmjs.org/p-locate/-/p-locate-4.1.0.tgz" + resolved "https://registry.yarnpkg.com/p-locate/-/p-locate-4.1.0.tgz#a3428bb7088b3a60292f66919278b7c297ad4f07" + integrity sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A== dependencies: p-limit "^2.2.0" @@ -5134,13 +5616,15 @@ p-locate@^5.0.0: p-map@^4.0.0: version "4.0.0" - resolved "https://registry.npmjs.org/p-map/-/p-map-4.0.0.tgz" + resolved "https://registry.yarnpkg.com/p-map/-/p-map-4.0.0.tgz#bb2f95a5eda2ec168ec9274e06a747c3e2904d2b" + integrity sha512-/bjOqmgETBYB5BoEeGVea8dmvHb2m9GLy1E9W43yeyfP6QQCZGFNa+XRceJEuDB6zqr+gKpIAmlLebMpykw/MQ== dependencies: aggregate-error "^3.0.0" p-try@^2.0.0: version "2.2.0" - resolved "https://registry.npmjs.org/p-try/-/p-try-2.2.0.tgz" + resolved "https://registry.yarnpkg.com/p-try/-/p-try-2.2.0.tgz#cb2868540e313d61de58fafbe35ce9004d5540e6" + integrity sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ== pacote@^15.0.0, pacote@^15.0.8, pacote@^15.1.1: version "15.1.1" @@ -5167,14 +5651,16 @@ pacote@^15.0.0, pacote@^15.0.8, pacote@^15.1.1: param-case@^3.0.4: version "3.0.4" - resolved "https://registry.npmjs.org/param-case/-/param-case-3.0.4.tgz" + resolved "https://registry.yarnpkg.com/param-case/-/param-case-3.0.4.tgz#7d17fe4aa12bde34d4a77d91acfb6219caad01c5" + integrity sha512-RXlj7zCYokReqWpOPH9oYivUzLYZ5vAPIfEmCTNViosC78F8F0H9y7T7gG2M39ymgutxF5gcFEsyZQSph9Bp3A== dependencies: dot-case "^3.0.4" tslib "^2.0.3" parent-module@^1.0.0: version "1.0.1" - resolved "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz" + resolved "https://registry.yarnpkg.com/parent-module/-/parent-module-1.0.1.tgz#691d2709e78c79fae3a156622452d00762caaaa2" + integrity sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g== dependencies: callsites "^3.0.0" @@ -5188,7 +5674,8 @@ parse-conflict-json@^3.0.0, parse-conflict-json@^3.0.1: parse-filepath@^1.0.2: version "1.0.2" - resolved "https://registry.npmjs.org/parse-filepath/-/parse-filepath-1.0.2.tgz" + resolved "https://registry.yarnpkg.com/parse-filepath/-/parse-filepath-1.0.2.tgz#a632127f53aaf3d15876f5872f3ffac763d6c891" + integrity sha512-FwdRXKCohSVeXqwtYonZTXtbGJKrn+HNyWDYVcp5yuJlesTwNH4rsmRZ+GrKAPJ5bLpRxESMeS+Rl0VCHRvB2Q== dependencies: is-absolute "^1.0.0" map-cache "^0.2.0" @@ -5196,7 +5683,8 @@ parse-filepath@^1.0.2: parse-json@^5.0.0: version "5.2.0" - resolved "https://registry.npmjs.org/parse-json/-/parse-json-5.2.0.tgz" + resolved "https://registry.yarnpkg.com/parse-json/-/parse-json-5.2.0.tgz#c76fc66dee54231c962b22bcc8a72cf2f99753cd" + integrity sha512-ayCKvm/phCGxOkYRSCM82iDwct8/EonSEgCSxWxD7ve6jHggsFl4fZVQBPRNgQoKiuV/odhFrGzQXZwbifC8Rg== dependencies: "@babel/code-frame" "^7.0.0" error-ex "^1.3.1" @@ -5205,25 +5693,29 @@ parse-json@^5.0.0: pascal-case@^3.1.2: version "3.1.2" - resolved "https://registry.npmjs.org/pascal-case/-/pascal-case-3.1.2.tgz" + resolved "https://registry.yarnpkg.com/pascal-case/-/pascal-case-3.1.2.tgz#b48e0ef2b98e205e7c1dae747d0b1508237660eb" + integrity sha512-uWlGT3YSnK9x3BQJaOdcZwrnV6hPpd8jFH1/ucpiLRPh/2zCVJKS19E4GvYHvaCcACn3foXZ0cLB9Wrx1KGe5g== dependencies: no-case "^3.0.4" tslib "^2.0.3" path-case@^3.0.4: version "3.0.4" - resolved "https://registry.npmjs.org/path-case/-/path-case-3.0.4.tgz" + resolved "https://registry.yarnpkg.com/path-case/-/path-case-3.0.4.tgz#9168645334eb942658375c56f80b4c0cb5f82c6f" + integrity sha512-qO4qCFjXqVTrcbPt/hQfhTQ+VhFsqNKOPtytgNKkKxSoEp3XPUQ8ObFuePylOIok5gjn69ry8XiULxCwot3Wfg== dependencies: dot-case "^3.0.4" tslib "^2.0.3" path-exists@^4.0.0: version "4.0.0" - resolved "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz" + resolved "https://registry.yarnpkg.com/path-exists/-/path-exists-4.0.0.tgz#513bdbe2d3b95d7762e8c1137efa195c6c61b5b3" + integrity sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w== path-is-absolute@^1.0.0: version "1.0.1" - resolved "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz" + resolved "https://registry.yarnpkg.com/path-is-absolute/-/path-is-absolute-1.0.1.tgz#174b9268735534ffbc7ace6bf53a5a9e1b5c5f5f" + integrity sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg== path-key@^3.1.0: version "3.1.1" @@ -5235,11 +5727,13 @@ path-parse@^1.0.7: path-root-regex@^0.1.0: version "0.1.2" - resolved "https://registry.npmjs.org/path-root-regex/-/path-root-regex-0.1.2.tgz" + resolved "https://registry.yarnpkg.com/path-root-regex/-/path-root-regex-0.1.2.tgz#bfccdc8df5b12dc52c8b43ec38d18d72c04ba96d" + integrity sha512-4GlJ6rZDhQZFE0DPVKh0e9jmZ5egZfxTkp7bcRDuPlJXbAwhxcl2dINPUAsjLdejqaLsCeg8axcLjIbvBjN4pQ== path-root@^0.1.1: version "0.1.1" - resolved "https://registry.npmjs.org/path-root/-/path-root-0.1.1.tgz" + resolved "https://registry.yarnpkg.com/path-root/-/path-root-0.1.1.tgz#9a4a6814cac1c0cd73360a95f32083c8ea4745b7" + integrity sha512-QLcPegTHF11axjfojBIoDygmS2E3Lf+8+jI6wOVmNVenrKSo3mFdSGiIgdSHenczw3wPtlVMQaFVwGmM7BJdtg== dependencies: path-root-regex "^0.1.0" @@ -5252,15 +5746,23 @@ path-scurry@^1.6.1: path-type@^4.0.0: version "4.0.0" - resolved "https://registry.npmjs.org/path-type/-/path-type-4.0.0.tgz" + resolved "https://registry.yarnpkg.com/path-type/-/path-type-4.0.0.tgz#84ed01c0a7ba380afe09d90a8c180dcd9d03043b" + integrity sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw== + +performance-now@^2.1.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/performance-now/-/performance-now-2.1.0.tgz#6309f4e0e5fa913ec1c69307ae364b4b377c9e7b" + integrity sha512-7EAHlyLHI56VEIdK57uwHdHKIaAGbnXPiw0yWbarQZOKaKpvUIgW0jWRVLiatnM+XXlSwsanIBH/hzGMJulMow== picocolors@^1.0.0: version "1.0.0" - resolved "https://registry.npmjs.org/picocolors/-/picocolors-1.0.0.tgz" + resolved "https://registry.yarnpkg.com/picocolors/-/picocolors-1.0.0.tgz#cb5bdc74ff3f51892236eaf79d68bc44564ab81c" + integrity sha512-1fygroTLlHu66zi26VoTDv8yRgm0Fccecssto+MhsZ0D/DGW2sm8E8AjW7NU5VVTRt5GxbeZ5qBuJr+HyLYkjQ== picomatch@^2.0.4, picomatch@^2.2.1, picomatch@^2.3.1: version "2.3.1" - resolved "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz" + resolved "https://registry.yarnpkg.com/picomatch/-/picomatch-2.3.1.tgz#3ba3833733646d9d3e4995946c1365a67fb07a42" + integrity sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA== pify@^2.3.0: version "2.3.0" @@ -5339,6 +5841,13 @@ postcss@^8.0.9, postcss@^8.4.31: picocolors "^1.0.0" source-map-js "^1.0.2" +posthog-js@^1.87.5: + version "1.87.5" + resolved "https://registry.yarnpkg.com/posthog-js/-/posthog-js-1.87.5.tgz#a279fc2016984008f2378a451bbbe33d9c89b857" + integrity sha512-GvSOX9oA1iPPaZSwFkuA333PDDo9yCKj4yqFYjxf/dU2mBIOuIMjdPLTiCvoVmsf2UL/2/9c7AwlnFAG4iRZuQ== + dependencies: + fflate "^0.4.1" + preact-render-to-string@^5.1.19: version "5.2.6" resolved "https://registry.npmjs.org/preact-render-to-string/-/preact-render-to-string-5.2.6.tgz" @@ -5396,7 +5905,8 @@ promise-retry@^2.0.1: promise@^7.1.1: version "7.3.1" - resolved "https://registry.npmjs.org/promise/-/promise-7.3.1.tgz" + resolved "https://registry.yarnpkg.com/promise/-/promise-7.3.1.tgz#064b72602b18f90f29192b8b1bc418ffd1ebd3bf" + integrity sha512-nolQXZ/4L+bP/UGlkfaIujX9BKxGwmQ9OT4mOt5yvy8iK1h3wqTEJCijzGANTCCl9nWjY41juyAn2K3Q1hLLTg== dependencies: asap "~2.0.3" @@ -5420,21 +5930,24 @@ proxy-from-env@^1.1.0: punycode@^1.3.2: version "1.4.1" - resolved "https://registry.npmjs.org/punycode/-/punycode-1.4.1.tgz" + resolved "https://registry.yarnpkg.com/punycode/-/punycode-1.4.1.tgz#c0d5a63b2718800ad8e1eb0fa5269c84dd41845e" + integrity sha512-jmYNElW7yvO7TV33CjSmvSiE2yco3bV2czu/OzDKdMNVZQWfxCblURLhf+47syQRBntjfLdd/H0egrzIG+oaFQ== punycode@^2.1.0: version "2.3.0" resolved "https://registry.npmjs.org/punycode/-/punycode-2.3.0.tgz" pvtsutils@^1.3.2: - version "1.3.2" - resolved "https://registry.npmjs.org/pvtsutils/-/pvtsutils-1.3.2.tgz" + version "1.3.5" + resolved "https://registry.yarnpkg.com/pvtsutils/-/pvtsutils-1.3.5.tgz#b8705b437b7b134cd7fd858f025a23456f1ce910" + integrity sha512-ARvb14YB9Nm2Xi6nBq1ZX6dAM0FsJnuk+31aUp4TrcZEdKUlSqOqsxJHUPJDNE3qiIp+iUPEIeR6Je/tgV7zsA== dependencies: - tslib "^2.4.0" + tslib "^2.6.1" pvutils@^1.1.3: version "1.1.3" - resolved "https://registry.npmjs.org/pvutils/-/pvutils-1.1.3.tgz" + resolved "https://registry.yarnpkg.com/pvutils/-/pvutils-1.1.3.tgz#f35fc1d27e7cd3dfbd39c0826d173e806a03f5a3" + integrity sha512-pMpnA0qRdFp32b1sJl1wOJNxZLQ2cbQx+k6tjNtZ8CpvVhNqEPRgivZ2WOUev2YMajecdH7ctUPDvEe87nariQ== qrcode-terminal@^0.12.0: version "0.12.0" @@ -5442,12 +5955,20 @@ qrcode-terminal@^0.12.0: queue-microtask@^1.2.2: version "1.2.3" - resolved "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz" + resolved "https://registry.yarnpkg.com/queue-microtask/-/queue-microtask-1.2.3.tgz#4929228bbc724dfac43e0efb058caf7b6cfb6243" + integrity sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A== quick-lru@^5.1.1: version "5.1.1" resolved "https://registry.npmjs.org/quick-lru/-/quick-lru-5.1.1.tgz" +raf@^3.4.1: + version "3.4.1" + resolved "https://registry.yarnpkg.com/raf/-/raf-3.4.1.tgz#0742e99a4a6552f445d73e3ee0328af0ff1ede39" + integrity sha512-Sq4CW4QhwOHE8ucn6J34MqtZCeWFP2aQSmrlroYgqAV1PjStIhJXxYuTgUIfkEk7zTLjmIjLmU5q+fbD1NnOJA== + dependencies: + performance-now "^2.1.0" + rdk@>=6.4.4: version "6.4.4" resolved "https://registry.npmjs.org/rdk/-/rdk-6.4.4.tgz" @@ -5515,9 +6036,10 @@ react-is@^16.13.1, react-is@^16.7.0: version "16.13.1" resolved "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz" -react-toastify@^9.1.1: - version "9.1.2" - resolved "https://registry.npmjs.org/react-toastify/-/react-toastify-9.1.2.tgz" +react-toastify@^9.1.3: + version "9.1.3" + resolved "https://registry.yarnpkg.com/react-toastify/-/react-toastify-9.1.3.tgz#1e798d260d606f50e0fab5ee31daaae1d628c5ff" + integrity sha512-fPfb8ghtn/XMxw3LkxQBk3IyagNpF/LIKjOBflbexr2AWxAH1MJgvnESwEwBn9liLFXgTKWgBSdZpw9m4OTHTg== dependencies: clsx "^1.1.1" @@ -5612,9 +6134,15 @@ reaviz@^14.4.2: react-fast-compare "^3.2.1" transformation-matrix "^2.9.0" -regenerator-runtime@^0.13.11: +regenerator-runtime@^0.13.11, regenerator-runtime@^0.13.7: version "0.13.11" - resolved "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.13.11.tgz" + resolved "https://registry.yarnpkg.com/regenerator-runtime/-/regenerator-runtime-0.13.11.tgz#f6dca3e7ceec20590d07ada785636a90cdca17f9" + integrity sha512-kY1AZVr2Ra+t+piVaJ4gxaFaReZVH40AKNo7UCX6W+dEwBo/2oZJzqfuN1qLq1oL45o56cPaTXELwrTh8Fpggg== + +regenerator-runtime@^0.14.0: + version "0.14.0" + resolved "https://registry.yarnpkg.com/regenerator-runtime/-/regenerator-runtime-0.14.0.tgz#5e19d68eb12d486f797e15a3c6a918f7cec5eb45" + integrity sha512-srw17NI0TUWHuGa5CFGGmhfNIeja30WMBfbslPNhf6JrqQlLN5gcrvig1oqPxiVaXb0oW0XRKtH6Nngs5lKCIA== regexp.prototype.flags@^1.2.0, regexp.prototype.flags@^1.4.3: version "1.4.3" @@ -5630,7 +6158,8 @@ regexpp@^3.2.0: relay-runtime@12.0.0: version "12.0.0" - resolved "https://registry.npmjs.org/relay-runtime/-/relay-runtime-12.0.0.tgz" + resolved "https://registry.yarnpkg.com/relay-runtime/-/relay-runtime-12.0.0.tgz#1e039282bdb5e0c1b9a7dc7f6b9a09d4f4ff8237" + integrity sha512-QU6JKr1tMsry22DXNy9Whsq5rmvwr3LSZiiWV/9+DFpuTWvp+WFhobWMc8TC4OjKFfNhEZy7mOiqUAn5atQtug== dependencies: "@babel/runtime" "^7.0.0" fbjs "^3.0.0" @@ -5638,15 +6167,18 @@ relay-runtime@12.0.0: remedial@^1.0.7: version "1.0.8" - resolved "https://registry.npmjs.org/remedial/-/remedial-1.0.8.tgz" + resolved "https://registry.yarnpkg.com/remedial/-/remedial-1.0.8.tgz#a5e4fd52a0e4956adbaf62da63a5a46a78c578a0" + integrity sha512-/62tYiOe6DzS5BqVsNpH/nkGlX45C/Sp6V+NtiN6JQNS1Viay7cWkazmRkrQrdFj2eshDe96SIQNIoMxqhzBOg== remove-trailing-separator@^1.0.1: version "1.1.0" - resolved "https://registry.npmjs.org/remove-trailing-separator/-/remove-trailing-separator-1.1.0.tgz" + resolved "https://registry.yarnpkg.com/remove-trailing-separator/-/remove-trailing-separator-1.1.0.tgz#c24bce2a283adad5bc3f58e0d48249b92379d8ef" + integrity sha512-/hS+Y0u3aOfIETiaiirUFwDBDzmXPvO+jAfKTitUngIPzdKc6Z0LoFjM/CK5PL4C+eKwHohlHAb6H0VFfmmUsw== remove-trailing-spaces@^1.0.6: version "1.0.8" - resolved "https://registry.npmjs.org/remove-trailing-spaces/-/remove-trailing-spaces-1.0.8.tgz" + resolved "https://registry.yarnpkg.com/remove-trailing-spaces/-/remove-trailing-spaces-1.0.8.tgz#4354d22f3236374702f58ee373168f6d6887ada7" + integrity sha512-O3vsMYfWighyFbTd8hk8VaSj9UAGENxAtX+//ugIst2RMk5e03h6RoIS+0ylsFxY1gvmPuAY/PO4It+gPEeySA== repeat-string@^1.5.2: version "1.6.1" @@ -5654,19 +6186,23 @@ repeat-string@^1.5.2: require-directory@^2.1.1: version "2.1.1" - resolved "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz" + resolved "https://registry.yarnpkg.com/require-directory/-/require-directory-2.1.1.tgz#8c64ad5fd30dab1c976e2344ffe7f792a6a6df42" + integrity sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q== require-main-filename@^2.0.0: version "2.0.0" - resolved "https://registry.npmjs.org/require-main-filename/-/require-main-filename-2.0.0.tgz" + resolved "https://registry.yarnpkg.com/require-main-filename/-/require-main-filename-2.0.0.tgz#d0b329ecc7cc0f61649f62215be69af54aa8989b" + integrity sha512-NKN5kMDylKuldxYLSUfrbo5Tuzh4hd+2E8NPPX02mZtn1VuREQToYe/ZdlJy+J3uCpfaiGF05e7B8W0iXbQHmg== resolve-from@5.0.0: version "5.0.0" - resolved "https://registry.npmjs.org/resolve-from/-/resolve-from-5.0.0.tgz" + resolved "https://registry.yarnpkg.com/resolve-from/-/resolve-from-5.0.0.tgz#c35225843df8f776df21c57557bc087e9dfdfc69" + integrity sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw== resolve-from@^4.0.0: version "4.0.0" - resolved "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz" + resolved "https://registry.yarnpkg.com/resolve-from/-/resolve-from-4.0.0.tgz#4abcd852ad32dd7baabfe9b40e00a36db5f392e6" + integrity sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g== resolve@^1.1.7, resolve@^1.22.1, resolve@~1.22.1: version "1.22.1" @@ -5690,7 +6226,8 @@ response-iterator@^0.2.6: restore-cursor@^3.1.0: version "3.1.0" - resolved "https://registry.npmjs.org/restore-cursor/-/restore-cursor-3.1.0.tgz" + resolved "https://registry.yarnpkg.com/restore-cursor/-/restore-cursor-3.1.0.tgz#39f67c54b3a7a58cea5236d95cf0034239631f7e" + integrity sha512-l+sSefzHpj5qimhFSE5a8nufZYAM3sBSVMAPtYkmC+4EH2anSGaEMXSD0izRQbu9nfyQ9y5JrVmp7E8oZrUjvA== dependencies: onetime "^5.1.0" signal-exit "^3.0.2" @@ -5707,11 +6244,18 @@ retry@^0.12.0: reusify@^1.0.4: version "1.0.4" - resolved "https://registry.npmjs.org/reusify/-/reusify-1.0.4.tgz" + resolved "https://registry.yarnpkg.com/reusify/-/reusify-1.0.4.tgz#90da382b1e126efc02146e90845a88db12925d76" + integrity sha512-U9nH88a3fc/ekCF1l0/UP1IosiuIjyTh7hBvXVMHYgVcfGvt897Xguj2UOLDeI5BG2m7/uwyaLVT6fbtCwTyzw== rfdc@^1.3.0: version "1.3.0" - resolved "https://registry.npmjs.org/rfdc/-/rfdc-1.3.0.tgz" + resolved "https://registry.yarnpkg.com/rfdc/-/rfdc-1.3.0.tgz#d0b7c441ab2720d05dc4cf26e01c89631d9da08b" + integrity sha512-V2hovdzFbOi77/WajaSMXk2OLm+xNIeQdMMuB7icj7bk6zi2F8GGAxigcnDFpJHbNyNcgyJDiP+8nOrY5cZGrA== + +rgbcolor@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/rgbcolor/-/rgbcolor-1.0.1.tgz#d6505ecdb304a6595da26fa4b43307306775945d" + integrity sha512-9aZLIrhRaD97sgVhtJOW6ckOEh6/GnvQtdVNfdZ6s67+3/XwLS9lBcQYzEEhYVeUowN7pRzMLsyGhK2i/xvWbw== right-align@^0.1.1: version "0.1.3" @@ -5735,11 +6279,13 @@ rollup@^0.25.8: run-async@^2.4.0: version "2.4.1" - resolved "https://registry.npmjs.org/run-async/-/run-async-2.4.1.tgz" + resolved "https://registry.yarnpkg.com/run-async/-/run-async-2.4.1.tgz#8440eccf99ea3e70bd409d49aab88e10c189a455" + integrity sha512-tvVnVv01b8c1RrA6Ep7JkStj85Guv/YrMcwqYQnwjsAS2cTmmPGBBjAjpCW7RrSodNSoE2/qg9O4bceNvUuDgQ== run-parallel@^1.1.9: version "1.2.0" - resolved "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz" + resolved "https://registry.yarnpkg.com/run-parallel/-/run-parallel-1.2.0.tgz#66d1368da7bdf921eb9d95bd1a9229e7f21a43ee" + integrity sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA== dependencies: queue-microtask "^1.2.2" @@ -5748,8 +6294,9 @@ rw@^1.3.2: resolved "https://registry.npmjs.org/rw/-/rw-1.3.3.tgz" rxjs@^7.5.5: - version "7.8.0" - resolved "https://registry.npmjs.org/rxjs/-/rxjs-7.8.0.tgz" + version "7.8.1" + resolved "https://registry.yarnpkg.com/rxjs/-/rxjs-7.8.1.tgz#6f6f3d99ea8044291efd92e7c7fcf562c4057543" + integrity sha512-AA3TVj+0A2iuIoQkWEK/tqFjBq2j+6PO6Y0zJcvzLAFhEFIO3HL0vls9hWLncZbAAbK0mar7oZ4V079I/qPMxg== dependencies: tslib "^2.1.0" @@ -5769,9 +6316,10 @@ safe-regex-test@^1.0.0: version "2.1.2" resolved "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz" -sass@^1.59.3: - version "1.62.0" - resolved "https://registry.npmjs.org/sass/-/sass-1.62.0.tgz" +sass@^1.69.4: + version "1.69.4" + resolved "https://registry.yarnpkg.com/sass/-/sass-1.69.4.tgz#10c735f55e3ea0b7742c6efa940bce30e07fbca2" + integrity sha512-+qEreVhqAy8o++aQfCJwp0sklr2xyEzkm9Pp/Igu9wNPoe7EZEQ8X/MBvvXggI2ql607cxKg/RKOwDj6pp2XDA== dependencies: chokidar ">=3.0.0 <4.0.0" immutable "^4.0.0" @@ -5785,11 +6333,13 @@ scheduler@^0.23.0: scuid@^1.1.0: version "1.1.0" - resolved "https://registry.npmjs.org/scuid/-/scuid-1.1.0.tgz" + resolved "https://registry.yarnpkg.com/scuid/-/scuid-1.1.0.tgz#d3f9f920956e737a60f72d0e4ad280bf324d5dab" + integrity sha512-MuCAyrGZcTLfQoH2XoBlQ8C6bzwN88XT/0slOGz0pn8+gIP85BOAfYa44ZXQUTOwRwPU0QvgU+V+OSajl/59Xg== -semver@^6.3.0: - version "6.3.0" - resolved "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz" +semver@^6.3.0, semver@^6.3.1: + version "6.3.1" + resolved "https://registry.yarnpkg.com/semver/-/semver-6.3.1.tgz#556d2ef8689146e46dcea4bfdd095f3434dffcb4" + integrity sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA== semver@^7.0.0, semver@^7.1.1, semver@^7.3.5, semver@^7.5.0: version "7.5.0" @@ -5805,7 +6355,8 @@ semver@^7.3.7, semver@^7.3.8: sentence-case@^3.0.4: version "3.0.4" - resolved "https://registry.npmjs.org/sentence-case/-/sentence-case-3.0.4.tgz" + resolved "https://registry.yarnpkg.com/sentence-case/-/sentence-case-3.0.4.tgz#3645a7b8c117c787fde8702056225bb62a45131f" + integrity sha512-8LS0JInaQMCRoQ7YUytAo/xUu5W2XnQxV2HI/6uM6U7CITS1RqPElr30V6uIqyMKM9lJGRVFy5/4CuzcixNYSg== dependencies: no-case "^3.0.4" tslib "^2.0.3" @@ -5813,11 +6364,13 @@ sentence-case@^3.0.4: set-blocking@^2.0.0: version "2.0.0" - resolved "https://registry.npmjs.org/set-blocking/-/set-blocking-2.0.0.tgz" + resolved "https://registry.yarnpkg.com/set-blocking/-/set-blocking-2.0.0.tgz#045f9782d011ae9a6803ddd382b24392b3d890f7" + integrity sha512-KiKBS8AnWGEyLzofFfmvKwpdPzqiy16LvQfK3yv/fVH7Bj13/wl3JSR1J+rfgRE9q7xUJK4qvgS8raSOeLUehw== setimmediate@^1.0.5: version "1.0.5" - resolved "https://registry.npmjs.org/setimmediate/-/setimmediate-1.0.5.tgz" + resolved "https://registry.yarnpkg.com/setimmediate/-/setimmediate-1.0.5.tgz#290cbb232e306942d7d7ea9b83732ab7856f8285" + integrity sha512-MATJdZp8sLqDl/68LfQmbP8zKPLQNV6BIZoIgrscFDQ+RsvK/BxeDQOgyxKKoh0y/8h3BqVFnCqQ/gd+reiIXA== shebang-command@^2.0.0: version "2.0.0" @@ -5831,7 +6384,8 @@ shebang-regex@^3.0.0: shell-quote@^1.7.3: version "1.8.1" - resolved "https://registry.npmjs.org/shell-quote/-/shell-quote-1.8.1.tgz" + resolved "https://registry.yarnpkg.com/shell-quote/-/shell-quote-1.8.1.tgz#6dbf4db75515ad5bac63b4f1894c3a154c766680" + integrity sha512-6j1W9l1iAs/4xYBI1SYOVZyFcCis9b4KCLQ8fgAGG07QvzaRLVVRQvAy85yNmmZSjYjg4MWh4gNvlPujU/5LpA== side-channel@^1.0.4: version "1.0.4" @@ -5847,7 +6401,8 @@ signal-exit@^3.0.2, signal-exit@^3.0.7: signedsource@^1.0.0: version "1.0.0" - resolved "https://registry.npmjs.org/signedsource/-/signedsource-1.0.0.tgz" + resolved "https://registry.yarnpkg.com/signedsource/-/signedsource-1.0.0.tgz#1ddace4981798f93bd833973803d80d52e93ad6a" + integrity sha512-6+eerH9fEnNmi/hyM1DXcRK3pWdoMQtlkQ+ns0ntzunjKqp5i3sKCc80ym8Fib3iaYhdJUOPdhlJWj1tvge2Ww== sigstore@^1.0.0: version "1.8.0" @@ -5860,7 +6415,8 @@ sigstore@^1.0.0: slash@^3.0.0: version "3.0.0" - resolved "https://registry.npmjs.org/slash/-/slash-3.0.0.tgz" + resolved "https://registry.yarnpkg.com/slash/-/slash-3.0.0.tgz#6539be870c165adbd5240220dbe361f1bc4d4634" + integrity sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q== slash@^4.0.0: version "4.0.0" @@ -5868,7 +6424,8 @@ slash@^4.0.0: slice-ansi@^3.0.0: version "3.0.0" - resolved "https://registry.npmjs.org/slice-ansi/-/slice-ansi-3.0.0.tgz" + resolved "https://registry.yarnpkg.com/slice-ansi/-/slice-ansi-3.0.0.tgz#31ddc10930a1b7e0b67b08c96c2f49b77a789787" + integrity sha512-pSyv7bSTC7ig9Dcgbw9AuRNUb5k5V6oDudjZoMBSr13qpLBG7tB+zgCkARjq7xIUgdz5P1Qe8u+rSGdouOOIyQ== dependencies: ansi-styles "^4.0.0" astral-regex "^2.0.0" @@ -5876,7 +6433,8 @@ slice-ansi@^3.0.0: slice-ansi@^4.0.0: version "4.0.0" - resolved "https://registry.npmjs.org/slice-ansi/-/slice-ansi-4.0.0.tgz" + resolved "https://registry.yarnpkg.com/slice-ansi/-/slice-ansi-4.0.0.tgz#500e8dd0fd55b05815086255b3195adf2a45fe6b" + integrity sha512-qMCMfhY040cVHT43K9BFygqYbUPFZKHOg7K73mtTWJRb8pyP3fzf4Ixd5SzdEJQ6MRUg/WBnOLxghZtKKurENQ== dependencies: ansi-styles "^4.0.0" astral-regex "^2.0.0" @@ -5888,7 +6446,8 @@ smart-buffer@^4.2.0: snake-case@^3.0.4: version "3.0.4" - resolved "https://registry.npmjs.org/snake-case/-/snake-case-3.0.4.tgz" + resolved "https://registry.yarnpkg.com/snake-case/-/snake-case-3.0.4.tgz#4f2bbd568e9935abdfd593f34c691dadb49c452c" + integrity sha512-LAOh4z89bGQvl9pFfNF8V146i7o7/CqFPbqzYgP+yYzDIDeS9HaNFtXABamRW+AQzEVODcvE79ljJ+8a9YSdMg== dependencies: dot-case "^3.0.4" tslib "^2.0.3" @@ -5952,7 +6511,8 @@ spdx-license-ids@^3.0.0: sponge-case@^1.0.1: version "1.0.1" - resolved "https://registry.npmjs.org/sponge-case/-/sponge-case-1.0.1.tgz" + resolved "https://registry.yarnpkg.com/sponge-case/-/sponge-case-1.0.1.tgz#260833b86453883d974f84854cdb63aecc5aef4c" + integrity sha512-dblb9Et4DAtiZ5YSUZHLl4XhH4uK80GhAZrVXdN4O2P4gQ40Wa5UIOPUHlA/nFd2PLblBZWUioLMMAVrgpoYcA== dependencies: tslib "^2.0.3" @@ -5968,6 +6528,11 @@ ssri@^9.0.0: dependencies: minipass "^3.1.1" +stackblur-canvas@^2.0.0: + version "2.6.0" + resolved "https://registry.yarnpkg.com/stackblur-canvas/-/stackblur-canvas-2.6.0.tgz#7876bab4ea99bfc97b69ce662614d7a1afb2d71b" + integrity sha512-8S1aIA+UoF6erJYnglGPug6MaHYGo1Ot7h5fuXx4fUPvcvQfcdw2o/ppCse63+eZf8PPidSu4v1JnmEVtEDnpg== + stop-iteration-iterator@^1.0.0: version "1.0.0" resolved "https://registry.npmjs.org/stop-iteration-iterator/-/stop-iteration-iterator-1.0.0.tgz" @@ -5976,11 +6541,13 @@ stop-iteration-iterator@^1.0.0: streamsearch@^1.1.0: version "1.1.0" - resolved "https://registry.npmjs.org/streamsearch/-/streamsearch-1.1.0.tgz" + resolved "https://registry.yarnpkg.com/streamsearch/-/streamsearch-1.1.0.tgz#404dd1e2247ca94af554e841a8ef0eaa238da764" + integrity sha512-Mcc5wHehp9aXz1ax6bZUyY5afg9u2rv5cqQI3mRrYkGC8rW2hM02jWuwjtL++LS5qinSyhj2QfLyNsuc+VsExg== string-env-interpolation@1.0.1, string-env-interpolation@^1.0.1: version "1.0.1" - resolved "https://registry.npmjs.org/string-env-interpolation/-/string-env-interpolation-1.0.1.tgz" + resolved "https://registry.yarnpkg.com/string-env-interpolation/-/string-env-interpolation-1.0.1.tgz#ad4397ae4ac53fe6c91d1402ad6f6a52862c7152" + integrity sha512-78lwMoCcn0nNu8LszbP1UA7g55OeE4v7rCeWnM5B453rnNr4aq+5it3FEYtZrSEiMvHZOZ9Jlqb0OD0M2VInqg== "string-width@^1.0.2 || 2 || 3 || 4", string-width@^4.1.0, string-width@^4.2.0, string-width@^4.2.3: version "4.2.3" @@ -6029,7 +6596,8 @@ string.prototype.trimstart@^1.0.6: string_decoder@^1.1.1: version "1.3.0" - resolved "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz" + resolved "https://registry.yarnpkg.com/string_decoder/-/string_decoder-1.3.0.tgz#42f114594a46cf1a8e30b0a84f56c78c3edac21e" + integrity sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA== dependencies: safe-buffer "~5.2.0" @@ -6041,7 +6609,8 @@ strip-ansi@^3.0.0: strip-ansi@^6.0.0, strip-ansi@^6.0.1: version "6.0.1" - resolved "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz" + resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-6.0.1.tgz#9e26c63d30f53443e9489495b2105d37b67a85d9" + integrity sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A== dependencies: ansi-regex "^5.0.1" @@ -6077,13 +6646,15 @@ supports-color@^2.0.0: supports-color@^5.3.0: version "5.5.0" - resolved "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz" + resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-5.5.0.tgz#e2e69a44ac8772f78a1ec0b35b689df6530efc8f" + integrity sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow== dependencies: has-flag "^3.0.0" supports-color@^7.1.0: version "7.2.0" - resolved "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz" + resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-7.2.0.tgz#1b7dcdcb32b8138801b3e478ba6a51caa89648da" + integrity sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw== dependencies: has-flag "^4.0.0" @@ -6091,9 +6662,15 @@ supports-preserve-symlinks-flag@^1.0.0: version "1.0.0" resolved "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz" +svg-pathdata@^6.0.3: + version "6.0.3" + resolved "https://registry.yarnpkg.com/svg-pathdata/-/svg-pathdata-6.0.3.tgz#80b0e0283b652ccbafb69ad4f8f73e8d3fbf2cac" + integrity sha512-qsjeeq5YjBZ5eMdFuUa4ZosMLxgr5RZ+F+Y1OrDhuOCEInRMA3x74XdBtggJcj9kOeInz0WE+LgCPDkZFlBYJw== + swap-case@^2.0.2: version "2.0.2" - resolved "https://registry.npmjs.org/swap-case/-/swap-case-2.0.2.tgz" + resolved "https://registry.yarnpkg.com/swap-case/-/swap-case-2.0.2.tgz#671aedb3c9c137e2985ef51c51f9e98445bf70d9" + integrity sha512-kc6S2YS/2yXbtkSMunBtKdah4VFETZ8Oh6ONSmSd9bRxhqTrtARUCBUiWXH3xVPpvR7tz2CSnkuXVE42EcGnMw== dependencies: tslib "^2.0.3" @@ -6176,6 +6753,13 @@ tar@^6.1.11, tar@^6.1.13, tar@^6.1.2: mkdirp "^1.0.3" yallist "^4.0.0" +text-segmentation@^1.0.3: + version "1.0.3" + resolved "https://registry.yarnpkg.com/text-segmentation/-/text-segmentation-1.0.3.tgz#52a388159efffe746b24a63ba311b6ac9f2d7943" + integrity sha512-iOiPUo/BGnZ6+54OsWxZidGCsdU8YbE4PSpdPinp7DeMtUJNJBoJ/ouUSTJjHkh1KntHaltHl/gDs2FC4i5+Nw== + dependencies: + utrie "^1.0.2" + text-table@^0.2.0, text-table@~0.2.0: version "0.2.0" resolved "https://registry.npmjs.org/text-table/-/text-table-0.2.0.tgz" @@ -6209,29 +6793,34 @@ tiny-relative-date@^1.3.0: title-case@^3.0.3: version "3.0.3" - resolved "https://registry.npmjs.org/title-case/-/title-case-3.0.3.tgz" + resolved "https://registry.yarnpkg.com/title-case/-/title-case-3.0.3.tgz#bc689b46f02e411f1d1e1d081f7c3deca0489982" + integrity sha512-e1zGYRvbffpcHIrnuqT0Dh+gEJtDaxDSoG4JAIpq4oDFyooziLBIiYQv0GBT4FUAnUop5uZ1hiIAj7oAF6sOCA== dependencies: tslib "^2.0.3" tmp@^0.0.33: version "0.0.33" - resolved "https://registry.npmjs.org/tmp/-/tmp-0.0.33.tgz" + resolved "https://registry.yarnpkg.com/tmp/-/tmp-0.0.33.tgz#6d34335889768d21b2bcda0aa277ced3b1bfadf9" + integrity sha512-jRCJlojKnZ3addtTOjdIqoRuPEKBvNXcGYqzO6zWZX8KfKEpnGY5jfggJQ3EjKuu8D4bJRr0y+cYJFmYbImXGw== dependencies: os-tmpdir "~1.0.2" to-fast-properties@^2.0.0: version "2.0.0" - resolved "https://registry.npmjs.org/to-fast-properties/-/to-fast-properties-2.0.0.tgz" + resolved "https://registry.yarnpkg.com/to-fast-properties/-/to-fast-properties-2.0.0.tgz#dc5e698cbd079265bc73e0377681a4e4e83f616e" + integrity sha512-/OaKK0xYrs3DmxRYqL/yDc+FxFUVYhDlXMhRmv3z915w2HF1tnN1omB354j8VUGO/hbRzyD6Y3sA7v7GS/ceog== to-regex-range@^5.0.1: version "5.0.1" - resolved "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz" + resolved "https://registry.yarnpkg.com/to-regex-range/-/to-regex-range-5.0.1.tgz#1648c44aae7c8d988a326018ed72f5b4dd0392e4" + integrity sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ== dependencies: is-number "^7.0.0" tr46@~0.0.3: version "0.0.3" - resolved "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz" + resolved "https://registry.yarnpkg.com/tr46/-/tr46-0.0.3.tgz#8184fd347dac9cdc185992f3a6622e14b9d9ab6a" + integrity sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw== transformation-matrix@^2.9.0: version "2.15.0" @@ -6253,7 +6842,8 @@ ts-invariant@^0.10.3: ts-log@^2.2.3: version "2.2.5" - resolved "https://registry.npmjs.org/ts-log/-/ts-log-2.2.5.tgz" + resolved "https://registry.yarnpkg.com/ts-log/-/ts-log-2.2.5.tgz#aef3252f1143d11047e2cb6f7cfaac7408d96623" + integrity sha512-PGcnJoTBnVGy6yYNFxWVNkdcAuAMstvutN9MgDJIV6L0oG8fB+ZNNy1T+wJzah8RPGor1mZuPQkVfXNDpy9eHA== tsconfig-paths@^3.14.1: version "3.14.1" @@ -6272,13 +6862,24 @@ tslib@^1.8.1: version "1.14.1" resolved "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz" -tslib@^2.0.0, tslib@^2.0.3, tslib@^2.1.0, tslib@^2.3.0, tslib@^2.3.1, tslib@^2.4.0, tslib@^2.5.0, tslib@~2.5.0: +tslib@^2.0.0, tslib@^2.0.3, tslib@^2.1.0, tslib@^2.3.1, tslib@^2.4.0, tslib@^2.5.0, tslib@^2.6.1: + version "2.6.2" + resolved "https://registry.yarnpkg.com/tslib/-/tslib-2.6.2.tgz#703ac29425e7b37cd6fd456e92404d46d1f3e4ae" + integrity sha512-AEYxH93jGFPn/a2iVAwW87VuUIkR1FVUKB77NwMF7nBTDkDrrT/Hpt/IrCJ0QXhW27jTBDcf5ZY7w6RiqTMw2Q== + +tslib@^2.3.0: version "2.5.0" resolved "https://registry.npmjs.org/tslib/-/tslib-2.5.0.tgz" tslib@~2.4.0: version "2.4.1" - resolved "https://registry.npmjs.org/tslib/-/tslib-2.4.1.tgz" + resolved "https://registry.yarnpkg.com/tslib/-/tslib-2.4.1.tgz#0d0bfbaac2880b91e22df0768e55be9753a5b17e" + integrity sha512-tGyy4dAjRIEwI7BzsB0lynWgOpfqjUdq91XXAlIWD2OwKBH7oCl/GZG/HT4BOHrTlPMOASlMQ7veyTqpmRcrNA== + +tslib@~2.5.0: + version "2.5.3" + resolved "https://registry.yarnpkg.com/tslib/-/tslib-2.5.3.tgz#24944ba2d990940e6e982c4bea147aba80209913" + integrity sha512-mSxlJJwl3BMEQCUNnxXBU9jP4JBktcEGhURcPR6VQVlnP0FdDEsIaz0C35dXNGLyRfrATNofF0F5p2KPxQgB+w== tsutils@^3.21.0: version "3.21.0" @@ -6306,7 +6907,8 @@ type-fest@^0.20.2: type-fest@^0.21.3: version "0.21.3" - resolved "https://registry.npmjs.org/type-fest/-/type-fest-0.21.3.tgz" + resolved "https://registry.yarnpkg.com/type-fest/-/type-fest-0.21.3.tgz#d260a24b0198436e133fa26a524a6d65fa3b2e37" + integrity sha512-t0rzBq87m3fVcduHDUFhKmyyX+9eo6WQjZvf51Ea/M0Q7+T374Jp1aUiyUl0GKxp8M/OETVHSDvmkyPgvX+X2w== typed-array-length@^1.0.4: version "1.0.4" @@ -6320,9 +6922,10 @@ typescript@4.9.5: version "4.9.5" resolved "https://registry.npmjs.org/typescript/-/typescript-4.9.5.tgz" -ua-parser-js@^0.7.30: - version "0.7.35" - resolved "https://registry.npmjs.org/ua-parser-js/-/ua-parser-js-0.7.35.tgz" +ua-parser-js@^1.0.35: + version "1.0.35" + resolved "https://registry.yarnpkg.com/ua-parser-js/-/ua-parser-js-1.0.35.tgz#c4ef44343bc3db0a3cbefdf21822f1b1fc1ab011" + integrity sha512-fKnGuqmTBnIE+/KXSzCn4db8RTigUzw1AN0DmdU6hJovUTbYJKyqj+8Mt1c4VfRDnOVJnENmfYkIPZ946UrSAA== uglify-js@^2.6.2: version "2.8.29" @@ -6348,7 +6951,8 @@ unbox-primitive@^1.0.2: unc-path-regex@^0.1.2: version "0.1.2" - resolved "https://registry.npmjs.org/unc-path-regex/-/unc-path-regex-0.1.2.tgz" + resolved "https://registry.yarnpkg.com/unc-path-regex/-/unc-path-regex-0.1.2.tgz#e73dd3d7b0d7c5ed86fbac6b0ae7d8c6a69d50fa" + integrity sha512-eXL4nmJT7oCpkZsHZUOJo8hcX3GbsiDOa0Qu9F646fi8dT3XuSVopVqAcEiVzSKKH7UoDti23wNX3qGFxcW5Qg== unique-filename@^2.0.0: version "2.0.1" @@ -6376,26 +6980,30 @@ unique-slug@^4.0.0: unixify@^1.0.0: version "1.0.0" - resolved "https://registry.npmjs.org/unixify/-/unixify-1.0.0.tgz" + resolved "https://registry.yarnpkg.com/unixify/-/unixify-1.0.0.tgz#3a641c8c2ffbce4da683a5c70f03a462940c2090" + integrity sha512-6bc58dPYhCMHHuwxldQxO3RRNZ4eCogZ/st++0+fcC1nr0jiGUtAdBJ2qzmLQWSxbtz42pWt4QQMiZ9HvZf5cg== dependencies: normalize-path "^2.1.1" -update-browserslist-db@^1.0.10: +update-browserslist-db@^1.0.10, update-browserslist-db@^1.0.11: version "1.0.11" - resolved "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.0.11.tgz" + resolved "https://registry.yarnpkg.com/update-browserslist-db/-/update-browserslist-db-1.0.11.tgz#9a2a641ad2907ae7b3616506f4b977851db5b940" + integrity sha512-dCwEFf0/oT85M1fHBg4F0jtLwJrutGoHSQXCh7u4o2t1drG+c0a9Flnqww6XUKSfQMPpJBRjU8d4RXB09qtvaA== dependencies: escalade "^3.1.1" picocolors "^1.0.0" upper-case-first@^2.0.2: version "2.0.2" - resolved "https://registry.npmjs.org/upper-case-first/-/upper-case-first-2.0.2.tgz" + resolved "https://registry.yarnpkg.com/upper-case-first/-/upper-case-first-2.0.2.tgz#992c3273f882abd19d1e02894cc147117f844324" + integrity sha512-514ppYHBaKwfJRK/pNC6c/OxfGa0obSnAl106u97Ed0I625Nin96KAjttZF6ZL3e1XLtphxnqrOi9iWgm+u+bg== dependencies: tslib "^2.0.3" upper-case@^2.0.2: version "2.0.2" - resolved "https://registry.npmjs.org/upper-case/-/upper-case-2.0.2.tgz" + resolved "https://registry.yarnpkg.com/upper-case/-/upper-case-2.0.2.tgz#d89810823faab1df1549b7d97a76f8662bae6f7a" + integrity sha512-KgdgDGJt2TpuwBUIjgG6lzw2GWFRCW9Qkfkiv0DxqHHLYJHmtmdUIKcZd8rHgFSjopVTlw6ggzCm1b8MFQwikg== dependencies: tslib "^2.0.3" @@ -6405,16 +7013,22 @@ uri-js@^4.2.2: dependencies: punycode "^2.1.0" -urlpattern-polyfill@^7.0.0: - version "7.0.0" - resolved "https://registry.npmjs.org/urlpattern-polyfill/-/urlpattern-polyfill-7.0.0.tgz" - dependencies: - braces "^3.0.2" +urlpattern-polyfill@^8.0.0: + version "8.0.2" + resolved "https://registry.yarnpkg.com/urlpattern-polyfill/-/urlpattern-polyfill-8.0.2.tgz#99f096e35eff8bf4b5a2aa7d58a1523d6ebc7ce5" + integrity sha512-Qp95D4TPJl1kC9SKigDcqgyM2VDVO4RiJc2d4qe5GrYm+zbIQCWWKAFaJNQ4BhdFeDGwBmAxqJBwWSJDb9T3BQ== util-deprecate@^1.0.1, util-deprecate@^1.0.2: version "1.0.2" resolved "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz" +utrie@^1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/utrie/-/utrie-1.0.2.tgz#d42fe44de9bc0119c25de7f564a6ed1b2c87a645" + integrity sha512-1MLa5ouZiOmQzUbjbu9VmjLzn1QLXBhwpUa7kdLUQK+KQ5KA9I1vk5U4YHe/X2Ch7PYnJfWuWT+VbuxbGwljhw== + dependencies: + base64-arraybuffer "^1.0.2" + uuid@^8.3.2: version "8.3.2" resolved "https://registry.npmjs.org/uuid/-/uuid-8.3.2.tgz" @@ -6432,9 +7046,10 @@ validate-npm-package-name@^5.0.0: dependencies: builtins "^5.0.0" -value-or-promise@1.0.12, value-or-promise@^1.0.11, value-or-promise@^1.0.12: +value-or-promise@^1.0.11, value-or-promise@^1.0.12: version "1.0.12" - resolved "https://registry.npmjs.org/value-or-promise/-/value-or-promise-1.0.12.tgz" + resolved "https://registry.yarnpkg.com/value-or-promise/-/value-or-promise-1.0.12.tgz#0e5abfeec70148c78460a849f6b003ea7986f15c" + integrity sha512-Z6Uz+TYwEqE7ZN50gwn+1LCVo9ZVrpxRPOhOLnncYkY1ZzOYtrX8Fwf/rFktZ8R5mJms6EZf5TqNOMeZmnPq9Q== walk-up-path@^3.0.1: version "3.0.1" @@ -6448,11 +7063,13 @@ wcwidth@^1.0.0, wcwidth@^1.0.1: web-streams-polyfill@^3.2.1: version "3.2.1" - resolved "https://registry.npmjs.org/web-streams-polyfill/-/web-streams-polyfill-3.2.1.tgz" + resolved "https://registry.yarnpkg.com/web-streams-polyfill/-/web-streams-polyfill-3.2.1.tgz#71c2718c52b45fd49dbeee88634b3a60ceab42a6" + integrity sha512-e0MO3wdXWKrLbL0DgGnUV7WHVuw9OUvL4hjgnPkIeEvESk74gAITi5G606JtZPp39cd8HA9VQzCIvA49LpPN5Q== webcrypto-core@^1.7.7: version "1.7.7" - resolved "https://registry.npmjs.org/webcrypto-core/-/webcrypto-core-1.7.7.tgz" + resolved "https://registry.yarnpkg.com/webcrypto-core/-/webcrypto-core-1.7.7.tgz#06f24b3498463e570fed64d7cab149e5437b162c" + integrity sha512-7FjigXNsBfopEj+5DV2nhNpfic2vumtjjgPmeDKk45z+MJwXKKfhPB7118Pfzrmh4jqOMST6Ch37iPAHoImg5g== dependencies: "@peculiar/asn1-schema" "^2.3.6" "@peculiar/json-schema" "^1.1.12" @@ -6462,11 +7079,13 @@ webcrypto-core@^1.7.7: webidl-conversions@^3.0.0: version "3.0.1" - resolved "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz" + resolved "https://registry.yarnpkg.com/webidl-conversions/-/webidl-conversions-3.0.1.tgz#24534275e2a7bc6be7bc86611cc16ae0a5654871" + integrity sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ== whatwg-url@^5.0.0: version "5.0.0" - resolved "https://registry.npmjs.org/whatwg-url/-/whatwg-url-5.0.0.tgz" + resolved "https://registry.yarnpkg.com/whatwg-url/-/whatwg-url-5.0.0.tgz#966454e8765462e37644d3626f6742ce8b70965d" + integrity sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw== dependencies: tr46 "~0.0.3" webidl-conversions "^3.0.0" @@ -6492,7 +7111,8 @@ which-collection@^1.0.1: which-module@^2.0.0: version "2.0.1" - resolved "https://registry.npmjs.org/which-module/-/which-module-2.0.1.tgz" + resolved "https://registry.yarnpkg.com/which-module/-/which-module-2.0.1.tgz#776b1fe35d90aebe99e8ac15eb24093389a4a409" + integrity sha512-iBdZ57RDvnOR9AGBhML2vFZf7h8vmBjhoaZqODJBFWHVtKkDmKuHai3cx5PgVMrX5YDNp27AofYbAwctSS+vhQ== which-typed-array@^1.1.9: version "1.1.9" @@ -6535,9 +7155,10 @@ wordwrap@0.0.2: version "0.0.2" resolved "https://registry.npmjs.org/wordwrap/-/wordwrap-0.0.2.tgz" -wrap-ansi@^6.2.0: +wrap-ansi@^6.0.1, wrap-ansi@^6.2.0: version "6.2.0" - resolved "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-6.2.0.tgz" + resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-6.2.0.tgz#e9393ba07102e6c91a3b221478f0257cd2856e53" + integrity sha512-r6lPcBGxZXlIcymEu7InxDMhdW0KDxpLgoFLcguasxCaJ/SOIZwINatK9KY/tf+ZrlywOKU0UDj3ATXUBfxJXA== dependencies: ansi-styles "^4.0.0" string-width "^4.1.0" @@ -6545,7 +7166,8 @@ wrap-ansi@^6.2.0: wrap-ansi@^7.0.0: version "7.0.0" - resolved "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz" + resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-7.0.0.tgz#67e145cff510a6a6984bdf1152911d69d2eb9e43" + integrity sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q== dependencies: ansi-styles "^4.0.0" string-width "^4.1.0" @@ -6553,7 +7175,8 @@ wrap-ansi@^7.0.0: wrappy@1: version "1.0.2" - resolved "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz" + resolved "https://registry.yarnpkg.com/wrappy/-/wrappy-1.0.2.tgz#b5243d8f3ec1aa35f1364605bc0d1036e30ab69f" + integrity sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ== write-file-atomic@^5.0.0: version "5.0.0" @@ -6564,19 +7187,23 @@ write-file-atomic@^5.0.0: ws@8.13.0, ws@^8.12.0: version "8.13.0" - resolved "https://registry.npmjs.org/ws/-/ws-8.13.0.tgz" + resolved "https://registry.yarnpkg.com/ws/-/ws-8.13.0.tgz#9a9fb92f93cf41512a0735c8f4dd09b8a1211cd0" + integrity sha512-x9vcZYTrFPC7aSIbj7sRCYo7L/Xb8Iy+pW0ng0wt2vCJv7M9HOMy0UoN3rr+IFC7hb7vXoqS+P9ktyLLLhO+LA== y18n@^4.0.0: version "4.0.3" - resolved "https://registry.npmjs.org/y18n/-/y18n-4.0.3.tgz" + resolved "https://registry.yarnpkg.com/y18n/-/y18n-4.0.3.tgz#b5f259c82cd6e336921efd7bfd8bf560de9eeedf" + integrity sha512-JKhqTOwSrqNA1NY5lSztJ1GrBiUodLMmIZuLiDaMRJ+itFd+ABVE8XBjOvIWL+rSqNDC74LCSFmlb/U4UZ4hJQ== y18n@^5.0.5: version "5.0.8" - resolved "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz" + resolved "https://registry.yarnpkg.com/y18n/-/y18n-5.0.8.tgz#7f4934d0f7ca8c56f95314939ddcd2dd91ce1d55" + integrity sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA== yallist@^3.0.2: version "3.1.1" - resolved "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz" + resolved "https://registry.yarnpkg.com/yallist/-/yallist-3.1.1.tgz#dbb7daf9bfd8bac9ab45ebf602b8cbad0d5d08fd" + integrity sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g== yallist@^4.0.0: version "4.0.0" @@ -6584,7 +7211,8 @@ yallist@^4.0.0: yaml-ast-parser@^0.0.43: version "0.0.43" - resolved "https://registry.npmjs.org/yaml-ast-parser/-/yaml-ast-parser-0.0.43.tgz" + resolved "https://registry.yarnpkg.com/yaml-ast-parser/-/yaml-ast-parser-0.0.43.tgz#e8a23e6fb4c38076ab92995c5dca33f3d3d7c9bb" + integrity sha512-2PTINUwsRqSd+s8XxKaJWQlUuEMHJQyEuh2edBbW8KNJz0SJPwUSD2zRWqezFEdN7IzAgeuYHFUCF7o8zRdZ0A== yaml@^1.10.0, yaml@^1.10.2: version "1.10.2" @@ -6592,18 +7220,21 @@ yaml@^1.10.0, yaml@^1.10.2: yargs-parser@^18.1.2: version "18.1.3" - resolved "https://registry.npmjs.org/yargs-parser/-/yargs-parser-18.1.3.tgz" + resolved "https://registry.yarnpkg.com/yargs-parser/-/yargs-parser-18.1.3.tgz#be68c4975c6b2abf469236b0c870362fab09a7b0" + integrity sha512-o50j0JeToy/4K6OZcaQmW6lyXXKhq7csREXcDwk2omFPJEwUNOVtJKvmDr9EI1fAJZUyZcRF7kxGBWmRXudrCQ== dependencies: camelcase "^5.0.0" decamelize "^1.2.0" yargs-parser@^21.1.1: version "21.1.1" - resolved "https://registry.npmjs.org/yargs-parser/-/yargs-parser-21.1.1.tgz" + resolved "https://registry.yarnpkg.com/yargs-parser/-/yargs-parser-21.1.1.tgz#9096bceebf990d21bb31fa9516e0ede294a77d35" + integrity sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw== yargs@^15.3.1: version "15.4.1" - resolved "https://registry.npmjs.org/yargs/-/yargs-15.4.1.tgz" + resolved "https://registry.yarnpkg.com/yargs/-/yargs-15.4.1.tgz#0d87a16de01aee9d8bec2bfbf74f67851730f4f8" + integrity sha512-aePbxDmcYW++PaqBsJ+HYUFwCdv4LVvdnhBy78E57PIor8/OVvhMrADFFEDh8DHDFRv/O9i3lPhsENjO7QX0+A== dependencies: cliui "^6.0.0" decamelize "^1.2.0" @@ -6618,8 +7249,9 @@ yargs@^15.3.1: yargs-parser "^18.1.2" yargs@^17.0.0: - version "17.7.1" - resolved "https://registry.npmjs.org/yargs/-/yargs-17.7.1.tgz" + version "17.7.2" + resolved "https://registry.yarnpkg.com/yargs/-/yargs-17.7.2.tgz#991df39aca675a192b816e1e0363f9d75d2aa269" + integrity sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w== dependencies: cliui "^8.0.1" escalade "^3.1.1" @@ -6640,7 +7272,8 @@ yargs@~3.10.0: yocto-queue@^0.1.0: version "0.1.0" - resolved "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz" + resolved "https://registry.yarnpkg.com/yocto-queue/-/yocto-queue-0.1.0.tgz#0294eb3dee05028d31ee1a5fa2c556a6aaf10a1b" + integrity sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q== zen-observable-ts@^1.2.5: version "1.2.5" diff --git a/img/console-home.png b/img/console-home.png deleted file mode 100644 index 70b186a2..00000000 Binary files a/img/console-home.png and /dev/null differ diff --git a/img/console-logs.png b/img/console-logs.png deleted file mode 100644 index 6e1dd087..00000000 Binary files a/img/console-logs.png and /dev/null differ diff --git a/img/environment-secrets.png b/img/environment-secrets.png new file mode 100644 index 00000000..5ee1dbd1 Binary files /dev/null and b/img/environment-secrets.png differ diff --git a/img/members.png b/img/members.png new file mode 100644 index 00000000..aac4e173 Binary files /dev/null and b/img/members.png differ diff --git a/img/secrets-overview.png b/img/secrets-overview.png new file mode 100644 index 00000000..495f55f3 Binary files /dev/null and b/img/secrets-overview.png differ diff --git a/img/vscode-demo.png b/img/vscode-demo.png deleted file mode 100644 index d4d77c44..00000000 Binary files a/img/vscode-demo.png and /dev/null differ diff --git a/nginx/default.conf b/nginx/default.conf index 461d45ba..48261dc2 100644 --- a/nginx/default.conf +++ b/nginx/default.conf @@ -5,19 +5,23 @@ server { ssl_certificate /etc/nginx/ssl/nginx.crt; ssl_certificate_key /etc/nginx/ssl/nginx.key; - location /ph-backend/ { - rewrite ^/ph-backend/(.*) /$1 break; + location /service/ { + rewrite ^/service/(.*) /$1 break; - proxy_set_header X-Real-IP $remote_addr; - proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + + proxy_set_header Host $http_host; + proxy_set_header X-NginX-Proxy true; - proxy_set_header Host $http_host; - proxy_set_header X-NginX-Proxy true; + proxy_pass http://backend:8000; + proxy_redirect off; - proxy_pass http://backend:8000; - proxy_redirect off; + proxy_cookie_path / "/; HttpOnly; SameSite=strict"; - proxy_cookie_path / "/; HttpOnly; SameSite=strict"; + proxy_buffers 16 32k; + proxy_buffer_size 64k; + proxy_busy_buffers_size 128k; } location /kms/ { @@ -33,6 +37,10 @@ server { proxy_redirect off; proxy_cookie_path / "/; HttpOnly; SameSite=strict"; + + proxy_buffers 16 32k; + proxy_buffer_size 64k; + proxy_busy_buffers_size 128k; } location / { @@ -49,5 +57,9 @@ server { proxy_pass http://frontend:3000; proxy_redirect off; + + proxy_buffers 16 32k; + proxy_buffer_size 64k; + proxy_busy_buffers_size 128k; } } diff --git a/staging-docker-compose.yml b/staging-docker-compose.yml index 14e21e1d..cb2f0ef0 100644 --- a/staging-docker-compose.yml +++ b/staging-docker-compose.yml @@ -31,8 +31,9 @@ services: NEXTAUTH_URL: "${HTTP_PROTOCOL}${HOST}" OAUTH_REDIRECT_URI: "${HTTP_PROTOCOL}${HOST}" BACKEND_API_BASE: "http://backend:8000" - NEXT_PUBLIC_BACKEND_API_BASE: "${HTTP_PROTOCOL}${HOST}/ph-backend" + NEXT_PUBLIC_BACKEND_API_BASE: "${HTTP_PROTOCOL}${HOST}/service" NEXT_PUBLIC_NEXTAUTH_PROVIDERS: "${SSO_PROVIDERS}" + NEXT_PUBLIC_POSTHOG_KEY: "${NEXT_PUBLIC_POSTHOG_KEY}" networks: - phase-net-dev @@ -54,7 +55,7 @@ services: postgres: container_name: phase-postgres - image: postgres + image: postgres:15.4-alpine3.17 restart: always env_file: - .env