Compare commits

...
Author SHA1 Message Date
Charles Bochet 0c3f3c4caa Regenerate SDK metadata client types after FileOutput addition 2026-05-23 15:39:43 +02:00
Charles Bochet a278cd6d85 Merge remote-tracking branch 'origin/main' into fix-app-logo 2026-05-23 15:39:13 +02:00
Charles Bochet 2d93bfa031 Extract .url from FileOutput in installedApplication logo consumers 2026-05-23 15:31:23 +02:00
Charles Bochet 9286e81275 Regenerate GraphQL types for FileOutput logo
Applied from CI's server-validation diff output. types.ts SDK numeric IDs
will follow in a separate commit after CI re-runs against the right base
(my local main is stale).
2026-05-23 15:25:18 +02:00
85d649e831 [Fix] Backfill missing command menu items conditional availability expression (#20852)
## Description

Following [report in
discord](https://discord.com/channels/1130383047699738754/1498690477044793386/1506602927412744242)

Some command menu items were showing to all users because they had no
conditional availability expression, whereas users did not actually have
access to the page or feature behind. For instance: "Go to Admin panel",
"Go to AI settings", "Send email" etc.

<img width="833" height="1245" alt="image"
src="https://github.com/user-attachments/assets/8d2a9404-9b81-4d58-9522-558e9924c457"
/>


## Fix

- Add conditional availability expressions
- Backfill expressions for existing workspaces as they are stored in db
(commandMenuItems table)

---------

Co-authored-by: Charles Bochet <charles@twenty.com>
2026-05-23 13:22:25 +00:00
Charles BochetandGitHub 2f4ebf8160 Move preview environment workflow to ci-privileged (#20872)
## Summary

Slims `preview-env-dispatch.yaml` to a single dispatch and deletes
`preview-env-keepalive.yaml`. The actual preview-env work moves to
**twentyhq/ci-privileged#22** (must merge as a pair).

## Why

Context: PR #20867 was a credential-exfil attempt against our workflows.
GitHub's default fork-PR-no-secrets policy + our existing gates
(`author_association` checks, `pull_request_target` checking out base,
`enableScripts: false`) neutralized the actual attack — but the audit
surfaced one workflow that *would* have given a malicious external PR
access to a real secret if a maintainer had applied the `preview-app`
label: `preview-env-keepalive.yaml`.

That workflow checked out the PR head SHA, did `docker login` with
`DOCKERHUB_PASSWORD`, then ran the PR's `docker-compose.yml`. A
malicious compose could have mounted `~/.docker/config.json` and
exfiltrated the Dockerhub credential.

After this PR, that workflow lives in `twentyhq/ci-privileged` instead,
paired with a rename of the credential to `DOCKERHUB_RO_TOKEN`
(Dockerhub PAT with `Public Repo Read-only` scope). A read-only PAT has
no exfiltration value — it's equivalent to anonymous Dockerhub access
plus rate-limit headroom — so the credential lives safely on the runner
without further hygiene tricks.

## What this PR does

- **Modifies** `.github/workflows/preview-env-dispatch.yaml`:
- Single dispatch to `twentyhq/ci-privileged` (was: self-dispatch to
twenty for the env + a separate dispatch to ci-privileged for the PR
comment).
  - `permissions: {}` (was: `contents: write`).
  - Drops `preview-env-keepalive.yaml` from the path-trigger list.
- **Deletes** `.github/workflows/preview-env-keepalive.yaml`. The
207-line workflow now lives in
`twentyhq/ci-privileged/.github/workflows/preview-env.yaml`.

Net `twenty` repo change: **-204 lines / +3 lines**.

## Companion PR

twentyhq/ci-privileged#22 — adds the new `preview-env.yaml`, deletes the
now-redundant `post-preview-comment.yaml`.

## Secrets fallout in this repo

After this PR, `DOCKERHUB_PASSWORD` in `twentyhq/twenty` secrets is only
used by `ci-test-docker-compose.yaml`, where:
- It evaluates to empty for fork PRs (GitHub default — secrets aren't
passed to fork-PR workflows).
- It's only needed for internal / merge_queue runs, for Dockerhub
rate-limit headroom on base-image pulls.

Recommend (separate change): also convert the twenty-side
`DOCKERHUB_PASSWORD` to a `Public Repo Read-only` Dockerhub PAT, and
rename it to `DOCKERHUB_RO_TOKEN` for consistency with ci-privileged.
The workflow change for `ci-test-docker-compose.yaml` would just be a
rename — login flow is identical for password vs. PAT.

## Test plan

- [ ] Merge twentyhq/ci-privileged#22 first (so the dispatched event has
a handler)
- [ ] Open an internal PR touching `packages/twenty-docker/**`, confirm
`Preview Environment Dispatch` runs and ci-privileged's `Preview
Environment` workflow runs the docker compose + posts the URL
- [ ] On an external contributor PR, apply the `preview-app` label,
confirm the same flow
- [ ] Confirm closing the PR doesn't break (no cleanup workflow was
changed)
2026-05-23 11:37:37 +00:00
Charles Bochet d348a4c908 Fix bare logo selections in remaining GraphQL queries
graphql-codegen rejects scalar selections on object types: findManyApplications
and the AI tools-table query were still selecting logo without subfields.
2026-05-23 13:31:18 +02:00
Charles Bochet 24be69dbb0 Use explicit GraphQL type tokens on nullable FileOutputDTO fields
NestJS schema builder errors with UndefinedTypeError when @Field is used
without an explicit type token on nullable scalars (the union with null
hides the reflected metadata).
2026-05-23 13:06:09 +02:00
Charles Bochet f470b4d5a5 Expose ApplicationRegistration logo as FileOutput, drop redundant column
- New core FileOutputDTO ({ fileId, label, extension, url }) mirroring the
  workspace-entity FileOutput shape, so application/registration logos no
  longer leak as raw URL strings.
- Drop the redundant `logo` text column: manifest already carries
  application.logoUrl for non-tarball sources, and tarballs use logoFileId
  + FileEntity. Backfill command removed.
- Logo resolvers (Application, ApplicationRegistration regular + admin,
  nested ApplicationRegistrationSummary) all return FileOutput via the
  shared ApplicationRegistrationLogoService.
- Frontend fragments select `logo { fileId label extension url }`;
  consumers read `.logo?.url`.
2026-05-23 13:00:25 +02:00
77603a4102 i18n - docs translations (#20869)
Created by Github action

Co-authored-by: github-actions <github-actions@twenty.com>
2026-05-23 12:47:00 +02:00
martmull cffd782de7 Fix wrong change 2026-05-22 12:14:40 +02:00
martmull 6482824a09 Code review returns 2026-05-22 12:01:03 +02:00
martmull 47386fd578 Code review returns 2026-05-22 12:00:24 +02:00
martmull 6095f872f5 Merge branch 'main' into fix-app-logo 2026-05-22 11:48:28 +02:00
martmull 1b1860b038 Fix missing db update 2026-05-22 09:46:34 +02:00
martmull 393f2f11f7 Restore deleted files 2026-05-22 09:41:04 +02:00
martmull 42875de279 Fix commit with conflicts 2026-05-22 09:34:19 +02:00
martmull db476b2f5c Merge branch 'main' into fix-app-logo 2026-05-22 09:09:04 +02:00
martmull c03d159e06 Merge branch 'main' into fix-app-logo 2026-05-21 20:24:40 +02:00
martmull 91f7fc66db Move to 2.8 2026-05-21 20:22:55 +02:00
martmull 3d096e9825 Remove mjs files 2026-05-21 20:10:46 +02:00
martmull 26d04719d7 Merge branch 'main' into fix-app-logo 2026-05-21 17:48:14 +02:00
martmull 5d0aa872ee Generate 2026-05-21 17:45:29 +02:00
martmull 8c2fa796e4 Remove wrong change 2026-05-21 17:44:40 +02:00
martmull 12685622c9 Fix lint 2026-05-21 17:40:32 +02:00
martmull 1c9b79013c Wrong sourceType display 2026-05-21 17:38:13 +02:00
martmull 116c17ee11 Fix source type when changing source type 2026-05-21 17:34:42 +02:00
martmull 64cfe8b07f Fix logo when tarball publish 2026-05-21 17:29:50 +02:00
martmull 6ed955e89f Init 2026-05-21 16:16:13 +02:00
77 changed files with 1900 additions and 1321 deletions
+3 -18
View File
@@ -1,7 +1,6 @@
name: 'Preview Environment Dispatch'
permissions:
contents: write
permissions: {}
on:
pull_request_target:
@@ -11,7 +10,6 @@ on:
- packages/twenty-server/**
- packages/twenty-front/**
- .github/workflows/preview-env-dispatch.yaml
- .github/workflows/preview-env-keepalive.yaml
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
@@ -35,28 +33,15 @@ jobs:
timeout-minutes: 5
runs-on: ubuntu-latest
steps:
- name: Trigger preview environment workflow
- name: Dispatch preview-env to ci-privileged
env:
GH_TOKEN: ${{ secrets.CI_PRIVILEGED_DISPATCH_TOKEN }}
PR_NUMBER: ${{ github.event.pull_request.number }}
PR_HEAD_SHA: ${{ github.event.pull_request.head.sha }}
REPOSITORY: ${{ github.repository }}
run: |
gh api repos/"$REPOSITORY"/dispatches \
gh api repos/twentyhq/ci-privileged/dispatches \
-f event_type=preview-environment \
-f "client_payload[pr_number]=$PR_NUMBER" \
-f "client_payload[pr_head_sha]=$PR_HEAD_SHA" \
-f "client_payload[repo_full_name]=$REPOSITORY"
- name: Dispatch to ci-privileged for PR comment
env:
GH_TOKEN: ${{ secrets.CI_PRIVILEGED_DISPATCH_TOKEN }}
PR_NUMBER: ${{ github.event.pull_request.number }}
KEEPALIVE_DISPATCH_TIME: ${{ github.event.pull_request.updated_at }}
REPOSITORY: ${{ github.repository }}
run: |
gh api repos/twentyhq/ci-privileged/dispatches \
-f event_type=preview-env-url \
-f "client_payload[pr_number]=$PR_NUMBER" \
-f "client_payload[keepalive_dispatch_time]=$KEEPALIVE_DISPATCH_TIME" \
-f "client_payload[repo]=$REPOSITORY"
@@ -1,186 +0,0 @@
name: 'Preview Environment Keep Alive'
permissions:
contents: read
on:
repository_dispatch:
types: [preview-environment]
jobs:
preview-environment:
timeout-minutes: 310
runs-on: ubuntu-latest
steps:
- name: Checkout PR
uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4.3.1
with:
ref: ${{ github.event.client_payload.pr_head_sha }}
- name: Login to Docker Hub
uses: docker/login-action@c94ce9fb468520275223c153574b00df6fe4bcc9 # v3.7.0
with:
username: ${{ vars.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_PASSWORD }}
- name: Run compose setup
run: |
echo "Patching docker-compose.yml..."
# change image to localbuild using yq
yq eval 'del(.services.server.image)' -i packages/twenty-docker/docker-compose.yml
yq eval '.services.server.build.context = "../../"' -i packages/twenty-docker/docker-compose.yml
yq eval '.services.server.build.dockerfile = "./packages/twenty-docker/twenty/Dockerfile"' -i packages/twenty-docker/docker-compose.yml
yq eval '.services.server.build.target = "twenty"' -i packages/twenty-docker/docker-compose.yml
yq eval 'del(.services.worker.image)' -i packages/twenty-docker/docker-compose.yml
yq eval '.services.worker.build.context = "../../"' -i packages/twenty-docker/docker-compose.yml
yq eval '.services.worker.build.dockerfile = "./packages/twenty-docker/twenty/Dockerfile"' -i packages/twenty-docker/docker-compose.yml
yq eval '.services.worker.build.target = "twenty"' -i packages/twenty-docker/docker-compose.yml
echo "Adding SIGN_IN_PREFILLED environment variable to server service..."
yq eval '.services.server.environment.SIGN_IN_PREFILLED = "${SIGN_IN_PREFILLED}"' -i packages/twenty-docker/docker-compose.yml
echo "Setting up .env file..."
cp packages/twenty-docker/.env.example packages/twenty-docker/.env
echo "Generating secrets..."
echo "" >> packages/twenty-docker/.env
echo "# === Randomly generated secrets ===" >> packages/twenty-docker/.env
echo "APP_SECRET=$(openssl rand -base64 32)" >> packages/twenty-docker/.env
echo "PG_DATABASE_PASSWORD=$(openssl rand -hex 16)" >> packages/twenty-docker/.env
echo "SIGN_IN_PREFILLED=true" >> packages/twenty-docker/.env
echo "Docker compose build..."
cd packages/twenty-docker/
docker compose build
working-directory: ./
- name: Create Tunnel
id: expose-tunnel
env:
CLOUDFLARED_VERSION: '2026.3.0'
run: |
set -euo pipefail
# Install cloudflared (pinned for reproducibility)
sudo curl -fsSL -o /usr/local/bin/cloudflared \
"https://github.com/cloudflare/cloudflared/releases/download/${CLOUDFLARED_VERSION}/cloudflared-linux-amd64"
sudo chmod +x /usr/local/bin/cloudflared
cloudflared --version
# Start an account-less "quick tunnel" pointing at the server container.
# Cloudflare prints the assigned https://*.trycloudflare.com URL into the log.
log_file="$RUNNER_TEMP/cloudflared.log"
: > "$log_file"
cloudflared tunnel \
--url http://localhost:3000 \
--no-autoupdate \
--logfile "$log_file" \
--loglevel info \
> "$RUNNER_TEMP/cloudflared.stdout" 2>&1 &
pid=$!
echo "$pid" > "$RUNNER_TEMP/cloudflared.pid"
echo "cloudflared PID: $pid"
# Wait up to 2 minutes for the URL to appear; fail fast if cloudflared exits.
url=''
for _ in $(seq 1 60); do
url=$(grep -oE 'https://[a-zA-Z0-9-]+\.trycloudflare\.com' "$log_file" 2>/dev/null | head -n1 || true)
[ -n "$url" ] && break
if ! kill -0 "$pid" 2>/dev/null; then
echo "cloudflared exited before producing a URL"
cat "$log_file" || true
exit 1
fi
sleep 2
done
if [ -z "$url" ]; then
echo "Timed out waiting for tunnel URL"
cat "$log_file" || true
exit 1
fi
echo "Tunnel URL: $url"
echo "tunnel-url=$url" >> "$GITHUB_OUTPUT"
- name: Start services with correct SERVER_URL
env:
TUNNEL_URL: ${{ steps.expose-tunnel.outputs.tunnel-url }}
run: |
cd packages/twenty-docker/
echo "Setting SERVER_URL to $TUNNEL_URL"
sed -i '/SERVER_URL=/d' .env
echo "" >> .env
echo "SERVER_URL=$TUNNEL_URL" >> .env
# Start the services
echo "Docker compose up..."
docker compose up -d || {
echo "Docker compose failed to start"
docker compose logs
exit 1
}
echo "Waiting for services to be ready..."
count=0
while [ ! $(docker inspect --format='{{.State.Health.Status}}' twenty-db-1) = "healthy" ] || [ ! $(docker inspect --format='{{.State.Health.Status}}' twenty-server-1) = "healthy" ]; do
sleep 5
count=$((count+1))
if [ $count -gt 60 ]; then
echo "Timeout waiting for services to be ready"
docker compose logs
exit 1
fi
echo "Still waiting for services... ($count/60)"
done
echo "All services are up and running!"
working-directory: ./
- name: Seed Dev Workspace
run: |
cd packages/twenty-docker/
echo "Seeding light dev workspace (Apple only)..."
if ! docker compose exec -T server yarn command:prod workspace:seed:dev --light; then
echo "❌ Seeding light dev workspace failed. Dumping server logs..."
docker compose logs server
exit 1
fi
working-directory: ./
- name: Output tunnel URL
env:
TUNNEL_URL: ${{ steps.expose-tunnel.outputs.tunnel-url }}
run: |
echo "✅ Preview Environment Ready!"
echo "🔗 Preview URL: $TUNNEL_URL"
echo "⏱️ This environment will be available for 5 hours"
echo "## 🚀 Preview Environment Ready!" >> "$GITHUB_STEP_SUMMARY"
echo "" >> "$GITHUB_STEP_SUMMARY"
echo "Preview URL: $TUNNEL_URL" >> "$GITHUB_STEP_SUMMARY"
echo "" >> "$GITHUB_STEP_SUMMARY"
echo "This environment will automatically shut down after 5 hours." >> "$GITHUB_STEP_SUMMARY"
echo "$TUNNEL_URL" > tunnel-url.txt
- name: Upload tunnel URL artifact
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2
with:
name: tunnel-url
path: tunnel-url.txt
retention-days: 1
- name: Keep tunnel alive for 5 hours
run: timeout 300m sleep 18000 # Stop on whichever we reach first (300m or 5hour sleep)
- name: Cleanup
if: always()
run: |
if [ -f "$RUNNER_TEMP/cloudflared.pid" ]; then
kill "$(cat "$RUNNER_TEMP/cloudflared.pid")" 2>/dev/null || true
fi
cd packages/twenty-docker/
docker compose down -v
working-directory: ./
@@ -48,9 +48,9 @@ type ApplicationRegistration {
isListed: Boolean!
isFeatured: Boolean!
isPreInstalled: Boolean!
logoUrl: String
createdAt: DateTime!
updatedAt: DateTime!
logo: FileOutput
isConfigured: Boolean!
}
@@ -283,11 +283,18 @@ type Role {
rowLevelPermissionPredicateGroups: [RowLevelPermissionPredicateGroup!]
}
type FileOutput {
fileId: UUID
label: String
extension: String
url: String!
}
type ApplicationRegistrationSummary {
id: UUID!
latestAvailableVersion: String
sourceType: ApplicationRegistrationSourceType!
logoUrl: String
logo: FileOutput
}
type ApplicationVariable {
@@ -691,7 +698,6 @@ type Application {
id: UUID!
name: String!
description: String
logo: String
version: String
universalIdentifier: String!
packageJsonChecksum: String
@@ -711,6 +717,7 @@ type Application {
objects: [Object!]!
applicationVariables: [ApplicationVariable!]!
applicationRegistration: ApplicationRegistrationSummary
logo: FileOutput
}
type ViewField {
@@ -1971,7 +1978,7 @@ type CreateApplicationRegistration {
type PublicApplicationRegistration {
id: UUID!
name: String!
logoUrl: String
logo: FileOutput
websiteUrl: String
oAuthScopes: [String!]!
}
@@ -2357,6 +2364,7 @@ type MarketplaceAppDetail {
sourceType: ApplicationRegistrationSourceType!
sourcePackage: String
latestAvailableVersion: String
logo: FileOutput
isListed: Boolean!
isFeatured: Boolean!
manifest: JSON
@@ -52,9 +52,9 @@ export interface ApplicationRegistration {
isListed: Scalars['Boolean']
isFeatured: Scalars['Boolean']
isPreInstalled: Scalars['Boolean']
logoUrl?: Scalars['String']
createdAt: Scalars['DateTime']
updatedAt: Scalars['DateTime']
logo?: FileOutput
isConfigured: Scalars['Boolean']
__typename: 'ApplicationRegistration'
}
@@ -232,11 +232,19 @@ export interface Role {
__typename: 'Role'
}
export interface FileOutput {
fileId?: Scalars['UUID']
label?: Scalars['String']
extension?: Scalars['String']
url: Scalars['String']
__typename: 'FileOutput'
}
export interface ApplicationRegistrationSummary {
id: Scalars['UUID']
latestAvailableVersion?: Scalars['String']
sourceType: ApplicationRegistrationSourceType
logoUrl?: Scalars['String']
logo?: FileOutput
__typename: 'ApplicationRegistrationSummary'
}
@@ -456,7 +464,6 @@ export interface Application {
id: Scalars['UUID']
name: Scalars['String']
description?: Scalars['String']
logo?: Scalars['String']
version?: Scalars['String']
universalIdentifier: Scalars['String']
packageJsonChecksum?: Scalars['String']
@@ -476,6 +483,7 @@ export interface Application {
objects: Object[]
applicationVariables: ApplicationVariable[]
applicationRegistration?: ApplicationRegistrationSummary
logo?: FileOutput
__typename: 'Application'
}
@@ -1611,7 +1619,7 @@ export interface CreateApplicationRegistration {
export interface PublicApplicationRegistration {
id: Scalars['UUID']
name: Scalars['String']
logoUrl?: Scalars['String']
logo?: FileOutput
websiteUrl?: Scalars['String']
oAuthScopes: Scalars['String'][]
__typename: 'PublicApplicationRegistration'
@@ -2034,6 +2042,7 @@ export interface MarketplaceAppDetail {
sourceType: ApplicationRegistrationSourceType
sourcePackage?: Scalars['String']
latestAvailableVersion?: Scalars['String']
logo?: FileOutput
isListed: Scalars['Boolean']
isFeatured: Scalars['Boolean']
manifest?: Scalars['JSON']
@@ -2923,9 +2932,9 @@ export interface ApplicationRegistrationGenqlSelection{
isListed?: boolean | number
isFeatured?: boolean | number
isPreInstalled?: boolean | number
logoUrl?: boolean | number
createdAt?: boolean | number
updatedAt?: boolean | number
logo?: FileOutputGenqlSelection
isConfigured?: boolean | number
__typename?: boolean | number
__scalar?: boolean | number
@@ -3096,11 +3105,20 @@ export interface RoleGenqlSelection{
__scalar?: boolean | number
}
export interface FileOutputGenqlSelection{
fileId?: boolean | number
label?: boolean | number
extension?: boolean | number
url?: boolean | number
__typename?: boolean | number
__scalar?: boolean | number
}
export interface ApplicationRegistrationSummaryGenqlSelection{
id?: boolean | number
latestAvailableVersion?: boolean | number
sourceType?: boolean | number
logoUrl?: boolean | number
logo?: FileOutputGenqlSelection
__typename?: boolean | number
__scalar?: boolean | number
}
@@ -3365,7 +3383,6 @@ export interface ApplicationGenqlSelection{
id?: boolean | number
name?: boolean | number
description?: boolean | number
logo?: boolean | number
version?: boolean | number
universalIdentifier?: boolean | number
packageJsonChecksum?: boolean | number
@@ -3385,6 +3402,7 @@ export interface ApplicationGenqlSelection{
objects?: ObjectGenqlSelection
applicationVariables?: ApplicationVariableGenqlSelection
applicationRegistration?: ApplicationRegistrationSummaryGenqlSelection
logo?: FileOutputGenqlSelection
__typename?: boolean | number
__scalar?: boolean | number
}
@@ -4555,7 +4573,7 @@ export interface CreateApplicationRegistrationGenqlSelection{
export interface PublicApplicationRegistrationGenqlSelection{
id?: boolean | number
name?: boolean | number
logoUrl?: boolean | number
logo?: FileOutputGenqlSelection
websiteUrl?: boolean | number
oAuthScopes?: boolean | number
__typename?: boolean | number
@@ -5030,6 +5048,7 @@ export interface MarketplaceAppDetailGenqlSelection{
sourceType?: boolean | number
sourcePackage?: boolean | number
latestAvailableVersion?: boolean | number
logo?: FileOutputGenqlSelection
isListed?: boolean | number
isFeatured?: boolean | number
manifest?: boolean | number
@@ -6409,6 +6428,14 @@ export interface LogicFunctionLogsInput {applicationId?: (Scalars['UUID'] | null
const FileOutput_possibleTypes: string[] = ['FileOutput']
export const isFileOutput = (obj?: { __typename?: any } | null): obj is FileOutput => {
if (!obj?.__typename) throw new Error('__typename is missing in "isFileOutput"')
return FileOutput_possibleTypes.includes(obj.__typename)
}
const ApplicationRegistrationSummary_possibleTypes: string[] = ['ApplicationRegistrationSummary']
export const isApplicationRegistrationSummary = (obj?: { __typename?: any } | null): obj is ApplicationRegistrationSummary => {
if (!obj?.__typename) throw new Error('__typename is missing in "isApplicationRegistrationSummary"')
File diff suppressed because it is too large Load Diff
@@ -16,10 +16,9 @@ Jeder Schlüssel enthält einen `publicKey` (unbefristet aufbewahrt, damit er zu
### Aktuellen Schlüssel rotieren
* **Manuell** — **Settings → Admin Panel → Signing keys → Revoke** in der aktuellen Zeile. Das Widerrufen löscht das verschlüsselte private Material und stuft es herab; der nächste Signieraufruf erstellt automatisch ein neues ES256-Schlüsselpaar als neuen aktuellen Schlüssel. Tokens, die unter einem anderen (nicht widerrufenen) `kid` signiert wurden, werden weiterhin verifiziert, bis sie ablaufen.
* **Enterprise (automatisch)** — ein täglicher Cron-Job (`'15 3 * * *'` UTC) erstellt einen neuen aktuellen Schlüssel, sobald der vorhandene Schlüssel seit `SIGNING_KEY_ROTATION_DAYS` (Standard `90`) aktuell ist. Der vorherige Schlüssel wird *nicht* widerrufen, daher werden Tokens, die darunter signiert wurden, weiterhin verifiziert. Registriere ihn einmalig mit `yarn command:prod cron:register:all`.
Setzen Sie `SIGNING_KEY_ROTATION_DAYS`, um die Funktion zu aktivieren: Ein täglicher Cronjob erstellt dann einen neuen aktuellen Schlüssel, sobald der bestehende Schlüssel älter als dieser Schwellenwert ist. Frühere Schlüssel werden *nicht* widerrufen, daher werden Tokens, die darunter signiert wurden, weiterhin verifiziert. Lassen Sie die Variable ungesetzt, um die automatische Rotation zu deaktivieren.
<Note>Der Enterprise-Cron-Job und `SIGNING_KEY_ROTATION_DAYS` sind ab v2.6+ enthalten.</Note>
<Note>Die automatische Rotation ist ab Version v2.6 verfügbar.</Note>
### Einen Schlüssel widerrufen (nur bei Leak / Notfall)
@@ -16,10 +16,9 @@ Cada chave carrega uma `publicKey` (mantida indefinidamente para que possa verif
### Rotacionar a chave atual
* **Manual** — **Settings → Admin Panel → Signing keys → Revoke** na linha atual. A revogação apaga o material privado criptografado e o rebaixa; a próxima chamada de assinatura gera automaticamente um novo par de chaves ES256 como o novo atual. Tokens assinados sob qualquer outro `kid` (não revogado) continuam sendo verificados até expirarem.
* **Enterprise (automático)** — um cron diário (`'15 3 * * *'` UTC) emite uma nova chave atual assim que a existente tiver sido atual por `SIGNING_KEY_ROTATION_DAYS` (padrão `90`). A chave anterior *não* é revogada, então tokens assinados sob ela continuam sendo verificados. Registre-o uma vez com `yarn command:prod cron:register:all`.
Defina `SIGNING_KEY_ROTATION_DAYS` para ativar: um cron diário então gera uma nova chave e a define como atual quando a existente for mais antiga do que esse limite. Chaves anteriores *não* são revogadas, então tokens assinados sob elas continuam sendo verificados. Deixe a variável não definida para desativar a rotação automática.
<Note>O cron do Enterprise e `SIGNING_KEY_ROTATION_DAYS` são disponibilizados a partir da v2.6+.</Note>
<Note>A rotação automática está disponível a partir da v2.6+.</Note>
### Revogar uma chave (apenas vazamento / emergência)
@@ -16,10 +16,9 @@ Fiecare cheie are un `publicKey` (păstrat pe termen nelimitat pentru a putea ve
### Rotește cheia curentă
* **Manual** — **Settings → Admin Panel → Signing keys → Revoke** pe rândul curent. Revocarea șterge materialul privat criptat și o retrogradează; următorul apel de semnare generează automat o nouă pereche de chei ES256 ca noua cheie curentă. Token-urile semnate sub orice alt `kid` (nerevocat) continuă să fie verificate până la expirare.
* **Enterprise (automat)** — un cron zilnic (`'15 3 * * *'` UTC) emite o nouă cheie curentă după ce cea existentă a fost curentă timp de `SIGNING_KEY_ROTATION_DAYS` (implicit `90`). Cheia anterioară *nu* este revocată, astfel încât token-urile semnate cu ea continuă să fie verificate. Înregistrează-l o singură dată cu `yarn command:prod cron:register:all`.
Setați `SIGNING_KEY_ROTATION_DAYS` pentru a activa rotația automată: un job cron zilnic va emite o nouă cheie curentă odată ce cea existentă este mai veche decât acest prag. Cheile anterioare *nu* sunt revocate, astfel încât token-urile semnate cu ele continuă să fie verificate. Lăsați variabila nesetată pentru a dezactiva rotația automată.
<Note>Cron-ul Enterprise și `SIGNING_KEY_ROTATION_DAYS` sunt disponibile în v2.6+.</Note>
<Note>Rotația automată este disponibilă începând cu versiunea v2.6+.</Note>
### Revocă o cheie (numai în caz de scurgere / urgență)
@@ -16,10 +16,9 @@ icon: rotate
### Выполнить ротацию текущего ключа
* **Вручную** — **Settings → Admin Panel → Signing keys → Revoke** в текущей строке. Отзыв стирает его зашифрованный закрытый материал и понижает его; следующий вызов подписи автоматически создает новую пару ключей ES256 как новый текущий ключ. Токены, подписанные любым другим (не отозванным) `kid`, продолжают успешно проходить проверку до истечения срока действия.
* **Enterprise (автоматически)** — ежедневный cron (`'15 3 * * *'` UTC) выпускает новый текущий ключ, как только существующий ключ находится в статусе текущего в течение `SIGNING_KEY_ROTATION_DAYS` (по умолчанию `90`). Предыдущий ключ *не* отзывается, поэтому токены, подписанные им, продолжают успешно проходить проверку. Зарегистрируйте его один раз с помощью `yarn command:prod cron:register:all`.
Установите `SIGNING_KEY_ROTATION_DAYS`, чтобы включить эту опцию: ежедневный cron затем будет выпускать новый текущий ключ, как только существующий ключ станет старше этого порогового значения. Предыдущие ключи *не* отзываются, поэтому токены, подписанные ими, продолжают успешно проходить проверку. Оставьте переменную не заданной, чтобы отключить автоматическую ротацию.
<Note>Enterprise-cron и `SIGNING_KEY_ROTATION_DAYS` поставляются начиная с v2.6+.</Note>
<Note>Автоматическая ротация доступна, начиная с версии v2.6+.</Note>
### Отозвать ключ (только при утечке / в экстренных случаях)
@@ -16,10 +16,9 @@ Her anahtar bir `publicKey` (önceden verilmiş belirteçleri doğrulayabilmesi
### Geçerli anahtarı döndür
* **El ile** — geçerli satırda **Settings → Admin Panel → Signing keys → Revoke**. İptal etmek, şifrelenmiş özel materyalini siler ve onu geçerli olmaktan çıkarır; bir sonraki imzalama çağrısı, yeni geçerli olarak otomatik olarak yeni bir ES256 anahtar çifti oluşturur. Herhangi başka bir (iptal edilmemiş) `kid` ile imzalanan belirteçler, süreleri dolana kadar doğrulanmaya devam eder.
* **Kurumsal (otomatik)** — günlük bir cron (`'15 3 * * *'` UTC), mevcut anahtar `SIGNING_KEY_ROTATION_DAYS` (varsayılan `90`) kadar süredir geçerli olduğunda yeni bir geçerli anahtar verir. Önceki anahtar *iptal edilmez*, bu nedenle onunla imzalanan belirteçler doğrulanmaya devam eder. `yarn command:prod cron:register:all` ile bir kez kaydedin.
Etkinleştirmek için `SIGNING_KEY_ROTATION_DAYS` değerini ayarlayın: günlük bir cron görevi, mevcut anahtar bu eşikten daha eski olduğunda yeni bir güncel anahtar oluşturur. Önceki anahtarlar *iptal edilmez*, bu nedenle onlarla imzalanmış belirteçler doğrulanmaya devam eder. Otomatik döndürmeyi devre dışı bırakmak için değişkeni ayarlamadan bırakın.
<Note>Kurumsal cron ve `SIGNING_KEY_ROTATION_DAYS` v2.6+ ile birlikte gelir.</Note>
<Note>Otomatik döndürme v2.6+ ile birlikte gelir.</Note>
### Bir anahtarı iptal et (yalnızca sızıntı / acil durum için)
@@ -16,10 +16,9 @@ Twenty 具有两个相互独立的密钥族:
### 轮换当前密钥
* **手动** — 在当前行上执行 **Settings → Admin Panel → Signing keys → Revoke**。 吊销操作会清除其加密的私有材料并将其降级;下一次签名调用会自动生成一个新的 ES256 密钥对,作为新的当前密钥。 在任何其他(未被吊销的)`kid` 下签名的令牌会一直验证,直到过期
* **Enterprise(自动)** — 每日 cron`'15 3 * * *'` UTC)会在现有密钥已作为当前密钥使用 `SIGNING_KEY_ROTATION_DAYS`(默认 `90`)后签发一个新的当前密钥。 先前的密钥*不会*被吊销,因此在其下签名的令牌仍然可以通过验证。 使用 `yarn command:prod cron:register:all` 注册一次。
将 `SIGNING_KEY_ROTATION_DAYS` 设为启用该功能:然后每日的 cron 任务会在现有密钥超过该阈值后签发新的当前密钥。 先前的密钥*不会*被吊销,因此在其下签名的令牌仍然可以通过验证。 将变量保持未设置以禁用自动轮换
<Note>Enterprise 的 cron 和 `SIGNING_KEY_ROTATION_DAYS` 从 v2.6+ 版本开始提供。</Note>
<Note>自动轮换在 v2.6+ 提供。</Note>
### 吊销密钥(仅限泄露 / 紧急情况)
@@ -176,7 +176,7 @@ export type ApplicationRegistration = {
isListed: Scalars['Boolean'];
isPreInstalled: Scalars['Boolean'];
latestAvailableVersion?: Maybe<Scalars['String']>;
logoUrl?: Maybe<Scalars['String']>;
logo?: Maybe<FileOutput>;
name: Scalars['String'];
oAuthClientId: Scalars['String'];
oAuthRedirectUris: Array<Scalars['String']>;
@@ -293,6 +293,14 @@ export enum FeatureFlagKey {
IS_UNIQUE_INDEXES_ENABLED = 'IS_UNIQUE_INDEXES_ENABLED'
}
export type FileOutput = {
__typename?: 'FileOutput';
extension?: Maybe<Scalars['String']>;
fileId?: Maybe<Scalars['UUID']>;
label?: Maybe<Scalars['String']>;
url: Scalars['String'];
};
export enum HealthIndicatorId {
app = 'app',
connectedAccount = 'connectedAccount',
@@ -933,7 +941,7 @@ export type FindAdminApplicationRegistrationVariablesQuery = { __typename?: 'Que
export type FindAllApplicationRegistrationsQueryVariables = Exact<{ [key: string]: never; }>;
export type FindAllApplicationRegistrationsQuery = { __typename?: 'Query', findAllApplicationRegistrations: Array<{ __typename?: 'ApplicationRegistration', id: string, universalIdentifier: string, name: string, logoUrl?: string | null, oAuthClientId: string, oAuthRedirectUris: Array<string>, oAuthScopes: Array<string>, sourceType: ApplicationRegistrationSourceType, sourcePackage?: string | null, latestAvailableVersion?: string | null, isListed: boolean, isFeatured: boolean, isPreInstalled: boolean, isConfigured: boolean, ownerWorkspaceId?: string | null, createdAt: string, updatedAt: string }> };
export type FindAllApplicationRegistrationsQuery = { __typename?: 'Query', findAllApplicationRegistrations: Array<{ __typename?: 'ApplicationRegistration', id: string, universalIdentifier: string, name: string, oAuthClientId: string, oAuthRedirectUris: Array<string>, oAuthScopes: Array<string>, sourceType: ApplicationRegistrationSourceType, sourcePackage?: string | null, latestAvailableVersion?: string | null, isListed: boolean, isFeatured: boolean, isPreInstalled: boolean, isConfigured: boolean, ownerWorkspaceId?: string | null, createdAt: string, updatedAt: string, logo?: { __typename?: 'FileOutput', fileId?: string | null, label?: string | null, extension?: string | null, url: string } | null }> };
export type CreateDatabaseConfigVariableMutationVariables = Exact<{
key: Scalars['String'];
@@ -1000,7 +1008,7 @@ export type FindOneAdminApplicationRegistrationQueryVariables = Exact<{
}>;
export type FindOneAdminApplicationRegistrationQuery = { __typename?: 'Query', findOneAdminApplicationRegistration: { __typename?: 'ApplicationRegistration', id: string, universalIdentifier: string, name: string, logoUrl?: string | null, oAuthClientId: string, oAuthRedirectUris: Array<string>, oAuthScopes: Array<string>, sourceType: ApplicationRegistrationSourceType, sourcePackage?: string | null, latestAvailableVersion?: string | null, isListed: boolean, isFeatured: boolean, isPreInstalled: boolean, isConfigured: boolean, ownerWorkspaceId?: string | null, createdAt: string, updatedAt: string } };
export type FindOneAdminApplicationRegistrationQuery = { __typename?: 'Query', findOneAdminApplicationRegistration: { __typename?: 'ApplicationRegistration', id: string, universalIdentifier: string, name: string, oAuthClientId: string, oAuthRedirectUris: Array<string>, oAuthScopes: Array<string>, sourceType: ApplicationRegistrationSourceType, sourcePackage?: string | null, latestAvailableVersion?: string | null, isListed: boolean, isFeatured: boolean, isPreInstalled: boolean, isConfigured: boolean, ownerWorkspaceId?: string | null, createdAt: string, updatedAt: string, logo?: { __typename?: 'FileOutput', fileId?: string | null, label?: string | null, extension?: string | null, url: string } | null } };
export type GetAdminChatThreadMessagesQueryVariables = Exact<{
threadId: Scalars['UUID'];
@@ -1136,10 +1144,10 @@ export type GetSigningKeysQueryVariables = Exact<{ [key: string]: never; }>;
export type GetSigningKeysQuery = { __typename?: 'Query', getSigningKeys: { __typename?: 'SigningKeysAdminPanelDTO', legacyVerifyCountInWindow: number, verifyWindowDays: number, signingKeys: Array<{ __typename?: 'SigningKeyDTO', id: string, publicKey: string, isCurrent: boolean, createdAt: string, revokedAt?: string | null, verifyCountInWindow: number }> } };
export type ApplicationRegistrationFragmentFragment = { __typename?: 'ApplicationRegistration', id: string, universalIdentifier: string, name: string, logoUrl?: string | null, oAuthClientId: string, oAuthRedirectUris: Array<string>, oAuthScopes: Array<string>, sourceType: ApplicationRegistrationSourceType, sourcePackage?: string | null, latestAvailableVersion?: string | null, isListed: boolean, isFeatured: boolean, isPreInstalled: boolean, isConfigured: boolean, ownerWorkspaceId?: string | null, createdAt: string, updatedAt: string };
export type ApplicationRegistrationFragmentFragment = { __typename?: 'ApplicationRegistration', id: string, universalIdentifier: string, name: string, oAuthClientId: string, oAuthRedirectUris: Array<string>, oAuthScopes: Array<string>, sourceType: ApplicationRegistrationSourceType, sourcePackage?: string | null, latestAvailableVersion?: string | null, isListed: boolean, isFeatured: boolean, isPreInstalled: boolean, isConfigured: boolean, ownerWorkspaceId?: string | null, createdAt: string, updatedAt: string, logo?: { __typename?: 'FileOutput', fileId?: string | null, label?: string | null, extension?: string | null, url: string } | null };
export const UserInfoFragmentFragmentDoc = {"kind":"Document","definitions":[{"kind":"FragmentDefinition","name":{"kind":"Name","value":"UserInfoFragment"},"typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"UserInfo"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"email"}},{"kind":"Field","name":{"kind":"Name","value":"firstName"}},{"kind":"Field","name":{"kind":"Name","value":"lastName"}},{"kind":"Field","name":{"kind":"Name","value":"createdAt"}}]}}]} as unknown as DocumentNode<UserInfoFragmentFragment, unknown>;
export const ApplicationRegistrationFragmentFragmentDoc = {"kind":"Document","definitions":[{"kind":"FragmentDefinition","name":{"kind":"Name","value":"ApplicationRegistrationFragment"},"typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"ApplicationRegistration"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"universalIdentifier"}},{"kind":"Field","name":{"kind":"Name","value":"name"}},{"kind":"Field","name":{"kind":"Name","value":"logoUrl"}},{"kind":"Field","name":{"kind":"Name","value":"oAuthClientId"}},{"kind":"Field","name":{"kind":"Name","value":"oAuthRedirectUris"}},{"kind":"Field","name":{"kind":"Name","value":"oAuthScopes"}},{"kind":"Field","name":{"kind":"Name","value":"sourceType"}},{"kind":"Field","name":{"kind":"Name","value":"sourcePackage"}},{"kind":"Field","name":{"kind":"Name","value":"latestAvailableVersion"}},{"kind":"Field","name":{"kind":"Name","value":"isListed"}},{"kind":"Field","name":{"kind":"Name","value":"isFeatured"}},{"kind":"Field","name":{"kind":"Name","value":"isPreInstalled"}},{"kind":"Field","name":{"kind":"Name","value":"isConfigured"}},{"kind":"Field","name":{"kind":"Name","value":"ownerWorkspaceId"}},{"kind":"Field","name":{"kind":"Name","value":"createdAt"}},{"kind":"Field","name":{"kind":"Name","value":"updatedAt"}}]}}]} as unknown as DocumentNode<ApplicationRegistrationFragmentFragment, unknown>;
export const ApplicationRegistrationFragmentFragmentDoc = {"kind":"Document","definitions":[{"kind":"FragmentDefinition","name":{"kind":"Name","value":"ApplicationRegistrationFragment"},"typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"ApplicationRegistration"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"universalIdentifier"}},{"kind":"Field","name":{"kind":"Name","value":"name"}},{"kind":"Field","name":{"kind":"Name","value":"logo"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"fileId"}},{"kind":"Field","name":{"kind":"Name","value":"label"}},{"kind":"Field","name":{"kind":"Name","value":"extension"}},{"kind":"Field","name":{"kind":"Name","value":"url"}}]}},{"kind":"Field","name":{"kind":"Name","value":"oAuthClientId"}},{"kind":"Field","name":{"kind":"Name","value":"oAuthRedirectUris"}},{"kind":"Field","name":{"kind":"Name","value":"oAuthScopes"}},{"kind":"Field","name":{"kind":"Name","value":"sourceType"}},{"kind":"Field","name":{"kind":"Name","value":"sourcePackage"}},{"kind":"Field","name":{"kind":"Name","value":"latestAvailableVersion"}},{"kind":"Field","name":{"kind":"Name","value":"isListed"}},{"kind":"Field","name":{"kind":"Name","value":"isFeatured"}},{"kind":"Field","name":{"kind":"Name","value":"isPreInstalled"}},{"kind":"Field","name":{"kind":"Name","value":"isConfigured"}},{"kind":"Field","name":{"kind":"Name","value":"ownerWorkspaceId"}},{"kind":"Field","name":{"kind":"Name","value":"createdAt"}},{"kind":"Field","name":{"kind":"Name","value":"updatedAt"}}]}}]} as unknown as DocumentNode<ApplicationRegistrationFragmentFragment, unknown>;
export const AddAiProviderDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"mutation","name":{"kind":"Name","value":"AddAiProvider"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"providerName"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"String"}}}},{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"providerConfig"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"JSON"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"addAiProvider"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"providerName"},"value":{"kind":"Variable","name":{"kind":"Name","value":"providerName"}}},{"kind":"Argument","name":{"kind":"Name","value":"providerConfig"},"value":{"kind":"Variable","name":{"kind":"Name","value":"providerConfig"}}}]}]}}]} as unknown as DocumentNode<AddAiProviderMutation, AddAiProviderMutationVariables>;
export const AddModelToProviderDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"mutation","name":{"kind":"Name","value":"AddModelToProvider"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"providerName"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"String"}}}},{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"modelConfig"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"JSON"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"addModelToProvider"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"providerName"},"value":{"kind":"Variable","name":{"kind":"Name","value":"providerName"}}},{"kind":"Argument","name":{"kind":"Name","value":"modelConfig"},"value":{"kind":"Variable","name":{"kind":"Name","value":"modelConfig"}}}]}]}}]} as unknown as DocumentNode<AddModelToProviderMutation, AddModelToProviderMutationVariables>;
export const RemoveAiProviderDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"mutation","name":{"kind":"Name","value":"RemoveAiProvider"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"providerName"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"String"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"removeAiProvider"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"providerName"},"value":{"kind":"Variable","name":{"kind":"Name","value":"providerName"}}}]}]}}]} as unknown as DocumentNode<RemoveAiProviderMutation, RemoveAiProviderMutationVariables>;
@@ -1155,7 +1163,7 @@ export const GetAiProvidersDocument = {"kind":"Document","definitions":[{"kind":
export const GetModelsDevProvidersDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"query","name":{"kind":"Name","value":"GetModelsDevProviders"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"getModelsDevProviders"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"modelCount"}},{"kind":"Field","name":{"kind":"Name","value":"npm"}}]}}]}}]} as unknown as DocumentNode<GetModelsDevProvidersQuery, GetModelsDevProvidersQueryVariables>;
export const GetModelsDevSuggestionsDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"query","name":{"kind":"Name","value":"GetModelsDevSuggestions"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"providerType"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"String"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"getModelsDevSuggestions"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"providerType"},"value":{"kind":"Variable","name":{"kind":"Name","value":"providerType"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"modelId"}},{"kind":"Field","name":{"kind":"Name","value":"name"}},{"kind":"Field","name":{"kind":"Name","value":"inputCostPerMillionTokens"}},{"kind":"Field","name":{"kind":"Name","value":"outputCostPerMillionTokens"}},{"kind":"Field","name":{"kind":"Name","value":"cachedInputCostPerMillionTokens"}},{"kind":"Field","name":{"kind":"Name","value":"cacheCreationCostPerMillionTokens"}},{"kind":"Field","name":{"kind":"Name","value":"contextWindowTokens"}},{"kind":"Field","name":{"kind":"Name","value":"maxOutputTokens"}},{"kind":"Field","name":{"kind":"Name","value":"modalities"}},{"kind":"Field","name":{"kind":"Name","value":"supportsReasoning"}}]}}]}}]} as unknown as DocumentNode<GetModelsDevSuggestionsQuery, GetModelsDevSuggestionsQueryVariables>;
export const FindAdminApplicationRegistrationVariablesDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"query","name":{"kind":"Name","value":"FindAdminApplicationRegistrationVariables"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"applicationRegistrationId"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"String"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"findAdminApplicationRegistrationVariables"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"applicationRegistrationId"},"value":{"kind":"Variable","name":{"kind":"Name","value":"applicationRegistrationId"}}}],"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":"description"}},{"kind":"Field","name":{"kind":"Name","value":"isSecret"}},{"kind":"Field","name":{"kind":"Name","value":"isRequired"}},{"kind":"Field","name":{"kind":"Name","value":"isFilled"}},{"kind":"Field","name":{"kind":"Name","value":"createdAt"}},{"kind":"Field","name":{"kind":"Name","value":"updatedAt"}}]}}]}}]} as unknown as DocumentNode<FindAdminApplicationRegistrationVariablesQuery, FindAdminApplicationRegistrationVariablesQueryVariables>;
export const FindAllApplicationRegistrationsDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"query","name":{"kind":"Name","value":"FindAllApplicationRegistrations"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"findAllApplicationRegistrations"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"FragmentSpread","name":{"kind":"Name","value":"ApplicationRegistrationFragment"}}]}}]}},{"kind":"FragmentDefinition","name":{"kind":"Name","value":"ApplicationRegistrationFragment"},"typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"ApplicationRegistration"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"universalIdentifier"}},{"kind":"Field","name":{"kind":"Name","value":"name"}},{"kind":"Field","name":{"kind":"Name","value":"logoUrl"}},{"kind":"Field","name":{"kind":"Name","value":"oAuthClientId"}},{"kind":"Field","name":{"kind":"Name","value":"oAuthRedirectUris"}},{"kind":"Field","name":{"kind":"Name","value":"oAuthScopes"}},{"kind":"Field","name":{"kind":"Name","value":"sourceType"}},{"kind":"Field","name":{"kind":"Name","value":"sourcePackage"}},{"kind":"Field","name":{"kind":"Name","value":"latestAvailableVersion"}},{"kind":"Field","name":{"kind":"Name","value":"isListed"}},{"kind":"Field","name":{"kind":"Name","value":"isFeatured"}},{"kind":"Field","name":{"kind":"Name","value":"isPreInstalled"}},{"kind":"Field","name":{"kind":"Name","value":"isConfigured"}},{"kind":"Field","name":{"kind":"Name","value":"ownerWorkspaceId"}},{"kind":"Field","name":{"kind":"Name","value":"createdAt"}},{"kind":"Field","name":{"kind":"Name","value":"updatedAt"}}]}}]} as unknown as DocumentNode<FindAllApplicationRegistrationsQuery, FindAllApplicationRegistrationsQueryVariables>;
export const FindAllApplicationRegistrationsDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"query","name":{"kind":"Name","value":"FindAllApplicationRegistrations"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"findAllApplicationRegistrations"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"FragmentSpread","name":{"kind":"Name","value":"ApplicationRegistrationFragment"}}]}}]}},{"kind":"FragmentDefinition","name":{"kind":"Name","value":"ApplicationRegistrationFragment"},"typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"ApplicationRegistration"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"universalIdentifier"}},{"kind":"Field","name":{"kind":"Name","value":"name"}},{"kind":"Field","name":{"kind":"Name","value":"logo"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"fileId"}},{"kind":"Field","name":{"kind":"Name","value":"label"}},{"kind":"Field","name":{"kind":"Name","value":"extension"}},{"kind":"Field","name":{"kind":"Name","value":"url"}}]}},{"kind":"Field","name":{"kind":"Name","value":"oAuthClientId"}},{"kind":"Field","name":{"kind":"Name","value":"oAuthRedirectUris"}},{"kind":"Field","name":{"kind":"Name","value":"oAuthScopes"}},{"kind":"Field","name":{"kind":"Name","value":"sourceType"}},{"kind":"Field","name":{"kind":"Name","value":"sourcePackage"}},{"kind":"Field","name":{"kind":"Name","value":"latestAvailableVersion"}},{"kind":"Field","name":{"kind":"Name","value":"isListed"}},{"kind":"Field","name":{"kind":"Name","value":"isFeatured"}},{"kind":"Field","name":{"kind":"Name","value":"isPreInstalled"}},{"kind":"Field","name":{"kind":"Name","value":"isConfigured"}},{"kind":"Field","name":{"kind":"Name","value":"ownerWorkspaceId"}},{"kind":"Field","name":{"kind":"Name","value":"createdAt"}},{"kind":"Field","name":{"kind":"Name","value":"updatedAt"}}]}}]} as unknown as DocumentNode<FindAllApplicationRegistrationsQuery, FindAllApplicationRegistrationsQueryVariables>;
export const CreateDatabaseConfigVariableDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"mutation","name":{"kind":"Name","value":"CreateDatabaseConfigVariable"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"key"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"String"}}}},{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"value"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"JSON"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"createDatabaseConfigVariable"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"key"},"value":{"kind":"Variable","name":{"kind":"Name","value":"key"}}},{"kind":"Argument","name":{"kind":"Name","value":"value"},"value":{"kind":"Variable","name":{"kind":"Name","value":"value"}}}]}]}}]} as unknown as DocumentNode<CreateDatabaseConfigVariableMutation, CreateDatabaseConfigVariableMutationVariables>;
export const DeleteDatabaseConfigVariableDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"mutation","name":{"kind":"Name","value":"DeleteDatabaseConfigVariable"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"key"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"String"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"deleteDatabaseConfigVariable"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"key"},"value":{"kind":"Variable","name":{"kind":"Name","value":"key"}}}]}]}}]} as unknown as DocumentNode<DeleteDatabaseConfigVariableMutation, DeleteDatabaseConfigVariableMutationVariables>;
export const UpdateDatabaseConfigVariableDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"mutation","name":{"kind":"Name","value":"UpdateDatabaseConfigVariable"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"key"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"String"}}}},{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"value"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"JSON"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"updateDatabaseConfigVariable"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"key"},"value":{"kind":"Variable","name":{"kind":"Name","value":"key"}}},{"kind":"Argument","name":{"kind":"Name","value":"value"},"value":{"kind":"Variable","name":{"kind":"Name","value":"value"}}}]}]}}]} as unknown as DocumentNode<UpdateDatabaseConfigVariableMutation, UpdateDatabaseConfigVariableMutationVariables>;
@@ -1164,7 +1172,7 @@ export const GetDatabaseConfigVariableDocument = {"kind":"Document","definitions
export const UpdateWorkspaceFeatureFlagDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"mutation","name":{"kind":"Name","value":"UpdateWorkspaceFeatureFlag"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"workspaceId"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"UUID"}}}},{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"featureFlag"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"String"}}}},{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"value"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"Boolean"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"updateWorkspaceFeatureFlag"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"workspaceId"},"value":{"kind":"Variable","name":{"kind":"Name","value":"workspaceId"}}},{"kind":"Argument","name":{"kind":"Name","value":"featureFlag"},"value":{"kind":"Variable","name":{"kind":"Name","value":"featureFlag"}}},{"kind":"Argument","name":{"kind":"Name","value":"value"},"value":{"kind":"Variable","name":{"kind":"Name","value":"value"}}}]}]}}]} as unknown as DocumentNode<UpdateWorkspaceFeatureFlagMutation, UpdateWorkspaceFeatureFlagMutationVariables>;
export const AdminPanelRecentUsersDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"query","name":{"kind":"Name","value":"AdminPanelRecentUsers"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"searchTerm"}},"type":{"kind":"NamedType","name":{"kind":"Name","value":"String"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"adminPanelRecentUsers"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"searchTerm"},"value":{"kind":"Variable","name":{"kind":"Name","value":"searchTerm"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"email"}},{"kind":"Field","name":{"kind":"Name","value":"firstName"}},{"kind":"Field","name":{"kind":"Name","value":"lastName"}},{"kind":"Field","name":{"kind":"Name","value":"avatarUrl"}},{"kind":"Field","name":{"kind":"Name","value":"createdAt"}},{"kind":"Field","name":{"kind":"Name","value":"workspaceName"}},{"kind":"Field","name":{"kind":"Name","value":"workspaceId"}},{"kind":"Field","name":{"kind":"Name","value":"workspaceLogo"}}]}}]}}]} as unknown as DocumentNode<AdminPanelRecentUsersQuery, AdminPanelRecentUsersQueryVariables>;
export const AdminPanelTopWorkspacesDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"query","name":{"kind":"Name","value":"AdminPanelTopWorkspaces"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"searchTerm"}},"type":{"kind":"NamedType","name":{"kind":"Name","value":"String"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"adminPanelTopWorkspaces"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"searchTerm"},"value":{"kind":"Variable","name":{"kind":"Name","value":"searchTerm"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"logoUrl"}},{"kind":"Field","name":{"kind":"Name","value":"name"}},{"kind":"Field","name":{"kind":"Name","value":"totalUsers"}},{"kind":"Field","name":{"kind":"Name","value":"subdomain"}}]}}]}}]} as unknown as DocumentNode<AdminPanelTopWorkspacesQuery, AdminPanelTopWorkspacesQueryVariables>;
export const FindOneAdminApplicationRegistrationDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"query","name":{"kind":"Name","value":"FindOneAdminApplicationRegistration"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"id"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"String"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"findOneAdminApplicationRegistration"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"id"},"value":{"kind":"Variable","name":{"kind":"Name","value":"id"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"FragmentSpread","name":{"kind":"Name","value":"ApplicationRegistrationFragment"}}]}}]}},{"kind":"FragmentDefinition","name":{"kind":"Name","value":"ApplicationRegistrationFragment"},"typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"ApplicationRegistration"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"universalIdentifier"}},{"kind":"Field","name":{"kind":"Name","value":"name"}},{"kind":"Field","name":{"kind":"Name","value":"logoUrl"}},{"kind":"Field","name":{"kind":"Name","value":"oAuthClientId"}},{"kind":"Field","name":{"kind":"Name","value":"oAuthRedirectUris"}},{"kind":"Field","name":{"kind":"Name","value":"oAuthScopes"}},{"kind":"Field","name":{"kind":"Name","value":"sourceType"}},{"kind":"Field","name":{"kind":"Name","value":"sourcePackage"}},{"kind":"Field","name":{"kind":"Name","value":"latestAvailableVersion"}},{"kind":"Field","name":{"kind":"Name","value":"isListed"}},{"kind":"Field","name":{"kind":"Name","value":"isFeatured"}},{"kind":"Field","name":{"kind":"Name","value":"isPreInstalled"}},{"kind":"Field","name":{"kind":"Name","value":"isConfigured"}},{"kind":"Field","name":{"kind":"Name","value":"ownerWorkspaceId"}},{"kind":"Field","name":{"kind":"Name","value":"createdAt"}},{"kind":"Field","name":{"kind":"Name","value":"updatedAt"}}]}}]} as unknown as DocumentNode<FindOneAdminApplicationRegistrationQuery, FindOneAdminApplicationRegistrationQueryVariables>;
export const FindOneAdminApplicationRegistrationDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"query","name":{"kind":"Name","value":"FindOneAdminApplicationRegistration"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"id"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"String"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"findOneAdminApplicationRegistration"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"id"},"value":{"kind":"Variable","name":{"kind":"Name","value":"id"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"FragmentSpread","name":{"kind":"Name","value":"ApplicationRegistrationFragment"}}]}}]}},{"kind":"FragmentDefinition","name":{"kind":"Name","value":"ApplicationRegistrationFragment"},"typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"ApplicationRegistration"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"universalIdentifier"}},{"kind":"Field","name":{"kind":"Name","value":"name"}},{"kind":"Field","name":{"kind":"Name","value":"logo"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"fileId"}},{"kind":"Field","name":{"kind":"Name","value":"label"}},{"kind":"Field","name":{"kind":"Name","value":"extension"}},{"kind":"Field","name":{"kind":"Name","value":"url"}}]}},{"kind":"Field","name":{"kind":"Name","value":"oAuthClientId"}},{"kind":"Field","name":{"kind":"Name","value":"oAuthRedirectUris"}},{"kind":"Field","name":{"kind":"Name","value":"oAuthScopes"}},{"kind":"Field","name":{"kind":"Name","value":"sourceType"}},{"kind":"Field","name":{"kind":"Name","value":"sourcePackage"}},{"kind":"Field","name":{"kind":"Name","value":"latestAvailableVersion"}},{"kind":"Field","name":{"kind":"Name","value":"isListed"}},{"kind":"Field","name":{"kind":"Name","value":"isFeatured"}},{"kind":"Field","name":{"kind":"Name","value":"isPreInstalled"}},{"kind":"Field","name":{"kind":"Name","value":"isConfigured"}},{"kind":"Field","name":{"kind":"Name","value":"ownerWorkspaceId"}},{"kind":"Field","name":{"kind":"Name","value":"createdAt"}},{"kind":"Field","name":{"kind":"Name","value":"updatedAt"}}]}}]} as unknown as DocumentNode<FindOneAdminApplicationRegistrationQuery, FindOneAdminApplicationRegistrationQueryVariables>;
export const GetAdminChatThreadMessagesDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"query","name":{"kind":"Name","value":"GetAdminChatThreadMessages"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"threadId"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"UUID"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"getAdminChatThreadMessages"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"threadId"},"value":{"kind":"Variable","name":{"kind":"Name","value":"threadId"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"thread"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"title"}},{"kind":"Field","name":{"kind":"Name","value":"totalInputTokens"}},{"kind":"Field","name":{"kind":"Name","value":"totalOutputTokens"}},{"kind":"Field","name":{"kind":"Name","value":"conversationSize"}},{"kind":"Field","name":{"kind":"Name","value":"createdAt"}},{"kind":"Field","name":{"kind":"Name","value":"updatedAt"}}]}},{"kind":"Field","name":{"kind":"Name","value":"messages"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"role"}},{"kind":"Field","name":{"kind":"Name","value":"parts"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"type"}},{"kind":"Field","name":{"kind":"Name","value":"textContent"}},{"kind":"Field","name":{"kind":"Name","value":"toolName"}}]}},{"kind":"Field","name":{"kind":"Name","value":"createdAt"}}]}}]}}]}}]} as unknown as DocumentNode<GetAdminChatThreadMessagesQuery, GetAdminChatThreadMessagesQueryVariables>;
export const GetAdminWorkspaceChatThreadsDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"query","name":{"kind":"Name","value":"GetAdminWorkspaceChatThreads"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"workspaceId"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"UUID"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"getAdminWorkspaceChatThreads"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"workspaceId"},"value":{"kind":"Variable","name":{"kind":"Name","value":"workspaceId"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"title"}},{"kind":"Field","name":{"kind":"Name","value":"totalInputTokens"}},{"kind":"Field","name":{"kind":"Name","value":"totalOutputTokens"}},{"kind":"Field","name":{"kind":"Name","value":"conversationSize"}},{"kind":"Field","name":{"kind":"Name","value":"createdAt"}},{"kind":"Field","name":{"kind":"Name","value":"updatedAt"}}]}}]}}]} as unknown as DocumentNode<GetAdminWorkspaceChatThreadsQuery, GetAdminWorkspaceChatThreadsQueryVariables>;
export const GetUpgradeStatusDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"query","name":{"kind":"Name","value":"GetUpgradeStatus"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"workspaceIds"}},"type":{"kind":"NonNullType","type":{"kind":"ListType","type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"UUID"}}}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"getUpgradeStatus"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"workspaceIds"},"value":{"kind":"Variable","name":{"kind":"Name","value":"workspaceIds"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"workspaceId"}},{"kind":"Field","name":{"kind":"Name","value":"displayName"}},{"kind":"Field","name":{"kind":"Name","value":"inferredVersion"}},{"kind":"Field","name":{"kind":"Name","value":"health"}},{"kind":"Field","name":{"kind":"Name","value":"latestCommand"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"name"}},{"kind":"Field","name":{"kind":"Name","value":"status"}},{"kind":"Field","name":{"kind":"Name","value":"executedByVersion"}},{"kind":"Field","name":{"kind":"Name","value":"errorMessage"}},{"kind":"Field","name":{"kind":"Name","value":"createdAt"}}]}}]}}]}}]} as unknown as DocumentNode<GetUpgradeStatusQuery, GetUpgradeStatusQueryVariables>;
File diff suppressed because one or more lines are too long
@@ -11,7 +11,12 @@ export const APPLICATION_FRAGMENT = gql`
id
name
description
logo
logo {
fileId
label
extension
url
}
version
universalIdentifier
applicationRegistrationId
@@ -19,7 +24,12 @@ export const APPLICATION_FRAGMENT = gql`
id
latestAvailableVersion
sourceType
logoUrl
logo {
fileId
label
extension
url
}
}
canBeUninstalled
defaultRoleId
@@ -6,7 +6,12 @@ export const FIND_MANY_APPLICATIONS = gql`
id
name
description
logo
logo {
fileId
label
extension
url
}
version
universalIdentifier
applicationRegistrationId
@@ -8,7 +8,6 @@ import { currentWorkspaceState } from '@/auth/states/currentWorkspaceState';
import { useAtomStateValue } from '@/ui/utilities/state/jotai/hooks/useAtomStateValue';
import { t } from '@lingui/core/macro';
import { isDefined } from 'twenty-shared/utils';
import { buildApplicationLogoUrl } from '@/applications/utils/buildApplicationLogoUrl';
import CustomLogo from '~/pages/settings/applications/assets/custom-illustrations/custom-logo.webp';
import StandardLogo from '~/pages/settings/applications/assets/standard-illustrations/standard-logo.webp';
@@ -67,11 +66,7 @@ export const useApplicationChipData = ({
? new URL(StandardLogo, window.location.href).toString()
: isCustom
? new URL(CustomLogo, window.location.href).toString()
: buildApplicationLogoUrl({
applicationId: application.id,
logo: application.logo,
workspaceId: currentWorkspace?.id,
});
: (application.logo?.url ?? undefined);
return {
applicationChipData: {
@@ -1,26 +0,0 @@
import { REACT_APP_SERVER_BASE_URL } from '~/config';
import { isDefined } from 'twenty-shared/utils';
export const buildApplicationLogoUrl = ({
workspaceId,
applicationId,
logo,
}: {
workspaceId?: string | null;
applicationId?: string | null;
logo?: string | null;
}): string | undefined => {
if (
!isDefined(logo) ||
logo.startsWith('http://') ||
logo.startsWith('https://')
) {
return logo ?? undefined;
}
if (!isDefined(workspaceId) || !isDefined(applicationId)) {
return undefined;
}
return `${REACT_APP_SERVER_BASE_URL}/public-assets/${workspaceId}/${applicationId}/${logo}`;
};
@@ -4,7 +4,12 @@ export const FIND_APPLICATION_REGISTRATION_BY_CLIENT_ID = gql`
query FindApplicationRegistrationByClientId($clientId: String!) {
findApplicationRegistrationByClientId(clientId: $clientId) {
id
logoUrl
logo {
fileId
label
extension
url
}
name
oAuthScopes
websiteUrl
@@ -1,3 +1,4 @@
import { currentUserState } from '@/auth/states/currentUserState';
import { currentUserWorkspaceState } from '@/auth/states/currentUserWorkspaceState';
import { currentWorkspaceState } from '@/auth/states/currentWorkspaceState';
import { objectPermissionsFamilySelector } from '@/auth/states/objectPermissionsFamilySelector';
@@ -27,6 +28,7 @@ export const StandalonePageCommandMenu = () => {
const commandMenuItems = useAtomStateValue(commandMenuItemsSelector);
const currentWorkspace = useAtomStateValue(currentWorkspaceState);
const currentUserWorkspace = useAtomStateValue(currentUserWorkspaceState);
const currentUser = useAtomStateValue(currentUserState);
const currentPageLayoutId = useAtomStateValue(currentPageLayoutIdState);
const isLayoutCustomizationModeEnabled = useAtomStateValue(
isLayoutCustomizationModeEnabledState,
@@ -85,12 +87,16 @@ export const StandalonePageCommandMenu = () => {
permissionFlags,
targetObjectReadPermissions,
targetObjectWritePermissions,
canImpersonate: currentUser?.canImpersonate === true,
canAccessFullAdminPanel: currentUser?.canAccessFullAdminPanel === true,
objectMetadataItem: {},
objectMetadataLabel: '',
};
}, [
currentWorkspace?.featureFlags,
currentUserWorkspace?.permissionFlags,
currentUser?.canImpersonate,
currentUser?.canAccessFullAdminPanel,
isLayoutCustomizationModeEnabled,
objectMetadataItems,
store,
@@ -27,6 +27,8 @@ export const EMPTY_COMMAND_MENU_CONTEXT_API: CommandMenuContextApi = {
permissionFlags: {},
targetObjectReadPermissions: {},
targetObjectWritePermissions: {},
canImpersonate: false,
canAccessFullAdminPanel: false,
objectMetadataItem: {},
objectMetadataLabel: '',
};
@@ -67,6 +67,8 @@ const getWrapper =
permissionFlags: {},
targetObjectReadPermissions: {},
targetObjectWritePermissions: {},
canImpersonate: false,
canAccessFullAdminPanel: false,
objectMetadataItem: {},
objectMetadataLabel: '',
},
@@ -1,3 +1,4 @@
import { currentUserState } from '@/auth/states/currentUserState';
import { currentUserWorkspaceState } from '@/auth/states/currentUserWorkspaceState';
import { currentWorkspaceState } from '@/auth/states/currentWorkspaceState';
import { objectPermissionsFamilySelector } from '@/auth/states/objectPermissionsFamilySelector';
@@ -151,6 +152,10 @@ export const useCurrentCommandMenuContextApi = (): CommandMenuContextApi => {
permissionFlags[flag] = true;
}
const currentUser = useAtomStateValue(currentUserState);
const canImpersonate = currentUser?.canImpersonate === true;
const canAccessFullAdminPanel = currentUser?.canAccessFullAdminPanel === true;
const targetObjectReadPermissions: Record<string, boolean> = {};
const targetObjectWritePermissions: Record<string, boolean> = {};
@@ -188,6 +193,8 @@ export const useCurrentCommandMenuContextApi = (): CommandMenuContextApi => {
permissionFlags,
targetObjectReadPermissions,
targetObjectWritePermissions,
canImpersonate,
canAccessFullAdminPanel,
objectMetadataItem: objectMetadataItem ?? {},
objectMetadataLabel,
};
@@ -8,6 +8,12 @@ export const MARKETPLACE_APP_DETAIL_FRAGMENT = gql`
sourceType
sourcePackage
latestAvailableVersion
logo {
fileId
label
extension
url
}
isListed
isFeatured
manifest
@@ -179,7 +179,7 @@ const SettingsAdminAppsTableRow = ({
<AppChip
size="md"
fallbackApplicationData={{
logo: registration.logoUrl,
logo: registration.logo?.url ?? null,
name: registration.name,
}}
/>
@@ -0,0 +1,21 @@
import { gql } from '@apollo/client';
export const FIND_ADMIN_APPLICATION_REGISTRATION_VARIABLES = gql`
query FindAdminApplicationRegistrationVariables(
$applicationRegistrationId: String!
) {
findAdminApplicationRegistrationVariables(
applicationRegistrationId: $applicationRegistrationId
) {
id
key
value
description
isSecret
isRequired
isFilled
createdAt
updatedAt
}
}
`;
@@ -5,7 +5,12 @@ export const APPLICATION_REGISTRATION_FRAGMENT = gql`
id
universalIdentifier
name
logoUrl
logo {
fileId
label
extension
url
}
oAuthClientId
oAuthRedirectUris
oAuthScopes
@@ -28,7 +28,14 @@ export const useObjectAndFieldRows = ({
}) => {
const objectMetadataItems = useAtomStateValue(objectMetadataItemsSelector);
const currentWorkspace = useAtomStateValue(currentWorkspaceState);
const installedApplications = currentWorkspace?.installedApplications;
const installedApplications = currentWorkspace?.installedApplications?.map(
(app) => ({
id: app.id,
name: app.name,
universalIdentifier: app.universalIdentifier,
logo: app.logo?.url ?? null,
}),
);
return useMemo(() => {
if (isDefined(installedApplication)) {
@@ -44,7 +44,7 @@ export const getInstalledApplicationObjectAndFieldRows = ({
}),
application: {
id: installedApplication.id,
logo: installedApplication.logo,
logo: installedApplication.logo?.url ?? null,
name: installedApplication.name,
universalIdentifier: installedApplication.universalIdentifier,
},
@@ -70,7 +70,12 @@ export const USER_QUERY_FRAGMENT = gql`
id
name
universalIdentifier
logo
logo {
fileId
label
extension
url
}
}
isCustomDomainEnabled
workspaceUrls {
@@ -285,7 +285,7 @@ export const Authorize = () => {
}
const appName = applicationRegistration.name;
const appLogoUrl = applicationRegistration.logoUrl;
const appLogoUrl = applicationRegistration.logo?.url ?? null;
const requestedScopes: string[] = applicationRegistration.oAuthScopes ?? [];
const showLogoImage = isNonEmptyString(appLogoUrl) && !hasLogoError;
@@ -20,7 +20,12 @@ const buildHandlers = (
application: {
id: string;
name: string;
logoUrl: string | null;
logo: {
fileId: string | null;
label: string | null;
extension: string | null;
url: string;
} | null;
oAuthScopes: string[];
websiteUrl: string | null;
} | null,
@@ -90,7 +95,7 @@ export const Default: Story = {
handlers: buildHandlers({
id: 'application-id-default',
name: 'ChatGPT',
logoUrl: null,
logo: null,
oAuthScopes: ['api', 'profile'],
websiteUrl: null,
}),
@@ -116,7 +121,7 @@ export const WithApiScopeOnly: Story = {
handlers: buildHandlers({
id: 'application-id-api-only',
name: 'Internal Tool',
logoUrl: null,
logo: null,
oAuthScopes: ['api'],
websiteUrl: null,
}),
@@ -144,7 +149,7 @@ export const WithLongAppName: Story = {
handlers: buildHandlers({
id: 'application-id-long-name',
name: 'Custom Workspace Automation Suite',
logoUrl: null,
logo: null,
oAuthScopes: ['api', 'profile'],
websiteUrl: null,
}),
@@ -8,12 +8,15 @@ import { SubMenuTopBarContainer } from '@/ui/layout/page/components/SubMenuTopBa
import { useApolloAdminClient } from '@/settings/admin-panel/apollo/hooks/useApolloAdminClient';
import { APPLICATION_REGISTRATION_ADMIN_PATH } from '@/settings/admin-panel/apps/constants/ApplicationRegistrationAdminPath';
import { SettingsPageContainer } from '@/settings/components/SettingsPageContainer';
import { styled } from '@linaria/react';
import {
Avatar,
IconInfoCircle,
IconKey,
IconSettings,
IconWorld,
} from 'twenty-ui/display';
import { themeCssVariables } from 'twenty-ui/theme-constants';
import { SettingsApplicationRegistrationConfigTab } from '~/pages/settings/applications/tabs/SettingsApplicationRegistrationConfigTab';
import { SettingsApplicationRegistrationOAuthTab } from '~/pages/settings/applications/tabs/SettingsApplicationRegistrationOAuthTab';
import { SettingsApplicationRegistrationDistributionTab } from '~/pages/settings/applications/tabs/SettingsApplicationRegistrationDistributionTab';
@@ -22,6 +25,12 @@ import { TabList } from '@/ui/layout/tab-list/components/TabList';
import { useAtomComponentStateValue } from '@/ui/utilities/state/jotai/hooks/useAtomComponentStateValue';
import { activeTabIdComponentState } from '@/ui/layout/tab-list/states/activeTabIdComponentState';
const StyledTitleContainer = styled.div`
align-items: center;
display: flex;
gap: ${themeCssVariables.spacing[2]};
`;
const REGISTRATION_DETAIL_TAB_LIST_ID =
'admin-application-registration-detail-tab-list';
@@ -94,7 +103,18 @@ export const SettingsAdminApplicationRegistrationDetail = () => {
return (
<SubMenuTopBarContainer
title={registration.name}
title={
<StyledTitleContainer>
<Avatar
type="app"
size="md"
avatarUrl={registration.logo?.url ?? undefined}
placeholder={registration.name}
placeholderColorSeed={registration.name}
/>
{registration.name}
</StyledTitleContainer>
}
links={[
{
children: t`Other`,
@@ -54,7 +54,12 @@ const FIND_MANY_APPLICATIONS_FOR_TOOL_TABLE = gql`
id
name
universalIdentifier
logo
logo {
fileId
label
extension
url
}
}
}
`;
@@ -92,7 +97,12 @@ export const SettingsToolsTable = () => {
id: string;
name: string;
universalIdentifier: string;
logo?: string | null;
logo?: {
fileId: string | null;
label: string | null;
extension: string | null;
url: string;
} | null;
}>;
}>(FIND_MANY_APPLICATIONS_FOR_TOOL_TABLE);
const { data: marketplaceAppsData } = useQuery<{
@@ -293,7 +293,7 @@ export const SettingsApplicationDetails = () => {
applicationInfo={{
id: application.id,
name: displayName,
logo: application.logo,
logo: application.logo?.url ?? null,
universalIdentifier: application.universalIdentifier,
}}
/>
@@ -8,12 +8,15 @@ import { useLingui } from '@lingui/react/macro';
import { Tag } from 'twenty-ui/components';
import { TabList } from '@/ui/layout/tab-list/components/TabList';
import { SettingsPageContainer } from '@/settings/components/SettingsPageContainer';
import { styled } from '@linaria/react';
import {
Avatar,
IconInfoCircle,
IconKey,
IconSettings,
IconWorld,
} from 'twenty-ui/display';
import { themeCssVariables } from 'twenty-ui/theme-constants';
import { SettingsApplicationRegistrationConfigTab } from '~/pages/settings/applications/tabs/SettingsApplicationRegistrationConfigTab';
import { SettingsApplicationRegistrationOAuthTab } from '~/pages/settings/applications/tabs/SettingsApplicationRegistrationOAuthTab';
import { SettingsApplicationRegistrationDistributionTab } from '~/pages/settings/applications/tabs/SettingsApplicationRegistrationDistributionTab';
@@ -21,6 +24,12 @@ import { SettingsApplicationRegistrationGeneralTab } from '~/pages/settings/appl
import { useAtomComponentStateValue } from '@/ui/utilities/state/jotai/hooks/useAtomComponentStateValue';
import { activeTabIdComponentState } from '@/ui/layout/tab-list/states/activeTabIdComponentState';
const StyledTitleContainer = styled.div`
align-items: center;
display: flex;
gap: ${themeCssVariables.spacing[2]};
`;
const REGISTRATION_DETAIL_TAB_LIST_ID =
'application-registration-detail-tab-list';
@@ -86,7 +95,18 @@ export const SettingsApplicationRegistrationDetails = () => {
return (
<SubMenuTopBarContainer
title={registration.name}
title={
<StyledTitleContainer>
<Avatar
type="app"
size="md"
avatarUrl={registration.logo?.url ?? undefined}
placeholder={registration.name}
placeholderColorSeed={registration.name}
/>
{registration.name}
</StyledTitleContainer>
}
tag={<Tag text={t`Owner`} color={'gray'} />}
links={[
{
@@ -245,7 +245,7 @@ export const SettingsAvailableApplicationDetails = () => {
manifestContent={manifest}
applicationInfo={{
name: displayName,
logo: app?.logoUrl,
logo: detail?.logo?.url,
universalIdentifier: detail.universalIdentifier,
}}
/>
@@ -286,6 +286,10 @@ export const SettingsAvailableApplicationDetails = () => {
displayName={displayName}
description={description}
applicationId={application?.id}
fallbackApplicationData={{
logo: detail?.logo?.url,
name: displayName,
}}
isUnlisted={isUnlisted}
/>
}
@@ -9,10 +9,11 @@ import { useApplicationChipData } from '@/applications/hooks/useApplicationChipD
type SettingsApplicationDetailTitleProps = {
displayName: string;
description?: string;
logoUrl?: string;
applicationId?: string;
applicationName?: string;
universalIdentifier?: string;
fallbackApplicationData?: {
logo?: string | null;
name?: string | null;
};
isUnlisted?: boolean;
};
@@ -73,12 +74,14 @@ export const SettingsApplicationDetailTitle = ({
displayName,
description,
applicationId,
fallbackApplicationData,
isUnlisted = false,
}: SettingsApplicationDetailTitleProps) => {
const descriptionSummary = getApplicationDescriptionSummary(description);
const { applicationChipData } = useApplicationChipData({
applicationId,
fallbackApplicationData,
});
return (
@@ -177,8 +177,8 @@ export const SettingsApplicationsDeveloperTab = () => {
<TableRow
gridTemplateColumns={APPLICATION_TABLE_ROW_GRID_TEMPLATE_COLUMNS}
>
<TableHeader> {t`Name`}</TableHeader>
<TableHeader>{''}</TableHeader>
<TableHeader>{t`Name`}</TableHeader>
<TableHeader>{t`Type`}</TableHeader>
<TableHeader>{''}</TableHeader>
<TableHeader />
</TableRow>
@@ -187,7 +187,11 @@ export const SettingsApplicationsDeveloperTab = () => {
return (
<SettingsApplicationTableRow
key={registration.id}
application={registration}
application={{
...registration,
logo: registration.logo?.url ?? null,
}}
sourceType={registration.sourceType}
action={
<IconChevronRight
size={theme.icon.size.md}
@@ -39,6 +39,8 @@ const buildMockCommandMenuContextApi = (
permissionFlags: {},
targetObjectReadPermissions: {},
targetObjectWritePermissions: {},
canImpersonate: false,
canAccessFullAdminPanel: false,
objectMetadataItem: {},
objectMetadataLabel: '',
...overrides,
@@ -25,6 +25,10 @@ export const targetObjectReadPermissions =
null as unknown as CommandMenuContextApi['targetObjectReadPermissions'];
export const targetObjectWritePermissions =
null as unknown as CommandMenuContextApi['targetObjectWritePermissions'];
export const canImpersonate =
null as unknown as CommandMenuContextApi['canImpersonate'];
export const canAccessFullAdminPanel =
null as unknown as CommandMenuContextApi['canAccessFullAdminPanel'];
export const objectMetadataItem =
null as unknown as CommandMenuContextApi['objectMetadataItem'];
@@ -12,6 +12,8 @@ export {
featureFlags,
targetObjectReadPermissions,
targetObjectWritePermissions,
canImpersonate,
canAccessFullAdminPanel,
isDefined,
isNonEmptyString,
includes,
@@ -2,6 +2,7 @@ import {
DEFAULT_API_URL_NAME,
DEFAULT_APP_ACCESS_TOKEN_NAME,
} from 'twenty-shared/application';
import { buildPublicAssetUrl } from 'twenty-shared/utils';
const decodeTokenPayload = (
token: string,
@@ -38,5 +39,16 @@ export const getPublicAssetUrl = (path: string): string => {
.map(encodeURIComponent)
.join('/');
return `${apiUrl}/public-assets/${workspaceId}/${applicationId}/${encodedPath}`;
const url = buildPublicAssetUrl({
path: encodedPath,
serverUrl: apiUrl,
workspaceId,
applicationId,
});
if (!url) {
throw new Error('Failed to build public asset URL');
}
return url;
};
@@ -29,9 +29,7 @@ export class AddWorkspaceIdToTotoFastInstanceCommand
implements FastInstanceCommand
{
public async up(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(
`ALTER TABLE "core"."toto" ADD "workspaceId" uuid`,
);
await queryRunner.query(`ALTER TABLE "core"."toto" ADD "workspaceId" uuid`);
}
public async down(queryRunner: QueryRunner): Promise<void> {
@@ -81,12 +79,9 @@ Workspace commands run per-workspace logic across all active or suspended worksp
@RegisteredWorkspaceCommand('1.22.0', 1780000002000)
@Command({
name: 'upgrade:1-22:backfill-standard-skills',
description:
'Backfill standard skills for existing workspaces',
description: 'Backfill standard skills for existing workspaces',
})
export class BackfillStandardSkillsCommand
extends ActiveOrSuspendedWorkspaceCommandRunner
{
export class BackfillStandardSkillsCommand extends ActiveOrSuspendedWorkspaceCommandRunner {
constructor(
protected readonly workspaceIteratorService: WorkspaceIteratorService,
// inject any services you need
@@ -3,12 +3,14 @@
This Bash script helps generate self-signed SSL certificates for local development. It uses OpenSSL to create a root certificate authority, a domain certificate, and configures them for local usage.
## Features
- Generates a private key and root certificate.
- Creates a signed certificate for a specified domain.
- Adds the root certificate to the macOS keychain for trusted usage (macOS only).
- Customizable with default values for easier use.
## Requirements
- OpenSSL
## Usage
@@ -30,24 +32,27 @@ To generate certificates using the default values:
#### Examples:
1. **Using Default Values**:
```sh
./script.sh
```
```sh
./script.sh
```
2. **Custom Domain Name**:
```sh
./script.sh example.com
```
```sh
./script.sh example.com
```
3. **Custom Domain Name and Root Certificate Name**:
```sh
./script.sh example.com customRootCertificate
```
```sh
./script.sh example.com customRootCertificate
```
4. **Custom Domain Name, Root Certificate Name, and Validity Days**:
```sh
./script.sh example.com customRootCertificate 398
```
```sh
./script.sh example.com customRootCertificate 398
```
## Script Details
@@ -71,4 +76,4 @@ The generated files are stored in `~/certs/{domain}`:
## Notes
- If running on non-macOS systems, you'll need to manually add the root certificate to your trusted certificate store.
- Ensure that OpenSSL is installed and available in your PATH.
- Ensure that OpenSSL is installed and available in your PATH.
@@ -0,0 +1,33 @@
import { QueryRunner } from 'typeorm';
import { RegisteredInstanceCommand } from 'src/engine/core-modules/upgrade/decorators/registered-instance-command.decorator';
import { FastInstanceCommand } from 'src/engine/core-modules/upgrade/interfaces/fast-instance-command.interface';
@RegisteredInstanceCommand('2.8.0', 1779387505162)
export class AddLogoToApplicationRegistrationFastInstanceCommand
implements FastInstanceCommand
{
public async up(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(
'ALTER TABLE "core"."applicationRegistration" ADD "logoFileId" uuid',
);
await queryRunner.query(
'ALTER TABLE "core"."applicationRegistration" ADD CONSTRAINT "UQ_796819fb23559c233e6ebd49f34" UNIQUE ("logoFileId")',
);
await queryRunner.query(
'ALTER TABLE "core"."applicationRegistration" ADD CONSTRAINT "FK_796819fb23559c233e6ebd49f34" FOREIGN KEY ("logoFileId") REFERENCES "core"."file"("id") ON DELETE SET NULL ON UPDATE NO ACTION',
);
}
public async down(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(
'ALTER TABLE "core"."applicationRegistration" DROP CONSTRAINT "FK_796819fb23559c233e6ebd49f34"',
);
await queryRunner.query(
'ALTER TABLE "core"."applicationRegistration" DROP CONSTRAINT "UQ_796819fb23559c233e6ebd49f34"',
);
await queryRunner.query(
'ALTER TABLE "core"."applicationRegistration" DROP COLUMN "logoFileId"',
);
}
}
@@ -3,6 +3,7 @@ import { Module } from '@nestjs/common';
import { WorkspaceIteratorModule } from 'src/database/commands/command-runners/workspace-iterator.module';
import { DropChannelStandardObjectsCommand } from 'src/database/commands/upgrade-version-command/2-8/2-8-workspace-command-1798000050000-drop-channel-standard-objects.command';
import { BackfillRelationJoinColumnIndexesCommand } from 'src/database/commands/upgrade-version-command/2-8/2-8-workspace-command-1798100000000-backfill-relation-join-column-indexes.command';
import { GateDefaultCommandMenuItemsByPermissionFlagCommand } from 'src/database/commands/upgrade-version-command/2-8/2-8-workspace-command-1798100010000-gate-default-command-menu-items-by-permission-flag.command';
import { ApplicationModule } from 'src/engine/core-modules/application/application.module';
import { WorkspaceSchemaManagerModule } from 'src/engine/twenty-orm/workspace-schema-manager/workspace-schema-manager.module';
import { WorkspaceCacheModule } from 'src/engine/workspace-cache/workspace-cache.module';
@@ -19,6 +20,7 @@ import { WorkspaceMigrationModule } from 'src/engine/workspace-manager/workspace
providers: [
DropChannelStandardObjectsCommand,
BackfillRelationJoinColumnIndexesCommand,
GateDefaultCommandMenuItemsByPermissionFlagCommand,
],
})
export class V2_8_UpgradeVersionCommandModule {}
@@ -0,0 +1,159 @@
import { Command } from 'nest-commander';
import { isDefined } from 'twenty-shared/utils';
import { ActiveOrSuspendedWorkspaceCommandRunner } from 'src/database/commands/command-runners/active-or-suspended-workspace.command-runner';
import { WorkspaceIteratorService } from 'src/database/commands/command-runners/workspace-iterator.service';
import { type RunOnWorkspaceArgs } from 'src/database/commands/command-runners/workspace.command-runner';
import { ApplicationService } from 'src/engine/core-modules/application/application.service';
import { RegisteredWorkspaceCommand } from 'src/engine/core-modules/upgrade/decorators/registered-workspace-command.decorator';
import { WorkspaceCacheService } from 'src/engine/workspace-cache/services/workspace-cache.service';
import { STANDARD_COMMAND_MENU_ITEMS } from 'src/engine/workspace-manager/twenty-standard-application/constants/standard-command-menu-item.constant';
import { computeTwentyStandardApplicationAllFlatEntityMaps } from 'src/engine/workspace-manager/twenty-standard-application/utils/twenty-standard-application-all-flat-entity-maps.constant';
import { WorkspaceMigrationValidateBuildAndRunService } from 'src/engine/workspace-manager/workspace-migration/services/workspace-migration-validate-build-and-run-service';
const UNIVERSAL_IDENTIFIERS_TO_FIX = new Set<string>([
STANDARD_COMMAND_MENU_ITEMS.askAi.universalIdentifier,
STANDARD_COMMAND_MENU_ITEMS.viewPreviousAiChats.universalIdentifier,
STANDARD_COMMAND_MENU_ITEMS.composeEmail.universalIdentifier,
STANDARD_COMMAND_MENU_ITEMS.composeEmailToPerson.universalIdentifier,
STANDARD_COMMAND_MENU_ITEMS.composeEmailToCompany.universalIdentifier,
STANDARD_COMMAND_MENU_ITEMS.composeEmailToOpportunity.universalIdentifier,
STANDARD_COMMAND_MENU_ITEMS.goToSettingsAccounts.universalIdentifier,
STANDARD_COMMAND_MENU_ITEMS.goToSettingsAccountsEmails.universalIdentifier,
STANDARD_COMMAND_MENU_ITEMS.goToSettingsAccountsCalendars.universalIdentifier,
STANDARD_COMMAND_MENU_ITEMS.goToSettingsGeneral.universalIdentifier,
STANDARD_COMMAND_MENU_ITEMS.goToSettingsObjects.universalIdentifier,
STANDARD_COMMAND_MENU_ITEMS.goToSettingsMembers.universalIdentifier,
STANDARD_COMMAND_MENU_ITEMS.goToSettingsRoles.universalIdentifier,
STANDARD_COMMAND_MENU_ITEMS.goToSettingsDomains.universalIdentifier,
STANDARD_COMMAND_MENU_ITEMS.goToSettingsBilling.universalIdentifier,
STANDARD_COMMAND_MENU_ITEMS.goToSettingsApiWebhooks.universalIdentifier,
STANDARD_COMMAND_MENU_ITEMS.goToSettingsApplications.universalIdentifier,
STANDARD_COMMAND_MENU_ITEMS.goToSettingsAI.universalIdentifier,
STANDARD_COMMAND_MENU_ITEMS.goToSettingsSecurity.universalIdentifier,
STANDARD_COMMAND_MENU_ITEMS.goToSettingsUpdates.universalIdentifier,
STANDARD_COMMAND_MENU_ITEMS.goToSettingsAdminPanel.universalIdentifier,
]);
@RegisteredWorkspaceCommand('2.8.0', 1798100010000)
@Command({
name: 'upgrade:2-8:gate-default-command-menu-items-by-permission-flag',
description:
'Gate default command menu items (Ask AI, settings navigation, compose email) behind their relevant permission flags so members without permission no longer see them',
})
export class GateDefaultCommandMenuItemsByPermissionFlagCommand extends ActiveOrSuspendedWorkspaceCommandRunner {
constructor(
protected readonly workspaceIteratorService: WorkspaceIteratorService,
private readonly applicationService: ApplicationService,
private readonly workspaceMigrationValidateBuildAndRunService: WorkspaceMigrationValidateBuildAndRunService,
private readonly workspaceCacheService: WorkspaceCacheService,
) {
super(workspaceIteratorService);
}
override async runOnWorkspace({
workspaceId,
options,
}: RunOnWorkspaceArgs): Promise<void> {
const isDryRun = options.dryRun ?? false;
this.logger.log(
`${isDryRun ? '[DRY RUN] ' : ''}Gating default command menu items by permission flag for workspace ${workspaceId}`,
);
const { twentyStandardFlatApplication } =
await this.applicationService.findWorkspaceTwentyStandardAndCustomApplicationOrThrow(
{ workspaceId },
);
const { flatCommandMenuItemMaps: existingFlatCommandMenuItemMaps } =
await this.workspaceCacheService.getOrRecompute(workspaceId, [
'flatCommandMenuItemMaps',
]);
const { allFlatEntityMaps: standardAllFlatEntityMaps } =
computeTwentyStandardApplicationAllFlatEntityMaps({
now: new Date().toISOString(),
workspaceId,
twentyStandardApplicationId: twentyStandardFlatApplication.id,
});
const itemsToUpdate = [...UNIVERSAL_IDENTIFIERS_TO_FIX]
.map((universalIdentifier) => {
const standardItem =
standardAllFlatEntityMaps.flatCommandMenuItemMaps
.byUniversalIdentifier[universalIdentifier];
const existingItem =
existingFlatCommandMenuItemMaps.byUniversalIdentifier[
universalIdentifier
];
if (
!isDefined(standardItem) ||
!isDefined(existingItem) ||
existingItem.conditionalAvailabilityExpression ===
standardItem.conditionalAvailabilityExpression
) {
return undefined;
}
return {
...existingItem,
conditionalAvailabilityExpression:
standardItem.conditionalAvailabilityExpression,
updatedAt: new Date().toISOString(),
};
})
.filter(isDefined);
if (itemsToUpdate.length === 0) {
this.logger.log(
`Default command menu item expressions already up to date for workspace ${workspaceId}`,
);
return;
}
this.logger.log(
`Found ${itemsToUpdate.length} command menu item(s) to update for workspace ${workspaceId}`,
);
if (isDryRun) {
this.logger.log(
`[DRY RUN] Would update ${itemsToUpdate.length} command menu item availability expression(s) for workspace ${workspaceId}`,
);
return;
}
const validateAndBuildResult =
await this.workspaceMigrationValidateBuildAndRunService.validateBuildAndRunWorkspaceMigration(
{
allFlatEntityOperationByMetadataName: {
commandMenuItem: {
flatEntityToCreate: [],
flatEntityToDelete: [],
flatEntityToUpdate: itemsToUpdate,
},
},
workspaceId,
applicationUniversalIdentifier:
twentyStandardFlatApplication.universalIdentifier,
},
);
if (validateAndBuildResult.status === 'fail') {
this.logger.error(
`Failed to update command menu item availability expressions:\n${JSON.stringify(validateAndBuildResult, null, 2)}`,
);
throw new Error(
`Failed to gate default command menu items by permission flag for workspace ${workspaceId}`,
);
}
this.logger.log(
`Successfully updated ${itemsToUpdate.length} command menu item availability expression(s) for workspace ${workspaceId}`,
);
}
}
@@ -53,6 +53,7 @@ import { DropPostgresCredentialsTableFastInstanceCommand } from 'src/database/co
import { AddRelationTargetFieldMetadataIdToViewFilterFastInstanceCommand } from 'src/database/commands/upgrade-version-command/2-6/2-6-instance-command-fast-1798000005000-add-relation-target-field-metadata-id-to-view-filter';
import { AddChannelSyncStageIndexesFastInstanceCommand } from 'src/database/commands/upgrade-version-command/2-6/2-6-instance-command-fast-1798000010000-add-channel-sync-stage-indexes';
import { FinalizeRolePermissionFlagCutoverFastInstanceCommand } from 'src/database/commands/upgrade-version-command/2-7/2-7-instance-command-fast-1779600000000-finalize-role-permission-flag-cutover';
import { AddLogoToApplicationRegistrationFastInstanceCommand } from 'src/database/commands/upgrade-version-command/2-8/2-8-instance-command-fast-1779387505162-add-logo-to-application-registration';
export const INSTANCE_COMMANDS = [
AddViewFieldGroupIdIndexOnViewFieldFastInstanceCommand,
@@ -108,4 +109,5 @@ export const INSTANCE_COMMANDS = [
AddRelationTargetFieldMetadataIdToViewFilterFastInstanceCommand,
AddChannelSyncStageIndexesFastInstanceCommand,
FinalizeRolePermissionFlagCutoverFastInstanceCommand,
AddLogoToApplicationRegistrationFastInstanceCommand,
];
@@ -2,10 +2,25 @@ import { Context, Parent, ResolveField } from '@nestjs/graphql';
import { AdminResolver } from 'src/engine/api/graphql/graphql-config/decorators/admin-resolver.decorator';
import { ApplicationRegistrationEntity } from 'src/engine/core-modules/application/application-registration/application-registration.entity';
import { ApplicationRegistrationLogoService } from 'src/engine/core-modules/application/application-registration/application-registration-logo.service';
import { type IDataloaders } from 'src/engine/dataloaders/dataloader.interface';
import { FileOutputDTO } from 'src/engine/core-modules/file/dtos/file-output.dto';
@AdminResolver(() => ApplicationRegistrationEntity)
export class AdminPanelApplicationRegistrationResolver {
constructor(
private readonly applicationRegistrationLogoService: ApplicationRegistrationLogoService,
) {}
@ResolveField(() => FileOutputDTO, { nullable: true })
async logo(
@Parent() registration: ApplicationRegistrationEntity,
): Promise<FileOutputDTO | null> {
return this.applicationRegistrationLogoService.resolveLogoFile(
registration,
);
}
@ResolveField(() => Boolean)
async isConfigured(
@Parent() registration: ApplicationRegistrationEntity,
@@ -22,7 +22,6 @@ import { UploadApplicationFileInput } from 'src/engine/core-modules/application/
import { WorkspaceMigrationDTO } from 'src/engine/core-modules/application/application-development/dtos/workspace-migration.dto';
import { ApplicationExceptionFilter } from 'src/engine/core-modules/application/application-exception-filter';
import { ApplicationSyncService } from 'src/engine/core-modules/application/application-manifest/application-sync.service';
import { resolveManifestAssetUrls } from 'src/engine/core-modules/application/application-marketplace/utils/resolve-manifest-asset-urls.util';
import { ApplicationTokenPairDTO } from 'src/engine/core-modules/application/application-oauth/dtos/application-token-pair.dto';
import { ApplicationRegistrationVariableService } from 'src/engine/core-modules/application/application-registration-variable/application-registration-variable.service';
import { ApplicationRegistrationService } from 'src/engine/core-modules/application/application-registration/application-registration.service';
@@ -38,7 +37,6 @@ import { FileDTO } from 'src/engine/core-modules/file/dtos/file.dto';
import { ResolverValidationPipe } from 'src/engine/core-modules/graphql/pipes/resolver-validation.pipe';
import { SdkClientGenerationService } from 'src/engine/core-modules/sdk-client/sdk-client-generation.service';
import { ThrottlerService } from 'src/engine/core-modules/throttler/throttler.service';
import { TwentyConfigService } from 'src/engine/core-modules/twenty-config/twenty-config.service';
import { type WorkspaceEntity } from 'src/engine/core-modules/workspace/workspace.entity';
import { AuthUserWorkspaceId } from 'src/engine/decorators/auth/auth-user-workspace-id.decorator';
import { AuthUser } from 'src/engine/decorators/auth/auth-user.decorator';
@@ -68,7 +66,6 @@ export class ApplicationDevelopmentResolver {
private readonly applicationRegistrationVariableService: ApplicationRegistrationVariableService,
private readonly fileStorageService: FileStorageService,
private readonly sdkClientGenerationService: SdkClientGenerationService,
private readonly twentyConfigService: TwentyConfigService,
private readonly throttlerService: ThrottlerService,
) {}
@@ -172,12 +169,7 @@ export class ApplicationDevelopmentResolver {
});
}
await this.syncRegistrationMetadata(
applicationRegistrationId,
manifest,
workspaceId,
application.id,
);
await this.syncRegistrationMetadata(applicationRegistrationId, manifest);
return {
applicationUniversalIdentifier:
@@ -279,20 +271,11 @@ export class ApplicationDevelopmentResolver {
private async syncRegistrationMetadata(
applicationRegistrationId: string,
manifest: ApplicationInput['manifest'],
workspaceId: string,
applicationId: string,
): Promise<void> {
const serverUrl = this.twentyConfigService.get('SERVER_URL');
const manifestWithResolvedUrls = resolveManifestAssetUrls(
manifest,
(filePath) =>
`${serverUrl}/public-assets/${workspaceId}/${applicationId}/${filePath}`,
);
await this.applicationRegistrationService.updateFromManifest(
applicationRegistrationId,
manifestWithResolvedUrls,
manifest,
ApplicationRegistrationSourceType.LOCAL,
);
if (manifest.application.serverVariables) {
@@ -9,6 +9,7 @@ import { ApplicationManifestModule } from 'src/engine/core-modules/application/a
import { ApplicationPackageModule } from 'src/engine/core-modules/application/application-package/application-package.module';
import { ApplicationInstallResolver } from 'src/engine/core-modules/application/application-install/application-install.resolver';
import { ApplicationInstallService } from 'src/engine/core-modules/application/application-install/application-install.service';
import { ApplicationLogoResolver } from 'src/engine/core-modules/application/application-install/application-logo.resolver';
import { FileStorageModule } from 'src/engine/core-modules/file-storage/file-storage.module';
import { LogicFunctionModule } from 'src/engine/core-modules/logic-function/logic-function.module';
import { SdkClientModule } from 'src/engine/core-modules/sdk-client/sdk-client.module';
@@ -29,7 +30,11 @@ import { WorkspaceCacheModule } from 'src/engine/workspace-cache/workspace-cache
FileStorageModule,
WorkspaceCacheModule,
],
providers: [ApplicationInstallResolver, ApplicationInstallService],
providers: [
ApplicationInstallResolver,
ApplicationInstallService,
ApplicationLogoResolver,
],
exports: [ApplicationInstallService],
})
export class ApplicationInstallModule {}
@@ -0,0 +1,33 @@
import { Parent, ResolveField } from '@nestjs/graphql';
import { MetadataResolver } from 'src/engine/api/graphql/graphql-config/decorators/metadata-resolver.decorator';
import { ApplicationDTO } from 'src/engine/core-modules/application/dtos/application.dto';
import { resolveApplicationLogoUrl } from 'src/engine/core-modules/application/utils/resolve-application-logo-url.util';
import { FileOutputDTO } from 'src/engine/core-modules/file/dtos/file-output.dto';
import { buildFileOutputFromUrl } from 'src/engine/core-modules/file/utils/build-file-output-from-url.util';
import { TwentyConfigService } from 'src/engine/core-modules/twenty-config/twenty-config.service';
@MetadataResolver(() => ApplicationDTO)
export class ApplicationLogoResolver {
constructor(private readonly twentyConfigService: TwentyConfigService) {}
@ResolveField(() => FileOutputDTO, { nullable: true })
logo(
@Parent() application: ApplicationDTO & { workspaceId?: string },
): FileOutputDTO | null {
if (!application.workspaceId) {
return buildFileOutputFromUrl(application.logo ?? null);
}
const serverUrl = this.twentyConfigService.get('SERVER_URL');
return buildFileOutputFromUrl(
resolveApplicationLogoUrl({
logo: application.logo,
serverUrl,
workspaceId: application.workspaceId,
applicationId: application.id,
}),
);
}
}
@@ -5,6 +5,7 @@ import { GraphQLJSON } from 'graphql-type-json';
import { type Manifest } from 'twenty-shared/application';
import { ApplicationRegistrationSourceType } from 'src/engine/core-modules/application/application-registration/enums/application-registration-source-type.enum';
import { FileOutputDTO } from 'src/engine/core-modules/file/dtos/file-output.dto';
@ObjectType('MarketplaceAppDetail')
export class MarketplaceAppDetailDTO {
@@ -36,6 +37,10 @@ export class MarketplaceAppDetailDTO {
@Field({ nullable: true })
latestAvailableVersion?: string;
@IsOptional()
@Field(() => FileOutputDTO, { nullable: true })
logo?: FileOutputDTO | null;
@IsBoolean()
@Field(() => Boolean)
isListed: boolean;
@@ -3,9 +3,6 @@ import { Injectable, Logger } from '@nestjs/common';
import { ApplicationRegistrationService } from 'src/engine/core-modules/application/application-registration/application-registration.service';
import { ApplicationRegistrationSourceType } from 'src/engine/core-modules/application/application-registration/enums/application-registration-source-type.enum';
import { MarketplaceService } from 'src/engine/core-modules/application/application-marketplace/marketplace.service';
import { buildRegistryCdnUrl } from 'src/engine/core-modules/application/application-marketplace/utils/build-registry-cdn-url.util';
import { resolveManifestAssetUrls } from 'src/engine/core-modules/application/application-marketplace/utils/resolve-manifest-asset-urls.util';
import { TwentyConfigService } from 'src/engine/core-modules/twenty-config/twenty-config.service';
@Injectable()
export class MarketplaceCatalogSyncService {
@@ -14,7 +11,6 @@ export class MarketplaceCatalogSyncService {
constructor(
private readonly applicationRegistrationService: ApplicationRegistrationService,
private readonly marketplaceService: MarketplaceService,
private readonly twentyConfigService: TwentyConfigService,
) {}
async syncCatalog(): Promise<void> {
@@ -59,26 +55,13 @@ export class MarketplaceCatalogSyncService {
}
: fetchedManifest;
const cdnBaseUrl = this.twentyConfigService.get('APP_REGISTRY_CDN_URL');
const manifestWithResolvedUrls = resolveManifestAssetUrls(
manifest,
(filePath) =>
buildRegistryCdnUrl({
cdnBaseUrl,
packageName: pkg.name,
version: pkg.version,
filePath,
}),
);
await this.applicationRegistrationService.upsertFromCatalog({
universalIdentifier,
name: manifest.application.displayName ?? pkg.name,
sourceType: ApplicationRegistrationSourceType.NPM,
sourcePackage: pkg.name,
latestAvailableVersion: pkg.version ?? null,
manifest: manifestWithResolvedUrls,
manifest,
});
} catch (error) {
this.logger.error(
@@ -12,9 +12,12 @@ import { ApplicationRegistrationService } from 'src/engine/core-modules/applicat
import { MarketplaceCatalogSyncCronJob } from 'src/engine/core-modules/application/application-marketplace/crons/marketplace-catalog-sync.cron.job';
import { MarketplaceAppDTO } from 'src/engine/core-modules/application/application-marketplace/dtos/marketplace-app.dto';
import { MarketplaceAppDetailDTO } from 'src/engine/core-modules/application/application-marketplace/dtos/marketplace-app-detail.dto';
import { resolveApplicationRegistrationLogoUrl } from 'src/engine/core-modules/application/utils/resolve-application-registration-logo-url.util';
import { buildFileOutputFromUrl } from 'src/engine/core-modules/file/utils/build-file-output-from-url.util';
import { InjectMessageQueue } from 'src/engine/core-modules/message-queue/decorators/message-queue.decorator';
import { MessageQueue } from 'src/engine/core-modules/message-queue/message-queue.constants';
import { MessageQueueService } from 'src/engine/core-modules/message-queue/services/message-queue.service';
import { TwentyConfigService } from 'src/engine/core-modules/twenty-config/twenty-config.service';
@Injectable()
export class MarketplaceQueryService {
@@ -24,6 +27,7 @@ export class MarketplaceQueryService {
constructor(
private readonly applicationRegistrationService: ApplicationRegistrationService,
private readonly applicationRegistrationVariableService: ApplicationRegistrationVariableService,
private readonly twentyConfigService: TwentyConfigService,
@InjectMessageQueue(MessageQueue.cronQueue)
private readonly messageQueueService: MessageQueueService,
) {}
@@ -96,7 +100,14 @@ export class MarketplaceQueryService {
description: app?.description ?? '',
author: `${app?.author ?? 'Unknown'}`,
category: app?.category ?? '',
logo: app?.logoUrl ?? undefined,
logo:
resolveApplicationRegistrationLogoUrl({
logo: app?.logoUrl ?? null,
sourceType: registration.sourceType,
sourcePackage: registration.sourcePackage,
latestAvailableVersion: registration.latestAvailableVersion,
cdnBaseUrl: this.twentyConfigService.get('APP_REGISTRY_CDN_URL'),
}) ?? undefined,
sourcePackage: registration.sourcePackage ?? undefined,
isFeatured: registration.isFeatured,
};
@@ -112,6 +123,15 @@ export class MarketplaceQueryService {
sourceType: registration.sourceType,
sourcePackage: registration.sourcePackage ?? undefined,
latestAvailableVersion: registration.latestAvailableVersion ?? undefined,
logo: buildFileOutputFromUrl(
resolveApplicationRegistrationLogoUrl({
logo: registration.manifest?.application?.logoUrl ?? null,
sourceType: registration.sourceType,
sourcePackage: registration.sourcePackage,
latestAvailableVersion: registration.latestAvailableVersion,
cdnBaseUrl: this.twentyConfigService.get('APP_REGISTRY_CDN_URL'),
}),
),
isListed: registration.isListed,
isFeatured: registration.isFeatured,
manifest: registration.manifest ?? undefined,
@@ -0,0 +1,74 @@
import { Injectable } from '@nestjs/common';
import { FileFolder } from 'twenty-shared/types';
import { ApplicationRegistrationEntity } from 'src/engine/core-modules/application/application-registration/application-registration.entity';
import { ApplicationRegistrationSourceType } from 'src/engine/core-modules/application/application-registration/enums/application-registration-source-type.enum';
import { ApplicationService } from 'src/engine/core-modules/application/application.service';
import { resolveApplicationLogoUrl } from 'src/engine/core-modules/application/utils/resolve-application-logo-url.util';
import { resolveApplicationRegistrationLogoUrl } from 'src/engine/core-modules/application/utils/resolve-application-registration-logo-url.util';
import { type FileOutputDTO } from 'src/engine/core-modules/file/dtos/file-output.dto';
import { FileUrlService } from 'src/engine/core-modules/file/file-url/file-url.service';
import { buildFileOutputFromUrl } from 'src/engine/core-modules/file/utils/build-file-output-from-url.util';
import { TwentyConfigService } from 'src/engine/core-modules/twenty-config/twenty-config.service';
@Injectable()
export class ApplicationRegistrationLogoService {
constructor(
private readonly twentyConfigService: TwentyConfigService,
private readonly applicationService: ApplicationService,
private readonly fileUrlService: FileUrlService,
) {}
async resolveLogoFile(
registration: ApplicationRegistrationEntity,
): Promise<FileOutputDTO | null> {
if (
registration.sourceType === ApplicationRegistrationSourceType.TARBALL &&
registration.logoFileId
) {
const url = await this.fileUrlService.signFileByIdUrl({
fileId: registration.logoFileId,
workspaceId: registration.ownerWorkspaceId ?? '',
fileFolder: FileFolder.AppTarball,
});
return buildFileOutputFromUrl(url, registration.logoFileId);
}
const manifestLogo = registration.manifest?.application?.logoUrl ?? null;
if (
registration.sourceType === ApplicationRegistrationSourceType.LOCAL &&
manifestLogo &&
registration.ownerWorkspaceId
) {
const application =
await this.applicationService.findByUniversalIdentifier({
universalIdentifier: registration.universalIdentifier,
workspaceId: registration.ownerWorkspaceId,
});
if (application) {
return buildFileOutputFromUrl(
resolveApplicationLogoUrl({
logo: manifestLogo,
serverUrl: this.twentyConfigService.get('SERVER_URL'),
workspaceId: registration.ownerWorkspaceId,
applicationId: application.id,
}),
);
}
}
return buildFileOutputFromUrl(
resolveApplicationRegistrationLogoUrl({
logo: manifestLogo,
sourceType: registration.sourceType,
sourcePackage: registration.sourcePackage,
latestAvailableVersion: registration.latestAvailableVersion,
cdnBaseUrl: this.twentyConfigService.get('APP_REGISTRY_CDN_URL'),
}),
);
}
}
@@ -0,0 +1,23 @@
import { Parent, ResolveField } from '@nestjs/graphql';
import { MetadataResolver } from 'src/engine/api/graphql/graphql-config/decorators/metadata-resolver.decorator';
import { ApplicationRegistrationEntity } from 'src/engine/core-modules/application/application-registration/application-registration.entity';
import { ApplicationRegistrationLogoService } from 'src/engine/core-modules/application/application-registration/application-registration-logo.service';
import { ApplicationRegistrationSummaryDTO } from 'src/engine/core-modules/application/application-registration/dtos/application-registration-summary.dto';
import { FileOutputDTO } from 'src/engine/core-modules/file/dtos/file-output.dto';
@MetadataResolver(() => ApplicationRegistrationSummaryDTO)
export class ApplicationRegistrationSummaryResolver {
constructor(
private readonly applicationRegistrationLogoService: ApplicationRegistrationLogoService,
) {}
@ResolveField(() => FileOutputDTO, { nullable: true })
async logo(
@Parent() registration: ApplicationRegistrationEntity,
): Promise<FileOutputDTO | null> {
return this.applicationRegistrationLogoService.resolveLogoFile(
registration,
);
}
}
@@ -21,6 +21,7 @@ import { UUIDScalarType } from 'src/engine/api/graphql/workspace-schema-builder/
import { type Manifest } from 'twenty-shared/application';
import { ApplicationRegistrationVariableEntity } from 'src/engine/core-modules/application/application-registration-variable/application-registration-variable.entity';
import { ApplicationRegistrationSourceType } from 'src/engine/core-modules/application/application-registration/enums/application-registration-source-type.enum';
import { WasIntroducedInUpgrade } from 'src/engine/core-modules/upgrade/decorators/was-introduced-in-upgrade.decorator';
import { FileEntity } from 'src/engine/core-modules/file/entities/file.entity';
import { UserEntity } from 'src/engine/core-modules/user/user.entity';
import { WorkspaceEntity } from 'src/engine/core-modules/workspace/workspace.entity';
@@ -133,10 +134,16 @@ export class ApplicationRegistrationEntity {
@Column({ type: 'jsonb', nullable: true })
manifest: Manifest | null;
@Field(() => String, { nullable: true })
get logoUrl(): string | null {
return this.manifest?.application?.logoUrl ?? null;
}
@Column({ nullable: true, type: 'uuid' })
@WasIntroducedInUpgrade({
upgradeCommandName:
'2.8.0_AddLogoToApplicationRegistrationFastInstanceCommand_1779387505162',
})
logoFileId: string | null;
@OneToOne(() => FileEntity, { onDelete: 'SET NULL', nullable: true })
@JoinColumn({ name: 'logoFileId' })
logoFile: Relation<FileEntity> | null;
@OneToMany(
() => ApplicationRegistrationVariableEntity,
@@ -2,8 +2,10 @@ import { Module } from '@nestjs/common';
import { TypeOrmModule } from '@nestjs/typeorm';
import { ApplicationRegistrationEntity } from 'src/engine/core-modules/application/application-registration/application-registration.entity';
import { ApplicationRegistrationLogoService } from 'src/engine/core-modules/application/application-registration/application-registration-logo.service';
import { ApplicationRegistrationResolver } from 'src/engine/core-modules/application/application-registration/application-registration.resolver';
import { ApplicationRegistrationService } from 'src/engine/core-modules/application/application-registration/application-registration.service';
import { ApplicationRegistrationSummaryResolver } from 'src/engine/core-modules/application/application-registration/application-registration-summary.resolver';
import { ApplicationRegistrationVariableModule } from 'src/engine/core-modules/application/application-registration-variable/application-registration-variable.module';
import { ApplicationTarballService } from 'src/engine/core-modules/application/application-registration/application-tarball.service';
import { ApplicationPackageModule } from 'src/engine/core-modules/application/application-package/application-package.module';
@@ -36,11 +38,14 @@ import { WorkspaceCacheStorageModule } from 'src/engine/workspace-cache-storage/
],
providers: [
ApplicationRegistrationService,
ApplicationRegistrationLogoService,
ApplicationRegistrationResolver,
ApplicationRegistrationSummaryResolver,
ApplicationTarballService,
],
exports: [
ApplicationRegistrationService,
ApplicationRegistrationLogoService,
ApplicationRegistrationVariableModule,
],
})
@@ -24,6 +24,7 @@ import { CreateApplicationRegistrationVariableInput } from 'src/engine/core-modu
import { UpdateApplicationRegistrationVariableInput } from 'src/engine/core-modules/application/application-registration-variable/dtos/update-application-registration-variable.input';
import { ApplicationRegistrationExceptionFilter } from 'src/engine/core-modules/application/application-registration/application-registration-exception-filter';
import { ApplicationRegistrationEntity } from 'src/engine/core-modules/application/application-registration/application-registration.entity';
import { ApplicationRegistrationLogoService } from 'src/engine/core-modules/application/application-registration/application-registration-logo.service';
import { ApplicationRegistrationService } from 'src/engine/core-modules/application/application-registration/application-registration.service';
import { ApplicationTarballService } from 'src/engine/core-modules/application/application-registration/application-tarball.service';
import { ApplicationRegistrationStatsDTO } from 'src/engine/core-modules/application/application-registration/dtos/application-registration-stats.dto';
@@ -35,6 +36,7 @@ import { TransferApplicationRegistrationOwnershipInput } from 'src/engine/core-m
import { UpdateApplicationRegistrationInput } from 'src/engine/core-modules/application/application-registration/dtos/update-application-registration.input';
import { ApplicationRegistrationSourceType } from 'src/engine/core-modules/application/application-registration/enums/application-registration-source-type.enum';
import { AuthGraphqlApiExceptionFilter } from 'src/engine/core-modules/auth/filters/auth-graphql-api-exception.filter';
import { FileOutputDTO } from 'src/engine/core-modules/file/dtos/file-output.dto';
import { FileUrlService } from 'src/engine/core-modules/file/file-url/file-url.service';
import { PreventNestToAutoLogGraphqlErrorsFilter } from 'src/engine/core-modules/graphql/filters/prevent-nest-to-auto-log-graphql-errors.filter';
import { ResolverValidationPipe } from 'src/engine/core-modules/graphql/pipes/resolver-validation.pipe';
@@ -64,6 +66,7 @@ import {
export class ApplicationRegistrationResolver {
constructor(
private readonly applicationRegistrationService: ApplicationRegistrationService,
private readonly applicationRegistrationLogoService: ApplicationRegistrationLogoService,
private readonly applicationRegistrationVariableService: ApplicationRegistrationVariableService,
private readonly applicationTarballService: ApplicationTarballService,
private readonly fileUrlService: FileUrlService,
@@ -330,6 +333,15 @@ export class ApplicationRegistrationResolver {
});
}
@ResolveField(() => FileOutputDTO, { nullable: true })
async logo(
@Parent() registration: ApplicationRegistrationEntity,
): Promise<FileOutputDTO | null> {
return this.applicationRegistrationLogoService.resolveLogoFile(
registration,
);
}
@ResolveField(() => Boolean)
async isConfigured(
@Parent() registration: ApplicationRegistrationEntity,
@@ -16,6 +16,9 @@ import {
ApplicationRegistrationException,
ApplicationRegistrationExceptionCode,
} from 'src/engine/core-modules/application/application-registration/application-registration.exception';
import { resolveApplicationRegistrationLogoUrl } from 'src/engine/core-modules/application/utils/resolve-application-registration-logo-url.util';
import { buildFileOutputFromUrl } from 'src/engine/core-modules/file/utils/build-file-output-from-url.util';
import { TwentyConfigService } from 'src/engine/core-modules/twenty-config/twenty-config.service';
import { type ApplicationRegistrationStatsDTO } from 'src/engine/core-modules/application/application-registration/dtos/application-registration-stats.dto';
import { type CreateApplicationRegistrationInput } from 'src/engine/core-modules/application/application-registration/dtos/create-application-registration.input';
import { type PublicApplicationRegistrationDTO } from 'src/engine/core-modules/application/application-registration/dtos/public-application-registration.dto';
@@ -39,6 +42,7 @@ export class ApplicationRegistrationService {
@InjectRepository(WorkspaceEntity)
private readonly workspaceRepository: Repository<WorkspaceEntity>,
private readonly applicationRegistrationVariableService: ApplicationRegistrationVariableService,
private readonly twentyConfigService: TwentyConfigService,
) {}
async findMany(
@@ -104,7 +108,6 @@ export class ApplicationRegistrationService {
): Promise<PublicApplicationRegistrationDTO | null> {
const registration = await this.applicationRegistrationRepository.findOne({
where: { oAuthClientId: clientId },
select: ['id', 'name', 'manifest', 'oAuthScopes'],
});
if (!registration) {
@@ -114,7 +117,15 @@ export class ApplicationRegistrationService {
return {
id: registration.id,
name: registration.name,
logoUrl: registration.manifest?.application?.logoUrl ?? null,
logo: buildFileOutputFromUrl(
resolveApplicationRegistrationLogoUrl({
logo: registration.manifest?.application?.logoUrl ?? null,
sourceType: registration.sourceType,
sourcePackage: registration.sourcePackage,
latestAvailableVersion: registration.latestAvailableVersion,
cdnBaseUrl: this.twentyConfigService.get('APP_REGISTRY_CDN_URL'),
}),
),
websiteUrl: registration.manifest?.application?.websiteUrl ?? null,
oAuthScopes: registration.oAuthScopes,
};
@@ -214,6 +225,7 @@ export class ApplicationRegistrationService {
async updateFromManifest(
applicationRegistrationId: string,
manifest: Manifest,
sourceType?: ApplicationRegistrationSourceType,
): Promise<void> {
const existing = await this.applicationRegistrationRepository.findOneOrFail(
{ where: { id: applicationRegistrationId } },
@@ -223,6 +235,7 @@ export class ApplicationRegistrationService {
...existing,
name: manifest.application.displayName,
manifest,
...(sourceType !== undefined && { sourceType }),
});
}
@@ -3,7 +3,7 @@ import { InjectRepository } from '@nestjs/typeorm';
import { promises as fs } from 'fs';
import { tmpdir } from 'os';
import { join } from 'path';
import { join, resolve, sep } from 'path';
import semver from 'semver';
import { FileFolder } from 'twenty-shared/types';
@@ -28,6 +28,8 @@ import { ApplicationService } from 'src/engine/core-modules/application/applicat
import { FileStorageService } from 'src/engine/core-modules/file-storage/file-storage.service';
import type { ApplicationManifest } from 'twenty-shared/application';
import { ApplicationRegistrationVariableService } from 'src/engine/core-modules/application/application-registration-variable/application-registration-variable.service';
import { extractFileInfo } from 'src/engine/core-modules/file/utils/extract-file-info.utils';
import { sanitizeFile } from 'src/engine/core-modules/file/utils/sanitize-file.utils';
@Injectable()
export class ApplicationTarballService {
@@ -202,10 +204,21 @@ export class ApplicationTarballService {
},
});
const logoFileId = await this.uploadLogoFromTarball({
contentDir,
logoPath: manifest.application?.logoUrl ?? null,
registrationId: appRegistration.id,
existingLogoFileId: appRegistration.logoFileId ?? undefined,
applicationUniversalIdentifier:
workspaceCustomFlatApplication.universalIdentifier,
workspaceId: params.ownerWorkspaceId,
});
await this.appRegistrationRepository.update(appRegistration.id, {
sourceType: ApplicationRegistrationSourceType.TARBALL,
tarballFileId: savedFile.id,
name: manifest.application?.displayName ?? 'Unknown App',
logoFileId,
manifest,
latestAvailableVersion: packageJson?.version ?? null,
isListed: false,
@@ -231,4 +244,59 @@ export class ApplicationTarballService {
await fs.rm(tempDir, { recursive: true, force: true });
}
}
private async uploadLogoFromTarball(params: {
contentDir: string;
logoPath: string | null;
registrationId: string;
existingLogoFileId?: string;
applicationUniversalIdentifier: string;
workspaceId: string;
}): Promise<string | null> {
if (
!params.logoPath ||
params.logoPath.startsWith('http://') ||
params.logoPath.startsWith('https://')
) {
return null;
}
const absolutePath = resolve(params.contentDir, params.logoPath);
const safePrefx = params.contentDir.endsWith(sep)
? params.contentDir
: params.contentDir + sep;
if (!absolutePath.startsWith(safePrefx)) {
return null;
}
let content: Buffer;
try {
content = await fs.readFile(absolutePath);
} catch {
this.logger.warn(`Logo file not found in tarball: ${params.logoPath}`);
return null;
}
const { mimeType, ext } = await extractFileInfo({
file: content,
filename: params.logoPath,
});
const sanitizedContent = sanitizeFile({ file: content, ext, mimeType });
const savedLogoFile = await this.fileStorageService.writeFile({
sourceFile: sanitizedContent,
mimeType,
fileFolder: FileFolder.AppTarball,
applicationUniversalIdentifier: params.applicationUniversalIdentifier,
workspaceId: params.workspaceId,
resourcePath: `${params.registrationId}/logo${ext}`,
fileId: params.existingLogoFileId ?? v4(),
settings: { isTemporaryFile: false, toDelete: false },
});
return savedLogoFile.id;
}
}
@@ -4,6 +4,7 @@ import { IsOptional, IsString } from 'class-validator';
import { UUIDScalarType } from 'src/engine/api/graphql/workspace-schema-builder/graphql-types/scalars';
import { ApplicationRegistrationSourceType } from 'src/engine/core-modules/application/application-registration/enums/application-registration-source-type.enum';
import { FileOutputDTO } from 'src/engine/core-modules/file/dtos/file-output.dto';
@ObjectType('ApplicationRegistrationSummary')
export class ApplicationRegistrationSummaryDTO {
@@ -18,8 +19,6 @@ export class ApplicationRegistrationSummaryDTO {
@Field(() => ApplicationRegistrationSourceType)
sourceType: ApplicationRegistrationSourceType;
@IsOptional()
@IsString()
@Field(() => String, { nullable: true })
logoUrl?: string | null;
@Field(() => FileOutputDTO, { nullable: true })
logo?: FileOutputDTO | null;
}
@@ -1,6 +1,7 @@
import { Field, ObjectType } from '@nestjs/graphql';
import { UUIDScalarType } from 'src/engine/api/graphql/workspace-schema-builder/graphql-types/scalars';
import { FileOutputDTO } from 'src/engine/core-modules/file/dtos/file-output.dto';
@ObjectType('PublicApplicationRegistration')
export class PublicApplicationRegistrationDTO {
@@ -10,8 +11,8 @@ export class PublicApplicationRegistrationDTO {
@Field()
name: string;
@Field(() => String, { nullable: true })
logoUrl: string | null;
@Field(() => FileOutputDTO, { nullable: true })
logo: FileOutputDTO | null;
@Field(() => String, { nullable: true })
websiteUrl: string | null;
@@ -35,9 +35,8 @@ export class ApplicationDTO {
@Field({ nullable: true })
description?: string;
@IsOptional()
@IsString()
@Field({ nullable: true })
// Internal raw value (path or absolute URL); GraphQL exposure as FileOutput
// is handled by ApplicationLogoResolver.
logo?: string;
@IsOptional()
@@ -14,8 +14,9 @@ export const fromFlatApplicationToApplicationDto = ({
availablePackages,
universalIdentifier,
version,
workspaceId,
settingsCustomTabFrontComponentId,
}: FlatApplication): ApplicationDTO => {
}: FlatApplication): ApplicationDTO & { workspaceId: string } => {
return {
canBeUninstalled,
description: description ?? undefined,
@@ -30,6 +31,7 @@ export const fromFlatApplicationToApplicationDto = ({
availablePackages: availablePackages ?? {},
universalIdentifier,
version: version ?? undefined,
workspaceId,
settingsCustomTabFrontComponentId:
settingsCustomTabFrontComponentId ?? undefined,
};
@@ -0,0 +1,20 @@
import { buildPublicAssetUrl } from 'twenty-shared/utils';
export const resolveApplicationLogoUrl = ({
logo,
serverUrl,
workspaceId,
applicationId,
}: {
logo: string | null | undefined;
serverUrl: string;
workspaceId: string;
applicationId: string;
}): string | null => {
return buildPublicAssetUrl({
path: logo,
serverUrl,
workspaceId,
applicationId,
});
};
@@ -0,0 +1,42 @@
import { ApplicationRegistrationSourceType } from 'src/engine/core-modules/application/application-registration/enums/application-registration-source-type.enum';
import { buildRegistryCdnUrl } from 'src/engine/core-modules/application/application-marketplace/utils/build-registry-cdn-url.util';
const isAbsoluteUrl = (url: string): boolean =>
url.startsWith('http://') || url.startsWith('https://');
export const resolveApplicationRegistrationLogoUrl = ({
logo,
sourceType,
sourcePackage,
latestAvailableVersion,
cdnBaseUrl,
}: {
logo: string | null | undefined;
sourceType: ApplicationRegistrationSourceType;
sourcePackage: string | null;
latestAvailableVersion: string | null;
cdnBaseUrl: string;
}): string | null => {
if (!logo) {
return null;
}
if (isAbsoluteUrl(logo)) {
return logo;
}
if (
sourceType === ApplicationRegistrationSourceType.NPM &&
sourcePackage &&
latestAvailableVersion
) {
return buildRegistryCdnUrl({
cdnBaseUrl,
packageName: sourcePackage,
version: latestAvailableVersion,
filePath: logo,
});
}
return null;
};
@@ -0,0 +1,18 @@
import { Field, ObjectType } from '@nestjs/graphql';
import { UUIDScalarType } from 'src/engine/api/graphql/workspace-schema-builder/graphql-types/scalars';
@ObjectType('FileOutput')
export class FileOutputDTO {
@Field(() => UUIDScalarType, { nullable: true })
fileId: string | null;
@Field(() => String, { nullable: true })
label: string | null;
@Field(() => String, { nullable: true })
extension: string | null;
@Field(() => String)
url: string;
}
@@ -0,0 +1,17 @@
import { type FileOutputDTO } from 'src/engine/core-modules/file/dtos/file-output.dto';
export const buildFileOutputFromUrl = (
url: string | null,
fileId: string | null = null,
): FileOutputDTO | null => {
if (url === null) {
return null;
}
return {
fileId,
label: null,
extension: null,
url,
};
};
@@ -644,7 +644,7 @@ export const STANDARD_COMMAND_MENU_ITEMS = {
position: 42,
shortLabel: 'Ask AI',
availabilityType: CommandMenuItemAvailabilityType.GLOBAL,
conditionalAvailabilityExpression: null,
conditionalAvailabilityExpression: 'permissionFlags.AI',
availabilityObjectMetadataUniversalIdentifier: null,
frontComponentUniversalIdentifier: null,
engineComponentKey: EngineComponentKey.ASK_AI,
@@ -658,7 +658,7 @@ export const STANDARD_COMMAND_MENU_ITEMS = {
position: 43,
shortLabel: 'Previous AI Chats',
availabilityType: CommandMenuItemAvailabilityType.GLOBAL,
conditionalAvailabilityExpression: null,
conditionalAvailabilityExpression: 'permissionFlags.AI',
availabilityObjectMetadataUniversalIdentifier: null,
frontComponentUniversalIdentifier: null,
engineComponentKey: EngineComponentKey.VIEW_PREVIOUS_AI_CHATS,
@@ -687,7 +687,7 @@ export const STANDARD_COMMAND_MENU_ITEMS = {
position: 45,
shortLabel: 'Compose',
availabilityType: CommandMenuItemAvailabilityType.GLOBAL,
conditionalAvailabilityExpression: null,
conditionalAvailabilityExpression: 'permissionFlags.SEND_EMAIL_TOOL',
availabilityObjectMetadataUniversalIdentifier: null,
frontComponentUniversalIdentifier: null,
engineComponentKey: EngineComponentKey.COMPOSE_EMAIL,
@@ -731,7 +731,7 @@ export const STANDARD_COMMAND_MENU_ITEMS = {
position: 48,
shortLabel: 'Accounts',
availabilityType: CommandMenuItemAvailabilityType.GLOBAL,
conditionalAvailabilityExpression: null,
conditionalAvailabilityExpression: 'permissionFlags.CONNECTED_ACCOUNTS',
availabilityObjectMetadataUniversalIdentifier: null,
frontComponentUniversalIdentifier: null,
engineComponentKey: EngineComponentKey.NAVIGATION,
@@ -746,7 +746,7 @@ export const STANDARD_COMMAND_MENU_ITEMS = {
position: 49,
shortLabel: 'Emails',
availabilityType: CommandMenuItemAvailabilityType.GLOBAL,
conditionalAvailabilityExpression: null,
conditionalAvailabilityExpression: 'permissionFlags.CONNECTED_ACCOUNTS',
availabilityObjectMetadataUniversalIdentifier: null,
frontComponentUniversalIdentifier: null,
engineComponentKey: EngineComponentKey.NAVIGATION,
@@ -761,7 +761,7 @@ export const STANDARD_COMMAND_MENU_ITEMS = {
position: 50,
shortLabel: 'Calendars',
availabilityType: CommandMenuItemAvailabilityType.GLOBAL,
conditionalAvailabilityExpression: null,
conditionalAvailabilityExpression: 'permissionFlags.CONNECTED_ACCOUNTS',
availabilityObjectMetadataUniversalIdentifier: null,
frontComponentUniversalIdentifier: null,
engineComponentKey: EngineComponentKey.NAVIGATION,
@@ -776,7 +776,7 @@ export const STANDARD_COMMAND_MENU_ITEMS = {
position: 51,
shortLabel: 'General',
availabilityType: CommandMenuItemAvailabilityType.GLOBAL,
conditionalAvailabilityExpression: null,
conditionalAvailabilityExpression: 'permissionFlags.WORKSPACE',
availabilityObjectMetadataUniversalIdentifier: null,
frontComponentUniversalIdentifier: null,
engineComponentKey: EngineComponentKey.NAVIGATION,
@@ -791,7 +791,7 @@ export const STANDARD_COMMAND_MENU_ITEMS = {
position: 52,
shortLabel: 'Data Model',
availabilityType: CommandMenuItemAvailabilityType.GLOBAL,
conditionalAvailabilityExpression: null,
conditionalAvailabilityExpression: 'permissionFlags.DATA_MODEL',
availabilityObjectMetadataUniversalIdentifier: null,
frontComponentUniversalIdentifier: null,
engineComponentKey: EngineComponentKey.NAVIGATION,
@@ -806,7 +806,7 @@ export const STANDARD_COMMAND_MENU_ITEMS = {
position: 53,
shortLabel: 'Members',
availabilityType: CommandMenuItemAvailabilityType.GLOBAL,
conditionalAvailabilityExpression: null,
conditionalAvailabilityExpression: 'permissionFlags.WORKSPACE_MEMBERS',
availabilityObjectMetadataUniversalIdentifier: null,
frontComponentUniversalIdentifier: null,
engineComponentKey: EngineComponentKey.NAVIGATION,
@@ -821,7 +821,7 @@ export const STANDARD_COMMAND_MENU_ITEMS = {
position: 54,
shortLabel: 'Roles',
availabilityType: CommandMenuItemAvailabilityType.GLOBAL,
conditionalAvailabilityExpression: null,
conditionalAvailabilityExpression: 'permissionFlags.WORKSPACE_MEMBERS',
availabilityObjectMetadataUniversalIdentifier: null,
frontComponentUniversalIdentifier: null,
engineComponentKey: EngineComponentKey.NAVIGATION,
@@ -836,7 +836,7 @@ export const STANDARD_COMMAND_MENU_ITEMS = {
position: 55,
shortLabel: 'Domains',
availabilityType: CommandMenuItemAvailabilityType.GLOBAL,
conditionalAvailabilityExpression: null,
conditionalAvailabilityExpression: 'permissionFlags.WORKSPACE',
availabilityObjectMetadataUniversalIdentifier: null,
frontComponentUniversalIdentifier: null,
engineComponentKey: EngineComponentKey.NAVIGATION,
@@ -851,7 +851,7 @@ export const STANDARD_COMMAND_MENU_ITEMS = {
position: 56,
shortLabel: 'Billing',
availabilityType: CommandMenuItemAvailabilityType.GLOBAL,
conditionalAvailabilityExpression: null,
conditionalAvailabilityExpression: 'permissionFlags.WORKSPACE',
availabilityObjectMetadataUniversalIdentifier: null,
frontComponentUniversalIdentifier: null,
engineComponentKey: EngineComponentKey.NAVIGATION,
@@ -866,7 +866,7 @@ export const STANDARD_COMMAND_MENU_ITEMS = {
position: 57,
shortLabel: 'APIs & Webhooks',
availabilityType: CommandMenuItemAvailabilityType.GLOBAL,
conditionalAvailabilityExpression: null,
conditionalAvailabilityExpression: 'permissionFlags.API_KEYS_AND_WEBHOOKS',
availabilityObjectMetadataUniversalIdentifier: null,
frontComponentUniversalIdentifier: null,
engineComponentKey: EngineComponentKey.NAVIGATION,
@@ -881,7 +881,7 @@ export const STANDARD_COMMAND_MENU_ITEMS = {
position: 58,
shortLabel: 'Apps',
availabilityType: CommandMenuItemAvailabilityType.GLOBAL,
conditionalAvailabilityExpression: null,
conditionalAvailabilityExpression: 'permissionFlags.APPLICATIONS',
availabilityObjectMetadataUniversalIdentifier: null,
frontComponentUniversalIdentifier: null,
engineComponentKey: EngineComponentKey.NAVIGATION,
@@ -896,7 +896,7 @@ export const STANDARD_COMMAND_MENU_ITEMS = {
position: 59,
shortLabel: 'AI',
availabilityType: CommandMenuItemAvailabilityType.GLOBAL,
conditionalAvailabilityExpression: null,
conditionalAvailabilityExpression: 'permissionFlags.WORKSPACE',
availabilityObjectMetadataUniversalIdentifier: null,
frontComponentUniversalIdentifier: null,
engineComponentKey: EngineComponentKey.NAVIGATION,
@@ -911,7 +911,7 @@ export const STANDARD_COMMAND_MENU_ITEMS = {
position: 60,
shortLabel: 'Security',
availabilityType: CommandMenuItemAvailabilityType.GLOBAL,
conditionalAvailabilityExpression: null,
conditionalAvailabilityExpression: 'permissionFlags.SECURITY',
availabilityObjectMetadataUniversalIdentifier: null,
frontComponentUniversalIdentifier: null,
engineComponentKey: EngineComponentKey.NAVIGATION,
@@ -926,7 +926,8 @@ export const STANDARD_COMMAND_MENU_ITEMS = {
position: 61,
shortLabel: 'Admin Panel',
availabilityType: CommandMenuItemAvailabilityType.GLOBAL,
conditionalAvailabilityExpression: null,
conditionalAvailabilityExpression:
'canImpersonate or canAccessFullAdminPanel',
availabilityObjectMetadataUniversalIdentifier: null,
frontComponentUniversalIdentifier: null,
engineComponentKey: EngineComponentKey.NAVIGATION,
@@ -956,7 +957,8 @@ export const STANDARD_COMMAND_MENU_ITEMS = {
position: 63,
shortLabel: 'Send Email',
availabilityType: CommandMenuItemAvailabilityType.RECORD_SELECTION,
conditionalAvailabilityExpression: 'numberOfSelectedRecords >= 1',
conditionalAvailabilityExpression:
'numberOfSelectedRecords >= 1 and permissionFlags.SEND_EMAIL_TOOL',
availabilityObjectMetadataUniversalIdentifier:
STANDARD_OBJECTS.person.universalIdentifier,
frontComponentUniversalIdentifier: null,
@@ -971,7 +973,8 @@ export const STANDARD_COMMAND_MENU_ITEMS = {
position: 64,
shortLabel: 'Send Email',
availabilityType: CommandMenuItemAvailabilityType.RECORD_SELECTION,
conditionalAvailabilityExpression: 'numberOfSelectedRecords == 1',
conditionalAvailabilityExpression:
'numberOfSelectedRecords == 1 and permissionFlags.SEND_EMAIL_TOOL',
availabilityObjectMetadataUniversalIdentifier:
STANDARD_OBJECTS.company.universalIdentifier,
frontComponentUniversalIdentifier: null,
@@ -986,7 +989,8 @@ export const STANDARD_COMMAND_MENU_ITEMS = {
position: 65,
shortLabel: 'Send Email',
availabilityType: CommandMenuItemAvailabilityType.RECORD_SELECTION,
conditionalAvailabilityExpression: 'numberOfSelectedRecords == 1',
conditionalAvailabilityExpression:
'numberOfSelectedRecords == 1 and permissionFlags.SEND_EMAIL_TOOL',
availabilityObjectMetadataUniversalIdentifier:
STANDARD_OBJECTS.opportunity.universalIdentifier,
frontComponentUniversalIdentifier: null,
@@ -17,6 +17,8 @@ export type CommandMenuContextApi = {
permissionFlags: Record<string, boolean>;
targetObjectReadPermissions: Record<string, boolean>;
targetObjectWritePermissions: Record<string, boolean>;
canImpersonate: boolean;
canAccessFullAdminPanel: boolean;
objectMetadataItem: Record<string, unknown>;
objectMetadataLabel: string;
};
@@ -27,6 +27,8 @@ const buildContext = (
permissionFlags: {},
targetObjectReadPermissions: {},
targetObjectWritePermissions: {},
canImpersonate: false,
canAccessFullAdminPanel: false,
objectMetadataItem: {},
objectMetadataLabel: '',
...overrides,
@@ -27,6 +27,8 @@ const buildContext = (
permissionFlags: {},
targetObjectReadPermissions: {},
targetObjectWritePermissions: {},
canImpersonate: false,
canAccessFullAdminPanel: false,
objectMetadataItem: {},
objectMetadataLabel: '',
...overrides,
@@ -199,6 +199,7 @@ export { isRecordGqlOperationSignature } from './typeguard/isRecordGqlOperationS
export { throwIfNotDefined } from './typeguard/throwIfNotDefined';
export { formatUpgradeCommandName } from './upgrade/formatUpgradeCommandName';
export { absoluteUrlSchema } from './url/absoluteUrlSchema';
export { buildPublicAssetUrl } from './url/buildPublicAssetUrl';
export { buildSignedPath } from './url/buildSignedPath';
export { ensureAbsoluteUrl } from './url/ensureAbsoluteUrl';
export { getAbsoluteUrlOrThrow } from './url/getAbsoluteUrlOrThrow';
@@ -0,0 +1,26 @@
const isAbsoluteUrl = (url: string): boolean =>
url.startsWith('http://') || url.startsWith('https://');
export const buildPublicAssetUrl = ({
path,
serverUrl,
workspaceId,
applicationId,
}: {
path: string | null | undefined;
serverUrl: string;
workspaceId: string;
applicationId: string;
}): string | null => {
if (!path) {
return null;
}
if (isAbsoluteUrl(path)) {
return path;
}
const baseUrl = serverUrl.endsWith('/') ? serverUrl.slice(0, -1) : serverUrl;
return `${baseUrl}/public-assets/${workspaceId}/${applicationId}/${path}`;
};
@@ -1,4 +1,5 @@
export * from './absoluteUrlSchema';
export * from './buildPublicAssetUrl';
export * from './getSafeUrl';
export * from './isSafeUrl';
export * from './ensureAbsoluteUrl';