Compare commits
44
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
2c63371e7c | ||
|
|
827f24df2b | ||
|
|
d5ff9eb515 | ||
|
|
71a7a3c42d | ||
|
|
cbac2ba0bf | ||
|
|
4452b0f03d | ||
|
|
57f13c9b92 | ||
|
|
72c0c36db5 | ||
|
|
77514ad14a | ||
|
|
3d5c6cb0e5 | ||
|
|
4b12fda3f5 | ||
|
|
9fddaf53d5 | ||
|
|
3512849004 | ||
|
|
72ce77864e | ||
|
|
6cd069ce40 | ||
|
|
db4a05301a | ||
|
|
f9e3683518 | ||
|
|
fecea1bfae | ||
|
|
3281d37bdf | ||
|
|
291ce5ccdb | ||
|
|
a9634b027e | ||
|
|
ae41751a8c | ||
|
|
11d8679f65 | ||
|
|
05f31c1837 | ||
|
|
2a92f34d06 | ||
|
|
d9b125efc5 | ||
|
|
e6b7f31fea | ||
|
|
bad1f20012 | ||
|
|
ce8ef261c1 | ||
|
|
1d3d3999e2 | ||
|
|
89579f5225 | ||
|
|
132d997474 | ||
|
|
7c252ff233 | ||
|
|
4ba9c0ca0b | ||
|
|
d03480472c | ||
|
|
8f3c336e62 | ||
|
|
e7a1448414 | ||
|
|
fc74938d7b | ||
|
|
d5e65c563e | ||
|
|
5c8ddb0c12 | ||
|
|
db0547f503 | ||
|
|
01535a3b3e | ||
|
|
45ac3e8218 | ||
|
|
cd09690d5d |
@@ -1,201 +0,0 @@
|
||||
name: CI Hello world App E2E
|
||||
|
||||
on:
|
||||
# Temporarily disabled — will be re-enabled when example apps are published.
|
||||
# push:
|
||||
# branches:
|
||||
# - main
|
||||
#
|
||||
# pull_request:
|
||||
workflow_dispatch:
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
concurrency:
|
||||
group: ${{ github.workflow }}-${{ github.ref }}
|
||||
cancel-in-progress: ${{ github.ref != 'refs/heads/main' }}
|
||||
|
||||
jobs:
|
||||
changed-files-check:
|
||||
uses: ./.github/workflows/changed-files.yaml
|
||||
with:
|
||||
files: |
|
||||
packages/create-twenty-app/**
|
||||
packages/twenty-sdk/**
|
||||
packages/twenty-client-sdk/**
|
||||
packages/twenty-shared/**
|
||||
!packages/create-twenty-app/package.json
|
||||
!packages/twenty-sdk/package.json
|
||||
!packages/twenty-client-sdk/package.json
|
||||
!packages/twenty-shared/package.json
|
||||
create-app-e2e-hello-world:
|
||||
needs: changed-files-check
|
||||
if: needs.changed-files-check.outputs.any_changed == 'true'
|
||||
timeout-minutes: 30
|
||||
runs-on: ubuntu-latest-4-cores
|
||||
services:
|
||||
postgres:
|
||||
image: postgres:18
|
||||
env:
|
||||
POSTGRES_USER: postgres
|
||||
POSTGRES_PASSWORD: postgres
|
||||
ports:
|
||||
- 5432:5432
|
||||
options: >-
|
||||
--health-cmd pg_isready
|
||||
--health-interval 10s
|
||||
--health-timeout 5s
|
||||
--health-retries 5
|
||||
redis:
|
||||
image: redis
|
||||
ports:
|
||||
- 6379:6379
|
||||
env:
|
||||
PUBLISHABLE_PACKAGES: twenty-client-sdk twenty-sdk create-twenty-app
|
||||
TWENTY_API_URL: http://localhost:3000
|
||||
TWENTY_API_KEY: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIyMDIwMjAyMC1lNmI1LTQ2ODAtOGEzMi1iODIwOTczNzE1NmIiLCJ1c2VySWQiOiIyMDIwMjAyMC1lNmI1LTQ2ODAtOGEzMi1iODIwOTczNzE1NmIiLCJ3b3Jrc3BhY2VJZCI6IjIwMjAyMDIwLTFjMjUtNGQwMi1iZjI1LTZhZWNjZjdlYTQxOSIsIndvcmtzcGFjZU1lbWJlcklkIjoiMjAyMDIwMjAtNDYzZi00MzViLTgyOGMtMTA3ZTAwN2EyNzExIiwidXNlcldvcmtzcGFjZUlkIjoiMjAyMDIwMjAtMWU3Yy00M2Q5LWE1ZGItNjg1YjUwNjlkODE2IiwidHlwZSI6IkFDQ0VTUyIsImF1dGhQcm92aWRlciI6InBhc3N3b3JkIiwiaWF0IjoxNzUxMjgxNzA0LCJleHAiOjIwNjY4NTc3MDR9.HMGqCsVlOAPVUBhKSGlD1X86VoHKt4LIUtET3CGIdik
|
||||
steps:
|
||||
- name: Fetch custom Github Actions and base branch history
|
||||
uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4.3.1
|
||||
with:
|
||||
fetch-depth: 10
|
||||
|
||||
- name: Install dependencies
|
||||
uses: ./.github/actions/yarn-install
|
||||
|
||||
- name: Set CI version and prepare packages for publish
|
||||
run: |
|
||||
CI_VERSION="0.0.0-ci.$(date +%s)"
|
||||
echo "CI_VERSION=$CI_VERSION" >> $GITHUB_ENV
|
||||
for pkg in $PUBLISHABLE_PACKAGES; do
|
||||
npx nx run $pkg:set-local-version --releaseVersion=$CI_VERSION
|
||||
done
|
||||
|
||||
- name: Build packages
|
||||
run: |
|
||||
for pkg in $PUBLISHABLE_PACKAGES; do
|
||||
npx nx build $pkg
|
||||
done
|
||||
|
||||
- name: Install and start Verdaccio
|
||||
run: |
|
||||
npx verdaccio --config .github/verdaccio-config.yaml &
|
||||
|
||||
for i in $(seq 1 30); do
|
||||
if curl -s http://localhost:4873 > /dev/null 2>&1; then
|
||||
echo "Verdaccio is ready"
|
||||
break
|
||||
fi
|
||||
echo "Waiting for Verdaccio... ($i/30)"
|
||||
sleep 1
|
||||
done
|
||||
|
||||
- name: Publish packages to local registry
|
||||
run: |
|
||||
yarn config set npmRegistryServer http://localhost:4873
|
||||
yarn config set unsafeHttpWhitelist --json '["localhost"]'
|
||||
yarn config set npmAuthToken ci-auth-token
|
||||
|
||||
for pkg in $PUBLISHABLE_PACKAGES; do
|
||||
cd packages/$pkg
|
||||
yarn npm publish --tag ci
|
||||
cd ../..
|
||||
done
|
||||
|
||||
- name: Scaffold app using published create-twenty-app
|
||||
run: |
|
||||
npm install -g create-twenty-app@$CI_VERSION --registry http://localhost:4873
|
||||
create-twenty-app --version
|
||||
mkdir -p /tmp/e2e-test-workspace
|
||||
cd /tmp/e2e-test-workspace
|
||||
create-twenty-app test-app --example hello-world --display-name "Test hello-world app" --description "E2E test hello-world app" --api-url http://localhost:3000
|
||||
|
||||
- name: Install scaffolded app dependencies
|
||||
run: |
|
||||
cd /tmp/e2e-test-workspace/test-app
|
||||
echo 'npmRegistryServer: "http://localhost:4873"' >> .yarnrc.yml
|
||||
echo 'unsafeHttpWhitelist: ["localhost"]' >> .yarnrc.yml
|
||||
YARN_ENABLE_IMMUTABLE_INSTALLS=false yarn install --no-immutable
|
||||
echo "--- Installing last SDK versions ---"
|
||||
YARN_ENABLE_IMMUTABLE_INSTALLS=false yarn add twenty-sdk twenty-client-sdk
|
||||
|
||||
- name: Verify installed app versions
|
||||
run: |
|
||||
cd /tmp/e2e-test-workspace/test-app
|
||||
echo "--- Checking package.json references correct SDK version ---"
|
||||
node -e "
|
||||
const pkg = require('./package.json');
|
||||
const sdkVersion = pkg.dependencies['twenty-sdk'];
|
||||
if (!sdkVersion.startsWith('0.0.0-ci.')) {
|
||||
console.error('Expected twenty-sdk version to start with 0.0.0-ci., got:', sdkVersion);
|
||||
process.exit(1);
|
||||
}
|
||||
console.log('SDK version in scaffolded app:', sdkVersion);
|
||||
"
|
||||
|
||||
- name: Verify SDK CLI is available
|
||||
run: |
|
||||
cd /tmp/e2e-test-workspace/test-app
|
||||
npx --no-install twenty --version
|
||||
|
||||
- name: Setup server environment
|
||||
run: npx nx reset:env:e2e-testing-server twenty-server
|
||||
|
||||
- name: Create databases
|
||||
run: |
|
||||
PGPASSWORD=postgres psql -h localhost -p 5432 -U postgres -d postgres -c 'CREATE DATABASE "default";'
|
||||
PGPASSWORD=postgres psql -h localhost -p 5432 -U postgres -d postgres -c 'CREATE DATABASE "test";'
|
||||
|
||||
- name: Setup database
|
||||
run: npx nx run twenty-server:database:reset
|
||||
|
||||
- name: Start server
|
||||
run: nohup npx nx start:ci twenty-server &
|
||||
|
||||
- name: Wait for server to be ready
|
||||
run: npx wait-on http://localhost:3000/healthz --timeout 120000 --interval 1000
|
||||
|
||||
- name: Authenticate with twenty-server
|
||||
run: |
|
||||
cd /tmp/e2e-test-workspace/test-app
|
||||
npx --no-install twenty remote add --api-key ${{ env.TWENTY_API_KEY }} --api-url ${{ env.TWENTY_API_URL }}
|
||||
|
||||
- name: Deploy scaffolded app
|
||||
run: |
|
||||
cd /tmp/e2e-test-workspace/test-app
|
||||
npx --no-install twenty deploy
|
||||
|
||||
- name: Install scaffolded app
|
||||
run: |
|
||||
cd /tmp/e2e-test-workspace/test-app
|
||||
npx --no-install twenty install
|
||||
|
||||
- name: Execute hello-world logic function
|
||||
run: |
|
||||
cd /tmp/e2e-test-workspace/test-app
|
||||
EXEC_OUTPUT=$(npx --no-install twenty exec --functionName hello-world-logic-function)
|
||||
echo "$EXEC_OUTPUT"
|
||||
echo "$EXEC_OUTPUT" | grep -q "Hello, World!"
|
||||
|
||||
- name: Execute create-hello-world-company logic function
|
||||
run: |
|
||||
cd /tmp/e2e-test-workspace/test-app
|
||||
EXEC_OUTPUT=$(npx --no-install twenty exec --functionName create-hello-world-company)
|
||||
echo "$EXEC_OUTPUT"
|
||||
echo "$EXEC_OUTPUT" | grep -q 'Created company.*Hello World.*with id'
|
||||
|
||||
- name: Run scaffolded app integration test
|
||||
run: |
|
||||
cd /tmp/e2e-test-workspace/test-app
|
||||
yarn test
|
||||
|
||||
ci-create-app-e2e-hello-world-status-check:
|
||||
if: always() && !cancelled()
|
||||
timeout-minutes: 5
|
||||
runs-on: ubuntu-latest
|
||||
needs: [changed-files-check, create-app-e2e-hello-world]
|
||||
steps:
|
||||
- name: Fail job if any needs failed
|
||||
if: contains(needs.*.result, 'failure')
|
||||
run: exit 1
|
||||
@@ -105,7 +105,7 @@ jobs:
|
||||
create-twenty-app --version
|
||||
mkdir -p /tmp/e2e-test-workspace
|
||||
cd /tmp/e2e-test-workspace
|
||||
create-twenty-app test-app --display-name "Test scaffolded app" --description "E2E test scaffolded app" --api-url http://localhost:3000
|
||||
create-twenty-app test-app --display-name "Test scaffolded app" --description "E2E test scaffolded app" --workspace-url http://localhost:3000
|
||||
|
||||
- name: Install scaffolded app dependencies
|
||||
run: |
|
||||
|
||||
@@ -1,199 +0,0 @@
|
||||
name: CI Postcard App E2E
|
||||
|
||||
on:
|
||||
# Temporarily disabled — will be re-enabled when example apps are published.
|
||||
# push:
|
||||
# branches:
|
||||
# - main
|
||||
#
|
||||
# pull_request:
|
||||
workflow_dispatch:
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
concurrency:
|
||||
group: ${{ github.workflow }}-${{ github.ref }}
|
||||
cancel-in-progress: ${{ github.ref != 'refs/heads/main' }}
|
||||
|
||||
jobs:
|
||||
changed-files-check:
|
||||
uses: ./.github/workflows/changed-files.yaml
|
||||
with:
|
||||
files: |
|
||||
packages/create-twenty-app/**
|
||||
packages/twenty-sdk/**
|
||||
packages/twenty-client-sdk/**
|
||||
packages/twenty-shared/**
|
||||
!packages/create-twenty-app/package.json
|
||||
!packages/twenty-sdk/package.json
|
||||
!packages/twenty-client-sdk/package.json
|
||||
!packages/twenty-shared/package.json
|
||||
create-app-e2e-postcard:
|
||||
needs: changed-files-check
|
||||
if: needs.changed-files-check.outputs.any_changed == 'true'
|
||||
timeout-minutes: 30
|
||||
runs-on: ubuntu-latest-4-cores
|
||||
services:
|
||||
postgres:
|
||||
image: postgres:18
|
||||
env:
|
||||
POSTGRES_USER: postgres
|
||||
POSTGRES_PASSWORD: postgres
|
||||
ports:
|
||||
- 5432:5432
|
||||
options: >-
|
||||
--health-cmd pg_isready
|
||||
--health-interval 10s
|
||||
--health-timeout 5s
|
||||
--health-retries 5
|
||||
redis:
|
||||
image: redis
|
||||
ports:
|
||||
- 6379:6379
|
||||
env:
|
||||
PUBLISHABLE_PACKAGES: twenty-client-sdk twenty-sdk create-twenty-app
|
||||
TWENTY_API_URL: http://localhost:3000
|
||||
TWENTY_API_KEY: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIyMDIwMjAyMC1lNmI1LTQ2ODAtOGEzMi1iODIwOTczNzE1NmIiLCJ1c2VySWQiOiIyMDIwMjAyMC1lNmI1LTQ2ODAtOGEzMi1iODIwOTczNzE1NmIiLCJ3b3Jrc3BhY2VJZCI6IjIwMjAyMDIwLTFjMjUtNGQwMi1iZjI1LTZhZWNjZjdlYTQxOSIsIndvcmtzcGFjZU1lbWJlcklkIjoiMjAyMDIwMjAtNDYzZi00MzViLTgyOGMtMTA3ZTAwN2EyNzExIiwidXNlcldvcmtzcGFjZUlkIjoiMjAyMDIwMjAtMWU3Yy00M2Q5LWE1ZGItNjg1YjUwNjlkODE2IiwidHlwZSI6IkFDQ0VTUyIsImF1dGhQcm92aWRlciI6InBhc3N3b3JkIiwiaWF0IjoxNzUxMjgxNzA0LCJleHAiOjIwNjY4NTc3MDR9.HMGqCsVlOAPVUBhKSGlD1X86VoHKt4LIUtET3CGIdik
|
||||
steps:
|
||||
- name: Fetch custom Github Actions and base branch history
|
||||
uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4.3.1
|
||||
with:
|
||||
fetch-depth: 10
|
||||
|
||||
- name: Install dependencies
|
||||
uses: ./.github/actions/yarn-install
|
||||
|
||||
- name: Set CI version and prepare packages for publish
|
||||
run: |
|
||||
CI_VERSION="0.0.0-ci.$(date +%s)"
|
||||
echo "CI_VERSION=$CI_VERSION" >> $GITHUB_ENV
|
||||
npx nx run-many -t set-local-version -p $PUBLISHABLE_PACKAGES --releaseVersion=$CI_VERSION
|
||||
|
||||
- name: Build packages
|
||||
run: |
|
||||
for pkg in $PUBLISHABLE_PACKAGES; do
|
||||
npx nx build $pkg
|
||||
done
|
||||
|
||||
- name: Install and start Verdaccio
|
||||
run: |
|
||||
npx verdaccio --config .github/verdaccio-config.yaml &
|
||||
|
||||
for i in $(seq 1 30); do
|
||||
if curl -s http://localhost:4873 > /dev/null 2>&1; then
|
||||
echo "Verdaccio is ready"
|
||||
break
|
||||
fi
|
||||
echo "Waiting for Verdaccio... ($i/30)"
|
||||
sleep 1
|
||||
done
|
||||
|
||||
- name: Publish packages to local registry
|
||||
run: |
|
||||
yarn config set npmRegistryServer http://localhost:4873
|
||||
yarn config set unsafeHttpWhitelist --json '["localhost"]'
|
||||
yarn config set npmAuthToken ci-auth-token
|
||||
|
||||
for pkg in $PUBLISHABLE_PACKAGES; do
|
||||
cd packages/$pkg
|
||||
yarn npm publish --tag ci
|
||||
cd ../..
|
||||
done
|
||||
|
||||
- name: Scaffold app using published create-twenty-app
|
||||
run: |
|
||||
npm install -g create-twenty-app@$CI_VERSION --registry http://localhost:4873
|
||||
create-twenty-app --version
|
||||
mkdir -p /tmp/e2e-test-workspace
|
||||
cd /tmp/e2e-test-workspace
|
||||
create-twenty-app test-app --example postcard --display-name "Test postcard app" --description "E2E test postcard app" --api-url http://localhost:3000
|
||||
|
||||
- name: Install scaffolded app dependencies
|
||||
run: |
|
||||
cd /tmp/e2e-test-workspace/test-app
|
||||
echo 'npmRegistryServer: "http://localhost:4873"' >> .yarnrc.yml
|
||||
echo 'unsafeHttpWhitelist: ["localhost"]' >> .yarnrc.yml
|
||||
YARN_ENABLE_IMMUTABLE_INSTALLS=false yarn install --no-immutable
|
||||
echo "--- Installing last SDK versions ---"
|
||||
YARN_ENABLE_IMMUTABLE_INSTALLS=false yarn add twenty-sdk twenty-client-sdk
|
||||
|
||||
- name: Verify installed app versions
|
||||
run: |
|
||||
cd /tmp/e2e-test-workspace/test-app
|
||||
echo "--- Checking package.json references correct SDK version ---"
|
||||
node -e "
|
||||
const pkg = require('./package.json');
|
||||
const sdkVersion = pkg.dependencies['twenty-sdk'];
|
||||
if (!sdkVersion.startsWith('0.0.0-ci.')) {
|
||||
console.error('Expected twenty-sdk version to start with 0.0.0-ci., got:', sdkVersion);
|
||||
process.exit(1);
|
||||
}
|
||||
console.log('SDK version in scaffolded app:', sdkVersion);
|
||||
"
|
||||
|
||||
- name: Verify SDK CLI is available
|
||||
run: |
|
||||
cd /tmp/e2e-test-workspace/test-app
|
||||
npx --no-install twenty --version
|
||||
|
||||
- name: Setup server environment
|
||||
run: npx nx reset:env:e2e-testing-server twenty-server
|
||||
|
||||
- name: Create databases
|
||||
run: |
|
||||
PGPASSWORD=postgres psql -h localhost -p 5432 -U postgres -d postgres -c 'CREATE DATABASE "default";'
|
||||
PGPASSWORD=postgres psql -h localhost -p 5432 -U postgres -d postgres -c 'CREATE DATABASE "test";'
|
||||
|
||||
- name: Setup database
|
||||
run: npx nx run twenty-server:database:reset
|
||||
|
||||
- name: Start server
|
||||
run: nohup npx nx start:ci twenty-server &
|
||||
|
||||
- name: Wait for server to be ready
|
||||
run: npx wait-on http://localhost:3000/healthz --timeout 120000 --interval 1000
|
||||
|
||||
- name: Authenticate with twenty-server
|
||||
run: |
|
||||
cd /tmp/e2e-test-workspace/test-app
|
||||
npx --no-install twenty remote add --api-key ${{ env.TWENTY_API_KEY }} --api-url ${{ env.TWENTY_API_URL }}
|
||||
|
||||
- name: Deploy scaffolded app
|
||||
run: |
|
||||
cd /tmp/e2e-test-workspace/test-app
|
||||
npx --no-install twenty deploy
|
||||
|
||||
- name: Install scaffolded app
|
||||
run: |
|
||||
cd /tmp/e2e-test-workspace/test-app
|
||||
npx --no-install twenty install
|
||||
|
||||
- name: Execute postcard logic function
|
||||
run: |
|
||||
cd /tmp/e2e-test-workspace/test-app
|
||||
EXEC_OUTPUT=$(npx --no-install twenty exec --functionName postcard-logic-function)
|
||||
echo "$EXEC_OUTPUT"
|
||||
echo "$EXEC_OUTPUT" | grep -q "Hello, World!"
|
||||
|
||||
- name: Execute create-postcard-company logic function
|
||||
run: |
|
||||
cd /tmp/e2e-test-workspace/test-app
|
||||
EXEC_OUTPUT=$(npx --no-install twenty exec --functionName create-postcard-company)
|
||||
echo "$EXEC_OUTPUT"
|
||||
echo "$EXEC_OUTPUT" | grep -q 'Created company.*Hello World.*with id'
|
||||
|
||||
- name: Run scaffolded app integration test
|
||||
run: |
|
||||
cd /tmp/e2e-test-workspace/test-app
|
||||
yarn test
|
||||
|
||||
ci-create-app-e2e-postcard-status-check:
|
||||
if: always() && !cancelled()
|
||||
timeout-minutes: 5
|
||||
runs-on: ubuntu-latest
|
||||
needs: [changed-files-check, create-app-e2e-postcard]
|
||||
steps:
|
||||
- name: Fail job if any needs failed
|
||||
if: contains(needs.*.result, 'failure')
|
||||
run: exit 1
|
||||
@@ -10,4 +10,9 @@ nodeLinker: node-modules
|
||||
|
||||
npmMinimalAgeGate: 3d
|
||||
|
||||
npmPreapprovedPackages:
|
||||
- twenty-sdk
|
||||
- twenty-client-sdk
|
||||
- create-twenty-app
|
||||
|
||||
yarnPath: .yarn/releases/yarn-4.13.0.cjs
|
||||
|
||||
@@ -32,21 +32,12 @@ The scaffolder will:
|
||||
|
||||
| Flag | Description |
|
||||
| ---------------------------------- | --------------------------------------------------------------------- |
|
||||
| `--example <name>` | Initialize from an example |
|
||||
| `--name <name>` | Set the app name |
|
||||
| `--display-name <displayName>` | Set the display name |
|
||||
| `--description <description>` | Set the description |
|
||||
| `--api-url <url>` | Twenty instance URL (default: `http://localhost:2020`) |
|
||||
| `--workspace-url <url>` | Twenty workspace URL (default: `http://localhost:2020`) |
|
||||
| `--authentication-method <method>` | `oauth` or `apiKey` (default: `apiKey` for local, `oauth` for remote) |
|
||||
|
||||
By default (no flags), a minimal app is generated with core files and an integration test. Use `--example` to start from a richer example:
|
||||
|
||||
```bash
|
||||
npx create-twenty-app@latest my-twenty-app --example hello-world
|
||||
```
|
||||
|
||||
Examples are sourced from [twentyhq/twenty/packages/twenty-apps/examples](https://github.com/twentyhq/twenty/tree/main/packages/twenty-apps/examples).
|
||||
|
||||
## Documentation
|
||||
|
||||
Full documentation is available at **[docs.twenty.com/developers/extend/apps](https://docs.twenty.com/developers/extend/apps/getting-started/quick-start)**:
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "create-twenty-app",
|
||||
"version": "2.5.0",
|
||||
"version": "2.5.1",
|
||||
"description": "Command-line interface to create Twenty application",
|
||||
"main": "dist/cli.cjs",
|
||||
"bin": "dist/cli.cjs",
|
||||
|
||||
@@ -15,14 +15,14 @@ const program = new Command(packageJson.name)
|
||||
'Output the current version of create-twenty-app.',
|
||||
)
|
||||
.argument('[directory]')
|
||||
.option('--example <name>', 'Initialize from an example')
|
||||
.option('-n, --name <name>', 'Application name')
|
||||
.option('-d, --display-name <displayName>', 'Application display name')
|
||||
.option('--description <description>', 'Application description')
|
||||
.option(
|
||||
'--api-url <apiUrl>',
|
||||
'Twenty instance URL (default: http://localhost:2020)',
|
||||
'--workspace-url <workspaceUrl>',
|
||||
'Twenty workspace URL (default: http://localhost:2020)',
|
||||
)
|
||||
.option('--api-url <apiUrl>', '[deprecated: use --workspace-url]')
|
||||
.option(
|
||||
'--authentication-method <method>',
|
||||
'Authentication method: oauth or apiKey (default: apiKey for local, oauth for remote)',
|
||||
@@ -32,10 +32,10 @@ const program = new Command(packageJson.name)
|
||||
async (
|
||||
directory?: string,
|
||||
options?: {
|
||||
example?: string;
|
||||
name?: string;
|
||||
displayName?: string;
|
||||
description?: string;
|
||||
workspaceUrl?: string;
|
||||
apiUrl?: string;
|
||||
authenticationMethod?: AuthenticationMethod;
|
||||
},
|
||||
@@ -66,13 +66,20 @@ const program = new Command(packageJson.name)
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
if (options?.apiUrl) {
|
||||
console.warn(
|
||||
chalk.yellow(
|
||||
'Warning: --api-url is deprecated. Use --workspace-url instead.',
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
await new CreateAppCommand().execute({
|
||||
directory,
|
||||
example: options?.example,
|
||||
name: options?.name,
|
||||
displayName: options?.displayName,
|
||||
description: options?.description,
|
||||
apiUrl: options?.apiUrl,
|
||||
workspaceUrl: options?.workspaceUrl ?? options?.apiUrl,
|
||||
authenticationMethod: options?.authenticationMethod,
|
||||
});
|
||||
},
|
||||
|
||||
@@ -0,0 +1,7 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 80 80" fill="none">
|
||||
<rect width="80" height="80" rx="16" fill="#141414"/>
|
||||
<rect x="20" y="20" width="16" height="16" rx="4" fill="#FAFAFA"/>
|
||||
<rect x="44" y="20" width="16" height="16" rx="4" fill="#FAFAFA" opacity="0.6"/>
|
||||
<rect x="20" y="44" width="16" height="16" rx="4" fill="#FAFAFA" opacity="0.6"/>
|
||||
<rect x="44" y="44" width="16" height="16" rx="4" fill="#FAFAFA" opacity="0.3"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 454 B |
+7
@@ -2,3 +2,10 @@ export const APP_DISPLAY_NAME = 'DISPLAY-NAME-TO-BE-GENERATED';
|
||||
export const APP_DESCRIPTION = 'DESCRIPTION-TO-BE-GENERATED';
|
||||
export const APPLICATION_UNIVERSAL_IDENTIFIER = 'UUID-TO-BE-GENERATED';
|
||||
export const DEFAULT_ROLE_UNIVERSAL_IDENTIFIER = 'UUID-TO-BE-GENERATED';
|
||||
export const MAIN_PAGE_FRONT_COMPONENT_UNIVERSAL_IDENTIFIER =
|
||||
'UUID-TO-BE-GENERATED';
|
||||
export const MAIN_PAGE_LAYOUT_UNIVERSAL_IDENTIFIER = 'UUID-TO-BE-GENERATED';
|
||||
export const MAIN_PAGE_LAYOUT_TAB_UNIVERSAL_IDENTIFIER = 'UUID-TO-BE-GENERATED';
|
||||
export const MAIN_PAGE_WIDGET_UNIVERSAL_IDENTIFIER = 'UUID-TO-BE-GENERATED';
|
||||
export const MAIN_PAGE_NAVIGATION_MENU_ITEM_UNIVERSAL_IDENTIFIER =
|
||||
'UUID-TO-BE-GENERATED';
|
||||
|
||||
@@ -0,0 +1,279 @@
|
||||
import { defineFrontComponent } from 'twenty-sdk/define';
|
||||
import { useState } from 'react';
|
||||
|
||||
import {
|
||||
APP_DISPLAY_NAME,
|
||||
MAIN_PAGE_FRONT_COMPONENT_UNIVERSAL_IDENTIFIER,
|
||||
} from 'src/constants/universal-identifiers';
|
||||
|
||||
const DOCS_BASE_URL = 'https://docs.twenty.com/developers/extend/apps';
|
||||
|
||||
const CATEGORIES = [
|
||||
{
|
||||
title: 'Data model',
|
||||
color: '#73D08D',
|
||||
items: [
|
||||
{ label: 'CUSTOM OBJECT', href: `${DOCS_BASE_URL}/data/objects` },
|
||||
{
|
||||
label: 'CUSTOM FIELDS',
|
||||
href: `${DOCS_BASE_URL}/data/extending-objects`,
|
||||
},
|
||||
],
|
||||
rotation: '2.4deg',
|
||||
},
|
||||
{
|
||||
title: 'Logic',
|
||||
color: '#F4D345',
|
||||
items: [
|
||||
{
|
||||
label: 'TOOLS',
|
||||
href: `${DOCS_BASE_URL}/logic/logic-functions`,
|
||||
},
|
||||
{
|
||||
label: 'SERVERLESS FUNCT.',
|
||||
href: `${DOCS_BASE_URL}/logic/logic-functions`,
|
||||
},
|
||||
{
|
||||
label: 'SKILLS',
|
||||
href: `${DOCS_BASE_URL}/logic/skills-and-agents`,
|
||||
},
|
||||
],
|
||||
rotation: '0deg',
|
||||
},
|
||||
{
|
||||
title: 'Layout',
|
||||
color: '#C4A2E0',
|
||||
items: [
|
||||
{ label: 'VIEWS', href: `${DOCS_BASE_URL}/layout/views` },
|
||||
{ label: 'WIDGETS', href: `${DOCS_BASE_URL}/layout/page-layouts` },
|
||||
{
|
||||
label: 'LAYOUT PAGES',
|
||||
href: `${DOCS_BASE_URL}/layout/page-layouts`,
|
||||
},
|
||||
{
|
||||
label: 'COMMANDS',
|
||||
href: `${DOCS_BASE_URL}/layout/command-menu-items`,
|
||||
},
|
||||
],
|
||||
rotation: '-2.8deg',
|
||||
},
|
||||
] as const;
|
||||
|
||||
const ItemIcon = ({ color }: { color: string }) => (
|
||||
<svg width="16" height="16" viewBox="0 0 16 16" fill="none">
|
||||
<rect x="2" y="2" width="5" height="5" rx="1" fill={color} />
|
||||
<rect x="9" y="2" width="5" height="5" rx="1" fill={color} opacity="0.6" />
|
||||
<rect x="2" y="9" width="5" height="5" rx="1" fill={color} opacity="0.6" />
|
||||
<rect x="9" y="9" width="5" height="5" rx="1" fill={color} opacity="0.3" />
|
||||
</svg>
|
||||
);
|
||||
|
||||
const ArrowUpRight = ({ color = '#999' }: { color?: string }) => (
|
||||
<svg width="14" height="14" viewBox="0 0 14 14" fill="none">
|
||||
<path
|
||||
d="M4.5 3.5H10.5V9.5M10.5 3.5L3.5 10.5"
|
||||
stroke={color}
|
||||
strokeWidth="1.2"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
/>
|
||||
</svg>
|
||||
);
|
||||
|
||||
const CategoryCard = ({
|
||||
title,
|
||||
color,
|
||||
items,
|
||||
rotation,
|
||||
}: {
|
||||
title: string;
|
||||
color: string;
|
||||
items: ReadonlyArray<{ label: string; href: string }>;
|
||||
rotation: string;
|
||||
}) => {
|
||||
const [hoveredItem, setHoveredItem] = useState<string | null>(null);
|
||||
|
||||
return (
|
||||
<div
|
||||
style={{
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
border: `1px solid ${color}80`,
|
||||
borderRadius: '12px',
|
||||
overflow: 'hidden',
|
||||
width: '240px',
|
||||
background: '#FFFFFF',
|
||||
transform: `rotate(${rotation})`,
|
||||
boxShadow: '0 2px 8px rgba(0, 0, 0, 0.04)',
|
||||
}}
|
||||
>
|
||||
<div
|
||||
style={{
|
||||
padding: '16px 20px',
|
||||
background: `${color}22`,
|
||||
}}
|
||||
>
|
||||
<span
|
||||
style={{
|
||||
fontSize: '16px',
|
||||
fontWeight: 600,
|
||||
color: color,
|
||||
}}
|
||||
>
|
||||
{title}
|
||||
</span>
|
||||
</div>
|
||||
<div
|
||||
style={{
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
padding: '8px',
|
||||
gap: '4px',
|
||||
}}
|
||||
>
|
||||
{items.map((item) => {
|
||||
const isHovered = hoveredItem === item.label;
|
||||
|
||||
return (
|
||||
<a
|
||||
key={item.label}
|
||||
href={item.href}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
onMouseEnter={() => setHoveredItem(item.label)}
|
||||
onMouseLeave={() => setHoveredItem(null)}
|
||||
style={{
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
gap: '10px',
|
||||
textDecoration: 'none',
|
||||
cursor: 'pointer',
|
||||
padding: '10px 12px',
|
||||
borderRadius: '8px',
|
||||
background: isHovered ? '#0000000A' : 'transparent',
|
||||
transition: 'background 0.15s',
|
||||
}}
|
||||
>
|
||||
<ItemIcon color={color} />
|
||||
<span
|
||||
style={{
|
||||
fontSize: '13px',
|
||||
fontWeight: 300,
|
||||
color: '#333',
|
||||
letterSpacing: '0.5px',
|
||||
flex: 1,
|
||||
}}
|
||||
>
|
||||
{item.label}
|
||||
</span>
|
||||
{isHovered && <ArrowUpRight />}
|
||||
</a>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const MainPage = () => {
|
||||
return (
|
||||
<div
|
||||
style={{
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
height: '100%',
|
||||
fontFamily:
|
||||
'Inter, -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif',
|
||||
gap: '8px',
|
||||
padding: '40px',
|
||||
}}
|
||||
>
|
||||
{/*<Avatar
|
||||
avatarUrl={getPublicAssetUrl('logo.svg')}
|
||||
placeholder={APP_DISPLAY_NAME}
|
||||
placeholderColorSeed={APP_DISPLAY_NAME}
|
||||
type="squared"
|
||||
size="xl"
|
||||
/>*/}
|
||||
<span
|
||||
style={{
|
||||
fontSize: '24px',
|
||||
fontWeight: 600,
|
||||
color: '#333',
|
||||
marginTop: '8px',
|
||||
}}
|
||||
>
|
||||
{APP_DISPLAY_NAME}
|
||||
</span>
|
||||
<span
|
||||
style={{
|
||||
fontSize: '13px',
|
||||
color: '#888',
|
||||
textAlign: 'center',
|
||||
lineHeight: '1.5',
|
||||
}}
|
||||
>
|
||||
Was installed successfully.
|
||||
<br />
|
||||
You can now add content to your app.
|
||||
</span>
|
||||
<a
|
||||
href="/settings/applications"
|
||||
style={{
|
||||
display: 'inline-flex',
|
||||
alignItems: 'center',
|
||||
gap: '6px',
|
||||
marginTop: '16px',
|
||||
fontSize: '13px',
|
||||
color: '#333',
|
||||
textDecoration: 'none',
|
||||
padding: '8px 16px',
|
||||
borderRadius: '8px',
|
||||
border: '1px solid #e0e0e0',
|
||||
background: '#fafafa',
|
||||
transition: 'background 0.15s, border-color 0.15s',
|
||||
}}
|
||||
onMouseEnter={(e) => {
|
||||
e.currentTarget.style.background = '#f0f0f0';
|
||||
e.currentTarget.style.borderColor = '#ccc';
|
||||
}}
|
||||
onMouseLeave={(e) => {
|
||||
e.currentTarget.style.background = '#fafafa';
|
||||
e.currentTarget.style.borderColor = '#e0e0e0';
|
||||
}}
|
||||
>
|
||||
Open app settings
|
||||
<ArrowUpRight color="#333" />
|
||||
</a>
|
||||
<div
|
||||
style={{
|
||||
display: 'flex',
|
||||
gap: '16px',
|
||||
marginTop: '32px',
|
||||
flexWrap: 'wrap',
|
||||
justifyContent: 'center',
|
||||
alignItems: 'flex-start',
|
||||
}}
|
||||
>
|
||||
{CATEGORIES.map((category) => (
|
||||
<CategoryCard
|
||||
key={category.title}
|
||||
title={category.title}
|
||||
color={category.color}
|
||||
items={category.items}
|
||||
rotation={category.rotation}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default defineFrontComponent({
|
||||
universalIdentifier: MAIN_PAGE_FRONT_COMPONENT_UNIVERSAL_IDENTIFIER,
|
||||
name: APP_DISPLAY_NAME,
|
||||
description: `${APP_DISPLAY_NAME} front component displaying the app logo and name`,
|
||||
component: MainPage,
|
||||
});
|
||||
+19
@@ -0,0 +1,19 @@
|
||||
import {
|
||||
defineNavigationMenuItem,
|
||||
NavigationMenuItemType,
|
||||
} from 'twenty-sdk/define';
|
||||
|
||||
import {
|
||||
APP_DISPLAY_NAME,
|
||||
MAIN_PAGE_LAYOUT_UNIVERSAL_IDENTIFIER,
|
||||
MAIN_PAGE_NAVIGATION_MENU_ITEM_UNIVERSAL_IDENTIFIER,
|
||||
} from 'src/constants/universal-identifiers';
|
||||
|
||||
export default defineNavigationMenuItem({
|
||||
universalIdentifier: MAIN_PAGE_NAVIGATION_MENU_ITEM_UNIVERSAL_IDENTIFIER,
|
||||
name: APP_DISPLAY_NAME,
|
||||
icon: 'IconFile',
|
||||
position: -1,
|
||||
type: NavigationMenuItemType.PAGE_LAYOUT,
|
||||
pageLayoutUniversalIdentifier: MAIN_PAGE_LAYOUT_UNIVERSAL_IDENTIFIER,
|
||||
});
|
||||
+37
@@ -0,0 +1,37 @@
|
||||
import { definePageLayout, PageLayoutTabLayoutMode } from 'twenty-sdk/define';
|
||||
|
||||
import {
|
||||
APP_DISPLAY_NAME,
|
||||
MAIN_PAGE_FRONT_COMPONENT_UNIVERSAL_IDENTIFIER,
|
||||
MAIN_PAGE_LAYOUT_TAB_UNIVERSAL_IDENTIFIER,
|
||||
MAIN_PAGE_LAYOUT_UNIVERSAL_IDENTIFIER,
|
||||
MAIN_PAGE_WIDGET_UNIVERSAL_IDENTIFIER,
|
||||
} from 'src/constants/universal-identifiers';
|
||||
|
||||
export default definePageLayout({
|
||||
universalIdentifier: MAIN_PAGE_LAYOUT_UNIVERSAL_IDENTIFIER,
|
||||
name: APP_DISPLAY_NAME,
|
||||
type: 'STANDALONE_PAGE',
|
||||
tabs: [
|
||||
{
|
||||
universalIdentifier: MAIN_PAGE_LAYOUT_TAB_UNIVERSAL_IDENTIFIER,
|
||||
title: 'Overview',
|
||||
position: 0,
|
||||
icon: 'IconApps',
|
||||
layoutMode: PageLayoutTabLayoutMode.CANVAS,
|
||||
widgets: [
|
||||
{
|
||||
universalIdentifier: MAIN_PAGE_WIDGET_UNIVERSAL_IDENTIFIER,
|
||||
title: ' ',
|
||||
type: 'FRONT_COMPONENT',
|
||||
gridPosition: { row: 0, column: 0, rowSpan: 12, columnSpan: 12 },
|
||||
configuration: {
|
||||
configurationType: 'FRONT_COMPONENT',
|
||||
frontComponentUniversalIdentifier:
|
||||
MAIN_PAGE_FRONT_COMPONENT_UNIVERSAL_IDENTIFIER,
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
});
|
||||
@@ -1,5 +1,4 @@
|
||||
import { copyBaseApplicationProject } from '@/utils/app-template';
|
||||
import { downloadExample } from '@/utils/download-example';
|
||||
import { convertToLabel } from '@/utils/convert-to-label';
|
||||
import { install } from '@/utils/install';
|
||||
import { tryGitInit } from '@/utils/try-git-init';
|
||||
@@ -31,11 +30,10 @@ export type AuthenticationMethod = 'oauth' | 'apiKey';
|
||||
|
||||
type CreateAppOptions = {
|
||||
directory?: string;
|
||||
example?: string;
|
||||
name?: string;
|
||||
displayName?: string;
|
||||
description?: string;
|
||||
apiUrl?: string;
|
||||
workspaceUrl?: string;
|
||||
authenticationMethod?: AuthenticationMethod;
|
||||
};
|
||||
|
||||
@@ -47,9 +45,9 @@ export class CreateAppCommand {
|
||||
const { appName, appDisplayName, appDirectory, appDescription } =
|
||||
this.getAppInfos(options);
|
||||
|
||||
const apiUrl = options.apiUrl ?? DEV_API_URL;
|
||||
const workspaceUrl = options.workspaceUrl ?? DEV_API_URL;
|
||||
|
||||
const skipLocalInstance = apiUrl !== DEV_API_URL;
|
||||
const skipLocalInstance = workspaceUrl !== DEV_API_URL;
|
||||
|
||||
if (!skipLocalInstance && !isDockerInstalled()) {
|
||||
console.log(chalk.yellow('\n' + getDockerInstallInstructions() + '\n'));
|
||||
@@ -92,30 +90,13 @@ export class CreateAppCommand {
|
||||
|
||||
this.logNextStep('Scaffolding project files');
|
||||
|
||||
if (options.example) {
|
||||
const exampleSucceeded = await this.tryDownloadExample(
|
||||
options.example,
|
||||
appDirectory,
|
||||
);
|
||||
|
||||
if (!exampleSucceeded) {
|
||||
await copyBaseApplicationProject({
|
||||
appName,
|
||||
appDisplayName,
|
||||
appDescription,
|
||||
appDirectory,
|
||||
onProgress: (message) => this.logDetail(message),
|
||||
});
|
||||
}
|
||||
} else {
|
||||
await copyBaseApplicationProject({
|
||||
appName,
|
||||
appDisplayName,
|
||||
appDescription,
|
||||
appDirectory,
|
||||
onProgress: (message) => this.logDetail(message),
|
||||
});
|
||||
}
|
||||
await copyBaseApplicationProject({
|
||||
appName,
|
||||
appDisplayName,
|
||||
appDescription,
|
||||
appDirectory,
|
||||
onProgress: (message) => this.logDetail(message),
|
||||
});
|
||||
|
||||
this.logNextStep('Installing dependencies');
|
||||
|
||||
@@ -137,7 +118,7 @@ export class CreateAppCommand {
|
||||
console.log('');
|
||||
|
||||
let authSucceeded = false;
|
||||
let resolvedApiUrl = apiUrl;
|
||||
let resolvedWorkspaceUrl = workspaceUrl;
|
||||
let serverReady = skipLocalInstance;
|
||||
|
||||
if (!skipLocalInstance) {
|
||||
@@ -145,20 +126,49 @@ export class CreateAppCommand {
|
||||
const serverResult = await this.ensureDockerServer(dockerPullPromise);
|
||||
|
||||
if (isDefined(serverResult.url)) {
|
||||
resolvedApiUrl = serverResult.url;
|
||||
resolvedWorkspaceUrl = serverResult.url;
|
||||
serverReady = true;
|
||||
}
|
||||
}
|
||||
|
||||
if (serverReady && authenticationMethod === 'oauth') {
|
||||
this.logNextStep('Authenticating via OAuth');
|
||||
authSucceeded = await this.authenticateWithOAuth(resolvedApiUrl);
|
||||
} else if (serverReady && authenticationMethod === 'apiKey') {
|
||||
this.logNextStep('Authenticating via API key');
|
||||
authSucceeded = await this.authenticateWithDevKey(resolvedApiUrl);
|
||||
if (serverReady) {
|
||||
this.logNextStep('Authenticating');
|
||||
|
||||
authSucceeded = await this.tryExistingAuth(resolvedWorkspaceUrl);
|
||||
|
||||
if (authSucceeded) {
|
||||
this.logDetail('Reusing existing credentials');
|
||||
} else if (authenticationMethod === 'oauth') {
|
||||
this.logDetail('Starting OAuth flow');
|
||||
authSucceeded =
|
||||
await this.authenticateWithOAuth(resolvedWorkspaceUrl);
|
||||
} else {
|
||||
this.logDetail('Using development API key');
|
||||
authSucceeded =
|
||||
await this.authenticateWithDevKey(resolvedWorkspaceUrl);
|
||||
}
|
||||
}
|
||||
|
||||
this.logSuccess(appDirectory, resolvedApiUrl, authSucceeded);
|
||||
this.logNextStep('Installing application');
|
||||
|
||||
let syncSucceeded = false;
|
||||
|
||||
if (serverReady && authSucceeded) {
|
||||
syncSucceeded = await this.syncApplication(appDirectory);
|
||||
|
||||
if (!syncSucceeded) {
|
||||
this.logDetail('Sync failed. Run `yarn twenty dev --once` manually.');
|
||||
return;
|
||||
}
|
||||
} else {
|
||||
this.logDetail('Skipped (server or authentication not available)');
|
||||
}
|
||||
|
||||
if (syncSucceeded) {
|
||||
await this.openMainPage(appDirectory, resolvedWorkspaceUrl);
|
||||
}
|
||||
|
||||
this.logSuccess(appDirectory, resolvedWorkspaceUrl, authSucceeded);
|
||||
} catch (error) {
|
||||
console.error(
|
||||
chalk.red('\nCreate application failed:'),
|
||||
@@ -180,6 +190,7 @@ export class CreateAppCommand {
|
||||
}
|
||||
|
||||
steps += 1; // authenticate (oauth or apiKey)
|
||||
steps += 1; // sync application
|
||||
|
||||
return steps;
|
||||
}
|
||||
@@ -193,7 +204,6 @@ export class CreateAppCommand {
|
||||
const appName = (
|
||||
options.name ??
|
||||
options.directory ??
|
||||
options.example ??
|
||||
'my-twenty-app'
|
||||
).trim();
|
||||
|
||||
@@ -222,28 +232,6 @@ export class CreateAppCommand {
|
||||
}
|
||||
}
|
||||
|
||||
private async tryDownloadExample(
|
||||
example: string,
|
||||
appDirectory: string,
|
||||
): Promise<boolean> {
|
||||
try {
|
||||
await downloadExample(example, appDirectory);
|
||||
|
||||
return true;
|
||||
} catch (error) {
|
||||
console.log(
|
||||
chalk.yellow(
|
||||
`\n${error instanceof Error ? error.message : 'Failed to download example.'}`,
|
||||
),
|
||||
);
|
||||
this.logDetail('Falling back to default template...');
|
||||
|
||||
await fs.emptyDir(appDirectory);
|
||||
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
private logPlan({
|
||||
appName,
|
||||
appDisplayName,
|
||||
@@ -325,11 +313,253 @@ export class CreateAppCommand {
|
||||
return {};
|
||||
}
|
||||
|
||||
private async authenticateWithDevKey(apiUrl: string): Promise<boolean> {
|
||||
private async openMainPage(
|
||||
appDirectory: string,
|
||||
workspaceUrl: string,
|
||||
): Promise<void> {
|
||||
try {
|
||||
const configService = new ConfigService();
|
||||
const config = await configService.getConfig();
|
||||
const token = config.twentyCLIAccessToken ?? config.apiKey;
|
||||
|
||||
if (!token) {
|
||||
return;
|
||||
}
|
||||
|
||||
const [universalIdentifier, frontUrl] = await Promise.all([
|
||||
this.readMainPageLayoutUniversalIdentifier(appDirectory),
|
||||
this.resolveWorkspaceFrontUrl(workspaceUrl, token),
|
||||
]);
|
||||
|
||||
if (!universalIdentifier || !frontUrl) {
|
||||
return;
|
||||
}
|
||||
|
||||
const pageLayoutId = await this.resolvePageLayoutId(
|
||||
workspaceUrl,
|
||||
universalIdentifier,
|
||||
token,
|
||||
);
|
||||
|
||||
if (!pageLayoutId) {
|
||||
return;
|
||||
}
|
||||
|
||||
const url = `${frontUrl}/page/${pageLayoutId}`;
|
||||
|
||||
this.logDetail(`Opening app welcome page: ${url}`);
|
||||
this.openInBrowser(url);
|
||||
} catch {
|
||||
// Best-effort — don't fail the scaffold if browser open fails
|
||||
}
|
||||
}
|
||||
|
||||
private async resolveWorkspaceFrontUrl(
|
||||
workspaceUrl: string,
|
||||
token: string,
|
||||
): Promise<string | null> {
|
||||
const query = `{ currentWorkspace { workspaceUrls { subdomainUrl customUrl } } }`;
|
||||
|
||||
const response = await fetch(`${workspaceUrl}/metadata`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
Authorization: `Bearer ${token}`,
|
||||
},
|
||||
body: JSON.stringify({ query }),
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const body = (await response.json()) as {
|
||||
data?: {
|
||||
currentWorkspace?: {
|
||||
workspaceUrls?: { subdomainUrl?: string; customUrl?: string };
|
||||
};
|
||||
};
|
||||
};
|
||||
|
||||
const urls = body.data?.currentWorkspace?.workspaceUrls;
|
||||
|
||||
if (!urls) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const frontUrl = urls.customUrl ?? urls.subdomainUrl;
|
||||
|
||||
return frontUrl?.replace(/\/+$/, '') ?? null;
|
||||
}
|
||||
|
||||
private async readMainPageLayoutUniversalIdentifier(
|
||||
appDirectory: string,
|
||||
): Promise<string | null> {
|
||||
const filePath = path.join(
|
||||
appDirectory,
|
||||
'src',
|
||||
'constants',
|
||||
'universal-identifiers.ts',
|
||||
);
|
||||
const content = await fs.readFile(filePath, 'utf-8');
|
||||
const match = content.match(
|
||||
/MAIN_PAGE_LAYOUT_UNIVERSAL_IDENTIFIER\s*=\s*'([^']+)'/,
|
||||
);
|
||||
|
||||
return match?.[1] ?? null;
|
||||
}
|
||||
|
||||
private async resolvePageLayoutId(
|
||||
workspaceUrl: string,
|
||||
universalIdentifier: string,
|
||||
token: string,
|
||||
): Promise<string | null> {
|
||||
const query = `{ getPageLayouts { id universalIdentifier } }`;
|
||||
|
||||
const response = await fetch(`${workspaceUrl}/metadata`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
Authorization: `Bearer ${token}`,
|
||||
},
|
||||
body: JSON.stringify({ query }),
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const body = (await response.json()) as {
|
||||
data?: {
|
||||
getPageLayouts?: { id: string; universalIdentifier: string }[];
|
||||
};
|
||||
};
|
||||
|
||||
const matching = body.data?.getPageLayouts?.find(
|
||||
(layout) => layout.universalIdentifier === universalIdentifier,
|
||||
);
|
||||
|
||||
return matching?.id ?? null;
|
||||
}
|
||||
|
||||
private sanitizeBrowserUrl(url: string): string | null {
|
||||
if (/[^\u0020-\u007E]/.test(url)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
try {
|
||||
const parsed = new URL(url);
|
||||
|
||||
if (parsed.protocol !== 'http:' && parsed.protocol !== 'https:') {
|
||||
return null;
|
||||
}
|
||||
|
||||
return parsed.toString();
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
private openInBrowser(url: string): void {
|
||||
const safeUrl = this.sanitizeBrowserUrl(url);
|
||||
|
||||
if (!safeUrl) {
|
||||
return;
|
||||
}
|
||||
|
||||
const isWindows = process.platform === 'win32';
|
||||
const command = isWindows
|
||||
? 'rundll32'
|
||||
: process.platform === 'darwin'
|
||||
? 'open'
|
||||
: 'xdg-open';
|
||||
const args = isWindows
|
||||
? ['url.dll,FileProtocolHandler', safeUrl]
|
||||
: [safeUrl];
|
||||
|
||||
const child = spawn(command, args, {
|
||||
stdio: 'ignore',
|
||||
detached: !isWindows,
|
||||
});
|
||||
child.on('error', () => undefined);
|
||||
if (!isWindows) {
|
||||
child.unref();
|
||||
}
|
||||
}
|
||||
|
||||
private async syncApplication(appDirectory: string): Promise<boolean> {
|
||||
this.logDetail('Running `yarn twenty dev --once`...');
|
||||
return new Promise((resolve) => {
|
||||
const child = spawn('yarn', ['twenty', 'dev', '--once'], {
|
||||
cwd: appDirectory,
|
||||
stdio: ['inherit', 'pipe', 'pipe'],
|
||||
});
|
||||
|
||||
child.stdout?.resume();
|
||||
child.stderr?.resume();
|
||||
|
||||
child.on('close', (code) => resolve(code === 0));
|
||||
child.on('error', () => resolve(false));
|
||||
});
|
||||
}
|
||||
|
||||
private async tryExistingAuth(workspaceUrl: string): Promise<boolean> {
|
||||
try {
|
||||
const configService = new ConfigService();
|
||||
const remoteNames = await configService.getRemotes();
|
||||
|
||||
for (const remoteName of remoteNames) {
|
||||
const remoteConfig = await configService.getConfigForRemote(remoteName);
|
||||
|
||||
if (remoteConfig.apiUrl !== workspaceUrl) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const token = remoteConfig.twentyCLIAccessToken ?? remoteConfig.apiKey;
|
||||
|
||||
if (!token) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const response = await fetch(`${workspaceUrl}/metadata`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
Authorization: `Bearer ${token}`,
|
||||
},
|
||||
body: JSON.stringify({
|
||||
query: '{ currentWorkspace { id } }',
|
||||
}),
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const body = (await response.json()) as {
|
||||
data?: { currentWorkspace?: { id: string } };
|
||||
errors?: unknown[];
|
||||
};
|
||||
|
||||
if (isDefined(body.data?.currentWorkspace) && !body.errors) {
|
||||
ConfigService.setActiveRemote(remoteName);
|
||||
await configService.setDefaultRemote(remoteName);
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
private async authenticateWithDevKey(workspaceUrl: string): Promise<boolean> {
|
||||
try {
|
||||
const result = await authLogin({
|
||||
apiKey: DEV_API_KEY,
|
||||
apiUrl,
|
||||
apiUrl: workspaceUrl,
|
||||
remote: 'local',
|
||||
});
|
||||
|
||||
@@ -368,21 +598,21 @@ export class CreateAppCommand {
|
||||
}
|
||||
}
|
||||
|
||||
private async authenticateWithOAuth(apiUrl: string): Promise<boolean> {
|
||||
private async authenticateWithOAuth(workspaceUrl: string): Promise<boolean> {
|
||||
try {
|
||||
const remoteName = this.deriveRemoteName(apiUrl);
|
||||
const remoteName = this.deriveRemoteName(workspaceUrl);
|
||||
|
||||
ConfigService.setActiveRemote(remoteName);
|
||||
|
||||
this.logDetail('Opening browser for OAuth...');
|
||||
|
||||
const result = await authLoginOAuth({ apiUrl });
|
||||
const result = await authLoginOAuth({ apiUrl: workspaceUrl });
|
||||
|
||||
if (result.success) {
|
||||
const configService = new ConfigService();
|
||||
|
||||
await configService.setDefaultRemote(remoteName);
|
||||
this.logDetail(`Authenticated via OAuth to ${apiUrl}`);
|
||||
this.logDetail(`Authenticated via OAuth to ${workspaceUrl}`);
|
||||
|
||||
return true;
|
||||
}
|
||||
@@ -390,7 +620,7 @@ export class CreateAppCommand {
|
||||
console.log(
|
||||
chalk.yellow(
|
||||
` OAuth failed: ${result.error.message}\n` +
|
||||
` Run \`yarn twenty remote add --api-url ${apiUrl}\` manually.`,
|
||||
` Run \`yarn twenty remote add --api-url ${workspaceUrl}\` manually.`,
|
||||
),
|
||||
);
|
||||
|
||||
@@ -398,7 +628,7 @@ export class CreateAppCommand {
|
||||
} catch {
|
||||
console.log(
|
||||
chalk.yellow(
|
||||
` Authentication failed. Run \`yarn twenty remote add --api-url ${apiUrl}\` manually.`,
|
||||
` Authentication failed. Run \`yarn twenty remote add --api-url ${workspaceUrl}\` manually.`,
|
||||
),
|
||||
);
|
||||
|
||||
@@ -408,7 +638,7 @@ export class CreateAppCommand {
|
||||
|
||||
private logSuccess(
|
||||
appDirectory: string,
|
||||
apiUrl: string,
|
||||
workspaceUrl: string,
|
||||
authSucceeded: boolean,
|
||||
): void {
|
||||
const dirName = basename(appDirectory);
|
||||
@@ -438,7 +668,7 @@ export class CreateAppCommand {
|
||||
stepNumber++;
|
||||
|
||||
console.log(chalk.white(` ${stepNumber}. Open your twenty instance`));
|
||||
console.log(chalk.cyan(` ${apiUrl}\n`));
|
||||
console.log(chalk.cyan(` ${workspaceUrl}\n`));
|
||||
|
||||
console.log(
|
||||
chalk.gray(
|
||||
|
||||
@@ -28,6 +28,6 @@ export const getDockerInstallInstructions = (): string => {
|
||||
' Then run this command again.',
|
||||
'',
|
||||
' Alternatively, connect to an existing Twenty instance:',
|
||||
' npx create-twenty-app@latest my-twenty-app --api-url <your-instance-url>',
|
||||
' npx create-twenty-app@latest my-twenty-app --workspace-url <your-instance-url>',
|
||||
].join('\n');
|
||||
};
|
||||
|
||||
@@ -1,179 +0,0 @@
|
||||
import { execSync } from 'node:child_process';
|
||||
import * as fs from 'fs-extra';
|
||||
import { join } from 'path';
|
||||
import { tmpdir } from 'node:os';
|
||||
import chalk from 'chalk';
|
||||
|
||||
const TWENTY_REPO_OWNER = 'twentyhq';
|
||||
const TWENTY_REPO_NAME = 'twenty';
|
||||
const TWENTY_FALLBACK_REF = 'main';
|
||||
const TWENTY_EXAMPLES_PATH = 'packages/twenty-apps/examples';
|
||||
const TWENTY_EXAMPLES_URL = `https://github.com/${TWENTY_REPO_OWNER}/${TWENTY_REPO_NAME}/tree/${TWENTY_FALLBACK_REF}/${TWENTY_EXAMPLES_PATH}`;
|
||||
|
||||
// Fetches the latest release tag from the repo, or falls back to main
|
||||
const resolveRef = async (): Promise<string> => {
|
||||
const response = await fetch(
|
||||
`https://api.github.com/repos/${TWENTY_REPO_OWNER}/${TWENTY_REPO_NAME}/releases/latest`,
|
||||
{ headers: { Accept: 'application/vnd.github.v3+json' } },
|
||||
);
|
||||
|
||||
if (response.ok) {
|
||||
const release = (await response.json()) as { tag_name: string };
|
||||
|
||||
return release.tag_name;
|
||||
}
|
||||
|
||||
return TWENTY_FALLBACK_REF;
|
||||
};
|
||||
|
||||
// Uses the GitHub Contents API to list directories — fast and doesn't download the repo
|
||||
const fetchGitHubDirectoryContents = async (
|
||||
path: string,
|
||||
ref: string,
|
||||
): Promise<{ name: string; type: string }[] | null> => {
|
||||
const apiUrl = `https://api.github.com/repos/${TWENTY_REPO_OWNER}/${TWENTY_REPO_NAME}/contents/${path}?ref=${ref}`;
|
||||
|
||||
const response = await fetch(apiUrl, {
|
||||
headers: { Accept: 'application/vnd.github.v3+json' },
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const data = await response.json();
|
||||
|
||||
if (!Array.isArray(data)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return data as { name: string; type: string }[];
|
||||
};
|
||||
|
||||
const listAvailableExamples = async (ref: string): Promise<string[]> => {
|
||||
const contents = await fetchGitHubDirectoryContents(
|
||||
TWENTY_EXAMPLES_PATH,
|
||||
ref,
|
||||
);
|
||||
|
||||
if (!contents) {
|
||||
return [];
|
||||
}
|
||||
|
||||
return contents
|
||||
.filter((entry) => entry.type === 'dir')
|
||||
.map((entry) => entry.name);
|
||||
};
|
||||
|
||||
const validateExampleExists = async (
|
||||
exampleName: string,
|
||||
ref: string,
|
||||
): Promise<void> => {
|
||||
const examplePath = `${TWENTY_EXAMPLES_PATH}/${exampleName}`;
|
||||
|
||||
const contents = await fetchGitHubDirectoryContents(examplePath, ref);
|
||||
|
||||
if (contents !== null) {
|
||||
return;
|
||||
}
|
||||
|
||||
const availableExamples = await listAvailableExamples(ref);
|
||||
|
||||
throw new Error(
|
||||
`Example "${exampleName}" not found.\n\n` +
|
||||
(availableExamples.length > 0
|
||||
? `Available examples:\n${availableExamples.map((name) => ` - ${name}`).join('\n')}\n\n`
|
||||
: '') +
|
||||
`Browse all examples: ${TWENTY_EXAMPLES_URL}`,
|
||||
);
|
||||
};
|
||||
|
||||
export const downloadExample = async (
|
||||
exampleName: string,
|
||||
targetDirectory: string,
|
||||
): Promise<void> => {
|
||||
if (
|
||||
exampleName.includes('/') ||
|
||||
exampleName.includes('\\') ||
|
||||
exampleName.includes('..')
|
||||
) {
|
||||
throw new Error(
|
||||
`Invalid example name: "${exampleName}". Example names must be simple directory names (e.g., "hello-world").`,
|
||||
);
|
||||
}
|
||||
|
||||
const ref = await resolveRef();
|
||||
const examplePath = `${TWENTY_EXAMPLES_PATH}/${exampleName}`;
|
||||
|
||||
console.log(chalk.gray(`Resolving examples from ref '${ref}'...`));
|
||||
|
||||
await validateExampleExists(exampleName, ref);
|
||||
|
||||
console.log(chalk.gray(`Example '${examplePath}' validated successfully.`));
|
||||
|
||||
const tarballUrl = `https://codeload.github.com/${TWENTY_REPO_OWNER}/${TWENTY_REPO_NAME}/tar.gz/${ref}`;
|
||||
|
||||
const tempDir = join(
|
||||
tmpdir(),
|
||||
`create-twenty-app-${Date.now()}-${Math.random().toString(36).slice(2)}`,
|
||||
);
|
||||
|
||||
try {
|
||||
await fs.ensureDir(tempDir);
|
||||
|
||||
console.log(chalk.gray(`Downloading tarball from ${tarballUrl}...`));
|
||||
|
||||
const response = await fetch(tarballUrl);
|
||||
|
||||
if (!response.ok) {
|
||||
if (response.status === 404) {
|
||||
throw new Error(
|
||||
`Could not find repository: ${TWENTY_REPO_OWNER}/${TWENTY_REPO_NAME} (ref: ${ref})`,
|
||||
);
|
||||
}
|
||||
throw new Error(
|
||||
`Failed to download from GitHub: ${response.status} ${response.statusText}`,
|
||||
);
|
||||
}
|
||||
|
||||
console.log(chalk.gray('Tarball downloaded. Writing to disk...'));
|
||||
|
||||
const tarballPath = join(tempDir, 'archive.tar.gz');
|
||||
|
||||
const buffer = Buffer.from(await response.arrayBuffer());
|
||||
|
||||
await fs.writeFile(tarballPath, buffer);
|
||||
|
||||
console.log(
|
||||
chalk.gray(
|
||||
`Tarball saved (${(buffer.length / 1024 / 1024).toFixed(1)} MB). Extracting...`,
|
||||
),
|
||||
);
|
||||
|
||||
execSync(`tar xzf "${tarballPath}" -C "${tempDir}"`, {
|
||||
stdio: 'pipe',
|
||||
});
|
||||
|
||||
// GitHub tarballs extract to a directory named {repo}-{ref}/
|
||||
const extractedEntries = await fs.readdir(tempDir);
|
||||
const extractedDir = extractedEntries.find(
|
||||
(entry) => entry !== 'archive.tar.gz',
|
||||
);
|
||||
|
||||
if (!extractedDir) {
|
||||
throw new Error('Failed to extract archive: no directory found');
|
||||
}
|
||||
|
||||
const sourcePath = join(tempDir, extractedDir, examplePath);
|
||||
|
||||
if (!(await fs.pathExists(sourcePath))) {
|
||||
throw new Error(
|
||||
`Example directory not found in archive: "${examplePath}"`,
|
||||
);
|
||||
}
|
||||
|
||||
await fs.copy(sourcePath, targetDirectory);
|
||||
} finally {
|
||||
await fs.remove(tempDir);
|
||||
}
|
||||
};
|
||||
@@ -28,12 +28,6 @@ yarn install
|
||||
yarn twenty dev
|
||||
```
|
||||
|
||||
Or use it as a template via create-twenty-app:
|
||||
|
||||
```bash
|
||||
npx create-twenty-app@latest my-app --example postcard
|
||||
```
|
||||
|
||||
## Learn more
|
||||
|
||||
- [Getting Started](https://docs.twenty.com/developers/extend/apps/getting-started)
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "twenty-client-sdk",
|
||||
"version": "2.5.0",
|
||||
"version": "2.5.1",
|
||||
"sideEffects": false,
|
||||
"license": "AGPL-3.0",
|
||||
"scripts": {
|
||||
|
||||
@@ -244,7 +244,7 @@ type FieldPermission {
|
||||
canUpdateFieldValue: Boolean
|
||||
}
|
||||
|
||||
type PermissionFlag {
|
||||
type RolePermissionFlag {
|
||||
id: UUID!
|
||||
roleId: UUID!
|
||||
flag: PermissionFlagType!
|
||||
@@ -276,7 +276,7 @@ type Role {
|
||||
canUpdateAllObjectRecords: Boolean!
|
||||
canSoftDeleteAllObjectRecords: Boolean!
|
||||
canDestroyAllObjectRecords: Boolean!
|
||||
permissionFlags: [PermissionFlag!]
|
||||
permissionFlags: [RolePermissionFlag!]
|
||||
objectPermissions: [ObjectPermission!]
|
||||
fieldPermissions: [FieldPermission!]
|
||||
rowLevelPermissionPredicates: [RowLevelPermissionPredicate!]
|
||||
@@ -1395,6 +1395,7 @@ type PageLayout {
|
||||
objectMetadataId: UUID
|
||||
tabs: [PageLayoutTab!]
|
||||
defaultTabToFocusOnMobileAndSidePanelId: UUID
|
||||
universalIdentifier: UUID!
|
||||
createdAt: DateTime!
|
||||
updatedAt: DateTime!
|
||||
deletedAt: DateTime
|
||||
@@ -1421,6 +1422,22 @@ type ApplicationConnectionProvider {
|
||||
oauth: ApplicationConnectionProviderOAuthConfig
|
||||
}
|
||||
|
||||
type EnterpriseLicenseInfoDTO {
|
||||
isValid: Boolean!
|
||||
licensee: String
|
||||
expiresAt: DateTime
|
||||
subscriptionId: String
|
||||
}
|
||||
|
||||
type EnterpriseSubscriptionStatusDTO {
|
||||
status: String!
|
||||
licensee: String
|
||||
expiresAt: DateTime
|
||||
cancelAt: DateTime
|
||||
currentPeriodEnd: DateTime
|
||||
isCancellationScheduled: Boolean!
|
||||
}
|
||||
|
||||
type Analytics {
|
||||
"""Boolean that confirms query was dispatched"""
|
||||
success: Boolean!
|
||||
@@ -1441,22 +1458,6 @@ type FileWithSignedUrl {
|
||||
url: String!
|
||||
}
|
||||
|
||||
type EnterpriseLicenseInfoDTO {
|
||||
isValid: Boolean!
|
||||
licensee: String
|
||||
expiresAt: DateTime
|
||||
subscriptionId: String
|
||||
}
|
||||
|
||||
type EnterpriseSubscriptionStatusDTO {
|
||||
status: String!
|
||||
licensee: String
|
||||
expiresAt: DateTime
|
||||
cancelAt: DateTime
|
||||
currentPeriodEnd: DateTime
|
||||
isCancellationScheduled: Boolean!
|
||||
}
|
||||
|
||||
type BillingSubscriptionSchedulePhaseItem {
|
||||
price: String!
|
||||
quantity: Float
|
||||
@@ -2417,18 +2418,49 @@ type PlaceDetailsResult {
|
||||
location: Location
|
||||
}
|
||||
|
||||
type ConnectionParametersOutput {
|
||||
type PublicConnectionParametersOutput {
|
||||
host: String!
|
||||
port: Float!
|
||||
username: String
|
||||
password: String!
|
||||
secure: Boolean
|
||||
}
|
||||
|
||||
type ImapSmtpCaldavConnectionParameters {
|
||||
IMAP: ConnectionParametersOutput
|
||||
SMTP: ConnectionParametersOutput
|
||||
CALDAV: ConnectionParametersOutput
|
||||
type PublicImapSmtpCaldavConnectionParameters {
|
||||
IMAP: PublicConnectionParametersOutput
|
||||
SMTP: PublicConnectionParametersOutput
|
||||
CALDAV: PublicConnectionParametersOutput
|
||||
}
|
||||
|
||||
type ConnectedAccountPublicDTO {
|
||||
id: UUID!
|
||||
handle: String!
|
||||
provider: String!
|
||||
lastCredentialsRefreshedAt: DateTime
|
||||
authFailedAt: DateTime
|
||||
handleAliases: [String!]
|
||||
scopes: [String!]
|
||||
lastSignedInAt: DateTime
|
||||
userWorkspaceId: UUID!
|
||||
connectionProviderId: UUID
|
||||
applicationId: UUID
|
||||
name: String
|
||||
visibility: String!
|
||||
createdAt: DateTime!
|
||||
updatedAt: DateTime!
|
||||
connectionParameters: PublicImapSmtpCaldavConnectionParameters
|
||||
}
|
||||
|
||||
type ImapSmtpCaldavPublicConnectionParams {
|
||||
host: String!
|
||||
port: Float!
|
||||
username: String
|
||||
secure: Boolean
|
||||
}
|
||||
|
||||
type ImapSmtpCaldavPublicConnectionParameters {
|
||||
IMAP: ImapSmtpCaldavPublicConnectionParams
|
||||
SMTP: ImapSmtpCaldavPublicConnectionParams
|
||||
CALDAV: ImapSmtpCaldavPublicConnectionParams
|
||||
}
|
||||
|
||||
type ConnectedImapSmtpCaldavAccount {
|
||||
@@ -2436,7 +2468,7 @@ type ConnectedImapSmtpCaldavAccount {
|
||||
handle: String!
|
||||
provider: String!
|
||||
userWorkspaceId: UUID!
|
||||
connectionParameters: ImapSmtpCaldavConnectionParameters
|
||||
connectionParameters: ImapSmtpCaldavPublicConnectionParameters
|
||||
}
|
||||
|
||||
type ImapSmtpCaldavConnectionSuccess {
|
||||
@@ -2551,57 +2583,6 @@ type DuplicatedDashboard {
|
||||
updatedAt: String!
|
||||
}
|
||||
|
||||
type ConnectedAccountDTO {
|
||||
id: UUID!
|
||||
handle: String!
|
||||
provider: String!
|
||||
lastCredentialsRefreshedAt: DateTime
|
||||
authFailedAt: DateTime
|
||||
handleAliases: [String!]
|
||||
scopes: [String!]
|
||||
connectionParameters: ImapSmtpCaldavConnectionParameters
|
||||
lastSignedInAt: DateTime
|
||||
userWorkspaceId: UUID!
|
||||
connectionProviderId: UUID
|
||||
applicationId: UUID
|
||||
name: String
|
||||
visibility: String!
|
||||
createdAt: DateTime!
|
||||
updatedAt: DateTime!
|
||||
}
|
||||
|
||||
type PublicConnectionParametersOutput {
|
||||
host: String!
|
||||
port: Float!
|
||||
username: String
|
||||
secure: Boolean
|
||||
}
|
||||
|
||||
type PublicImapSmtpCaldavConnectionParameters {
|
||||
IMAP: PublicConnectionParametersOutput
|
||||
SMTP: PublicConnectionParametersOutput
|
||||
CALDAV: PublicConnectionParametersOutput
|
||||
}
|
||||
|
||||
type ConnectedAccountPublicDTO {
|
||||
id: UUID!
|
||||
handle: String!
|
||||
provider: String!
|
||||
lastCredentialsRefreshedAt: DateTime
|
||||
authFailedAt: DateTime
|
||||
handleAliases: [String!]
|
||||
scopes: [String!]
|
||||
lastSignedInAt: DateTime
|
||||
userWorkspaceId: UUID!
|
||||
connectionProviderId: UUID
|
||||
applicationId: UUID
|
||||
name: String
|
||||
visibility: String!
|
||||
createdAt: DateTime!
|
||||
updatedAt: DateTime!
|
||||
connectionParameters: PublicImapSmtpCaldavConnectionParameters
|
||||
}
|
||||
|
||||
type SendEmailOutput {
|
||||
success: Boolean!
|
||||
error: String
|
||||
@@ -2884,6 +2865,7 @@ enum AllMetadataName {
|
||||
pageLayoutTab
|
||||
commandMenuItem
|
||||
navigationMenuItem
|
||||
rolePermissionFlag
|
||||
permissionFlag
|
||||
objectPermission
|
||||
fieldPermission
|
||||
@@ -2935,6 +2917,9 @@ type Webhook {
|
||||
type Query {
|
||||
navigationMenuItems: [NavigationMenuItem!]!
|
||||
navigationMenuItem(id: UUID!): NavigationMenuItem
|
||||
enterprisePortalSession(returnUrlPath: String): String
|
||||
enterpriseCheckoutSession(billingInterval: String): String
|
||||
enterpriseSubscriptionStatus: EnterpriseSubscriptionStatusDTO
|
||||
getViewFilterGroups(viewId: String): [ViewFilterGroup!]!
|
||||
getViewFilterGroup(id: String!): ViewFilterGroup
|
||||
getViewFilters(viewId: String): [ViewFilter!]!
|
||||
@@ -2949,9 +2934,6 @@ type Query {
|
||||
getViewFieldGroup(id: String!): ViewFieldGroup
|
||||
apiKeys: [ApiKey!]!
|
||||
apiKey(input: GetApiKeyInput!): ApiKey
|
||||
enterprisePortalSession(returnUrlPath: String): String
|
||||
enterpriseCheckoutSession(billingInterval: String): String
|
||||
enterpriseSubscriptionStatus: EnterpriseSubscriptionStatusDTO
|
||||
billingPortalSession(returnUrlPath: String): BillingSession!
|
||||
listPlans: [BillingPlan!]!
|
||||
getResourceCreditUsage: [BillingResourceCreditUsage!]!
|
||||
@@ -3015,9 +2997,9 @@ type Query {
|
||||
getViewGroup(id: String!): ViewGroup
|
||||
myMessageFolders(messageChannelId: UUID): [MessageFolder!]!
|
||||
myMessageChannels(connectedAccountId: UUID): [MessageChannel!]!
|
||||
myConnectedAccounts: [ConnectedAccountDTO!]!
|
||||
myConnectedAccounts: [ConnectedAccountPublicDTO!]!
|
||||
connectedAccountById(id: UUID!): ConnectedAccountPublicDTO
|
||||
connectedAccounts: [ConnectedAccountDTO!]!
|
||||
connectedAccounts: [ConnectedAccountPublicDTO!]!
|
||||
myCalendarChannels(connectedAccountId: UUID): [CalendarChannel!]!
|
||||
webhooks: [Webhook!]!
|
||||
webhook(id: UUID!): Webhook
|
||||
@@ -3144,6 +3126,8 @@ type Mutation {
|
||||
deleteManyNavigationMenuItems(ids: [UUID!]!): [NavigationMenuItem!]!
|
||||
deleteNavigationMenuItem(id: UUID!): NavigationMenuItem!
|
||||
uploadEmailAttachmentFile(file: Upload!): FileWithSignedUrl!
|
||||
refreshEnterpriseValidityToken: Boolean!
|
||||
setEnterpriseKey(enterpriseKey: String!): EnterpriseLicenseInfoDTO!
|
||||
uploadAiChatFile(file: Upload!): FileWithSignedUrl!
|
||||
uploadWorkflowFile(file: Upload!): FileWithSignedUrl!
|
||||
uploadWorkspaceLogo(file: Upload!): FileWithSignedUrl!
|
||||
@@ -3184,8 +3168,6 @@ type Mutation {
|
||||
assignRoleToApiKey(apiKeyId: UUID!, roleId: UUID!): Boolean!
|
||||
createObjectEvent(event: String!, recordId: UUID!, objectMetadataId: UUID!, properties: JSON): Analytics!
|
||||
trackAnalytics(type: AnalyticsType!, name: String, event: String, properties: JSON): Analytics!
|
||||
refreshEnterpriseValidityToken: Boolean!
|
||||
setEnterpriseKey(enterpriseKey: String!): EnterpriseLicenseInfoDTO!
|
||||
skipSyncEmailOnboardingStep: OnboardingStepSuccess!
|
||||
skipBookOnboardingStep: OnboardingStepSuccess!
|
||||
checkoutSession(recurringInterval: SubscriptionInterval!, plan: BillingPlanKey! = PRO, requirePaymentMethod: Boolean! = true, successUrlPath: String): BillingSession!
|
||||
@@ -3237,7 +3219,7 @@ type Mutation {
|
||||
updateOneRole(updateRoleInput: UpdateRoleInput!): Role!
|
||||
deleteOneRole(roleId: UUID!): String!
|
||||
upsertObjectPermissions(upsertObjectPermissionsInput: UpsertObjectPermissionsInput!): [ObjectPermission!]!
|
||||
upsertPermissionFlags(upsertPermissionFlagsInput: UpsertPermissionFlagsInput!): [PermissionFlag!]!
|
||||
upsertPermissionFlags(upsertPermissionFlagsInput: UpsertPermissionFlagsInput!): [RolePermissionFlag!]!
|
||||
upsertFieldPermissions(upsertFieldPermissionsInput: UpsertFieldPermissionsInput!): [FieldPermission!]!
|
||||
upsertRowLevelPermissionPredicates(input: UpsertRowLevelPermissionPredicatesInput!): UpsertRowLevelPermissionPredicatesResult!
|
||||
assignRoleToAgent(agentId: UUID!, roleId: UUID!): Boolean!
|
||||
@@ -3256,13 +3238,13 @@ type Mutation {
|
||||
updateMessageChannel(input: UpdateMessageChannelInput!): MessageChannel!
|
||||
createEmailGroupChannel(input: CreateEmailGroupChannelInput!): CreateEmailGroupChannelOutput!
|
||||
deleteEmailGroupChannel(id: UUID!): MessageChannel!
|
||||
deleteConnectedAccount(id: UUID!): ConnectedAccountDTO!
|
||||
deleteConnectedAccount(id: UUID!): ConnectedAccountPublicDTO!
|
||||
updateCalendarChannel(input: UpdateCalendarChannelInput!): CalendarChannel!
|
||||
createWebhook(input: CreateWebhookInput!): Webhook!
|
||||
updateWebhook(input: UpdateWebhookInput!): Webhook!
|
||||
deleteWebhook(id: UUID!): Webhook!
|
||||
createChatThread: AgentChatThread!
|
||||
sendChatMessage(threadId: UUID!, text: String!, messageId: UUID!, browsingContext: JSON, modelId: String, fileIds: [UUID!]): SendChatMessageResult!
|
||||
sendChatMessage(threadId: UUID!, text: String!, messageId: UUID!, browsingContext: JSON, modelId: String, fileAttachments: [FileAttachmentInput!]): SendChatMessageResult!
|
||||
stopAgentChatStream(threadId: UUID!): Boolean!
|
||||
renameChatThread(id: UUID!, title: String!): AgentChatThread!
|
||||
archiveChatThread(id: UUID!): AgentChatThread!
|
||||
@@ -3325,7 +3307,7 @@ type Mutation {
|
||||
impersonate(userId: UUID!, workspaceId: UUID!): Impersonate!
|
||||
sendEmail(input: SendEmailInput!): SendEmailOutput!
|
||||
startChannelSync(connectedAccountId: UUID!): ChannelSyncSuccess!
|
||||
saveImapSmtpCaldavAccount(accountOwnerId: UUID!, handle: String!, connectionParameters: EmailAccountConnectionParameters!, id: UUID): ImapSmtpCaldavConnectionSuccess!
|
||||
saveImapSmtpCaldavAccount(handle: String!, connectionParameters: EmailAccountConnectionParameters!, id: UUID): ImapSmtpCaldavConnectionSuccess!
|
||||
updateLabPublicFeatureFlag(input: UpdateLabPublicFeatureFlagInput!): FeatureFlag!
|
||||
createPublicDomain(domain: String!, applicationId: String): PublicDomain!
|
||||
updatePublicDomain(domain: String!, applicationId: String): PublicDomain!
|
||||
@@ -4227,6 +4209,11 @@ input UpdateWebhookInputUpdates {
|
||||
secret: String
|
||||
}
|
||||
|
||||
input FileAttachmentInput {
|
||||
id: UUID!
|
||||
filename: String!
|
||||
}
|
||||
|
||||
input CreateSkillInput {
|
||||
id: UUID
|
||||
name: String!
|
||||
@@ -4383,16 +4370,16 @@ input SendEmailAttachmentInput {
|
||||
}
|
||||
|
||||
input EmailAccountConnectionParameters {
|
||||
IMAP: ConnectionParameters
|
||||
SMTP: ConnectionParameters
|
||||
CALDAV: ConnectionParameters
|
||||
IMAP: ConnectionParametersInput
|
||||
SMTP: ConnectionParametersInput
|
||||
CALDAV: ConnectionParametersInput
|
||||
}
|
||||
|
||||
input ConnectionParameters {
|
||||
input ConnectionParametersInput {
|
||||
host: String!
|
||||
port: Float!
|
||||
username: String
|
||||
password: String!
|
||||
password: String
|
||||
secure: Boolean
|
||||
}
|
||||
|
||||
|
||||
@@ -190,11 +190,11 @@ export interface FieldPermission {
|
||||
__typename: 'FieldPermission'
|
||||
}
|
||||
|
||||
export interface PermissionFlag {
|
||||
export interface RolePermissionFlag {
|
||||
id: Scalars['UUID']
|
||||
roleId: Scalars['UUID']
|
||||
flag: PermissionFlagType
|
||||
__typename: 'PermissionFlag'
|
||||
__typename: 'RolePermissionFlag'
|
||||
}
|
||||
|
||||
export interface ApiKeyForRole {
|
||||
@@ -224,7 +224,7 @@ export interface Role {
|
||||
canUpdateAllObjectRecords: Scalars['Boolean']
|
||||
canSoftDeleteAllObjectRecords: Scalars['Boolean']
|
||||
canDestroyAllObjectRecords: Scalars['Boolean']
|
||||
permissionFlags?: PermissionFlag[]
|
||||
permissionFlags?: RolePermissionFlag[]
|
||||
objectPermissions?: ObjectPermission[]
|
||||
fieldPermissions?: FieldPermission[]
|
||||
rowLevelPermissionPredicates?: RowLevelPermissionPredicate[]
|
||||
@@ -1056,6 +1056,7 @@ export interface PageLayout {
|
||||
objectMetadataId?: Scalars['UUID']
|
||||
tabs?: PageLayoutTab[]
|
||||
defaultTabToFocusOnMobileAndSidePanelId?: Scalars['UUID']
|
||||
universalIdentifier: Scalars['UUID']
|
||||
createdAt: Scalars['DateTime']
|
||||
updatedAt: Scalars['DateTime']
|
||||
deletedAt?: Scalars['DateTime']
|
||||
@@ -1080,6 +1081,24 @@ export interface ApplicationConnectionProvider {
|
||||
__typename: 'ApplicationConnectionProvider'
|
||||
}
|
||||
|
||||
export interface EnterpriseLicenseInfoDTO {
|
||||
isValid: Scalars['Boolean']
|
||||
licensee?: Scalars['String']
|
||||
expiresAt?: Scalars['DateTime']
|
||||
subscriptionId?: Scalars['String']
|
||||
__typename: 'EnterpriseLicenseInfoDTO'
|
||||
}
|
||||
|
||||
export interface EnterpriseSubscriptionStatusDTO {
|
||||
status: Scalars['String']
|
||||
licensee?: Scalars['String']
|
||||
expiresAt?: Scalars['DateTime']
|
||||
cancelAt?: Scalars['DateTime']
|
||||
currentPeriodEnd?: Scalars['DateTime']
|
||||
isCancellationScheduled: Scalars['Boolean']
|
||||
__typename: 'EnterpriseSubscriptionStatusDTO'
|
||||
}
|
||||
|
||||
export interface Analytics {
|
||||
/** Boolean that confirms query was dispatched */
|
||||
success: Scalars['Boolean']
|
||||
@@ -1103,24 +1122,6 @@ export interface FileWithSignedUrl {
|
||||
__typename: 'FileWithSignedUrl'
|
||||
}
|
||||
|
||||
export interface EnterpriseLicenseInfoDTO {
|
||||
isValid: Scalars['Boolean']
|
||||
licensee?: Scalars['String']
|
||||
expiresAt?: Scalars['DateTime']
|
||||
subscriptionId?: Scalars['String']
|
||||
__typename: 'EnterpriseLicenseInfoDTO'
|
||||
}
|
||||
|
||||
export interface EnterpriseSubscriptionStatusDTO {
|
||||
status: Scalars['String']
|
||||
licensee?: Scalars['String']
|
||||
expiresAt?: Scalars['DateTime']
|
||||
cancelAt?: Scalars['DateTime']
|
||||
currentPeriodEnd?: Scalars['DateTime']
|
||||
isCancellationScheduled: Scalars['Boolean']
|
||||
__typename: 'EnterpriseSubscriptionStatusDTO'
|
||||
}
|
||||
|
||||
export interface BillingSubscriptionSchedulePhaseItem {
|
||||
price: Scalars['String']
|
||||
quantity?: Scalars['Float']
|
||||
@@ -2094,20 +2095,54 @@ export interface PlaceDetailsResult {
|
||||
__typename: 'PlaceDetailsResult'
|
||||
}
|
||||
|
||||
export interface ConnectionParametersOutput {
|
||||
export interface PublicConnectionParametersOutput {
|
||||
host: Scalars['String']
|
||||
port: Scalars['Float']
|
||||
username?: Scalars['String']
|
||||
password: Scalars['String']
|
||||
secure?: Scalars['Boolean']
|
||||
__typename: 'ConnectionParametersOutput'
|
||||
__typename: 'PublicConnectionParametersOutput'
|
||||
}
|
||||
|
||||
export interface ImapSmtpCaldavConnectionParameters {
|
||||
IMAP?: ConnectionParametersOutput
|
||||
SMTP?: ConnectionParametersOutput
|
||||
CALDAV?: ConnectionParametersOutput
|
||||
__typename: 'ImapSmtpCaldavConnectionParameters'
|
||||
export interface PublicImapSmtpCaldavConnectionParameters {
|
||||
IMAP?: PublicConnectionParametersOutput
|
||||
SMTP?: PublicConnectionParametersOutput
|
||||
CALDAV?: PublicConnectionParametersOutput
|
||||
__typename: 'PublicImapSmtpCaldavConnectionParameters'
|
||||
}
|
||||
|
||||
export interface ConnectedAccountPublicDTO {
|
||||
id: Scalars['UUID']
|
||||
handle: Scalars['String']
|
||||
provider: Scalars['String']
|
||||
lastCredentialsRefreshedAt?: Scalars['DateTime']
|
||||
authFailedAt?: Scalars['DateTime']
|
||||
handleAliases?: Scalars['String'][]
|
||||
scopes?: Scalars['String'][]
|
||||
lastSignedInAt?: Scalars['DateTime']
|
||||
userWorkspaceId: Scalars['UUID']
|
||||
connectionProviderId?: Scalars['UUID']
|
||||
applicationId?: Scalars['UUID']
|
||||
name?: Scalars['String']
|
||||
visibility: Scalars['String']
|
||||
createdAt: Scalars['DateTime']
|
||||
updatedAt: Scalars['DateTime']
|
||||
connectionParameters?: PublicImapSmtpCaldavConnectionParameters
|
||||
__typename: 'ConnectedAccountPublicDTO'
|
||||
}
|
||||
|
||||
export interface ImapSmtpCaldavPublicConnectionParams {
|
||||
host: Scalars['String']
|
||||
port: Scalars['Float']
|
||||
username?: Scalars['String']
|
||||
secure?: Scalars['Boolean']
|
||||
__typename: 'ImapSmtpCaldavPublicConnectionParams'
|
||||
}
|
||||
|
||||
export interface ImapSmtpCaldavPublicConnectionParameters {
|
||||
IMAP?: ImapSmtpCaldavPublicConnectionParams
|
||||
SMTP?: ImapSmtpCaldavPublicConnectionParams
|
||||
CALDAV?: ImapSmtpCaldavPublicConnectionParams
|
||||
__typename: 'ImapSmtpCaldavPublicConnectionParameters'
|
||||
}
|
||||
|
||||
export interface ConnectedImapSmtpCaldavAccount {
|
||||
@@ -2115,7 +2150,7 @@ export interface ConnectedImapSmtpCaldavAccount {
|
||||
handle: Scalars['String']
|
||||
provider: Scalars['String']
|
||||
userWorkspaceId: Scalars['UUID']
|
||||
connectionParameters?: ImapSmtpCaldavConnectionParameters
|
||||
connectionParameters?: ImapSmtpCaldavPublicConnectionParameters
|
||||
__typename: 'ConnectedImapSmtpCaldavAccount'
|
||||
}
|
||||
|
||||
@@ -2243,61 +2278,6 @@ export interface DuplicatedDashboard {
|
||||
__typename: 'DuplicatedDashboard'
|
||||
}
|
||||
|
||||
export interface ConnectedAccountDTO {
|
||||
id: Scalars['UUID']
|
||||
handle: Scalars['String']
|
||||
provider: Scalars['String']
|
||||
lastCredentialsRefreshedAt?: Scalars['DateTime']
|
||||
authFailedAt?: Scalars['DateTime']
|
||||
handleAliases?: Scalars['String'][]
|
||||
scopes?: Scalars['String'][]
|
||||
connectionParameters?: ImapSmtpCaldavConnectionParameters
|
||||
lastSignedInAt?: Scalars['DateTime']
|
||||
userWorkspaceId: Scalars['UUID']
|
||||
connectionProviderId?: Scalars['UUID']
|
||||
applicationId?: Scalars['UUID']
|
||||
name?: Scalars['String']
|
||||
visibility: Scalars['String']
|
||||
createdAt: Scalars['DateTime']
|
||||
updatedAt: Scalars['DateTime']
|
||||
__typename: 'ConnectedAccountDTO'
|
||||
}
|
||||
|
||||
export interface PublicConnectionParametersOutput {
|
||||
host: Scalars['String']
|
||||
port: Scalars['Float']
|
||||
username?: Scalars['String']
|
||||
secure?: Scalars['Boolean']
|
||||
__typename: 'PublicConnectionParametersOutput'
|
||||
}
|
||||
|
||||
export interface PublicImapSmtpCaldavConnectionParameters {
|
||||
IMAP?: PublicConnectionParametersOutput
|
||||
SMTP?: PublicConnectionParametersOutput
|
||||
CALDAV?: PublicConnectionParametersOutput
|
||||
__typename: 'PublicImapSmtpCaldavConnectionParameters'
|
||||
}
|
||||
|
||||
export interface ConnectedAccountPublicDTO {
|
||||
id: Scalars['UUID']
|
||||
handle: Scalars['String']
|
||||
provider: Scalars['String']
|
||||
lastCredentialsRefreshedAt?: Scalars['DateTime']
|
||||
authFailedAt?: Scalars['DateTime']
|
||||
handleAliases?: Scalars['String'][]
|
||||
scopes?: Scalars['String'][]
|
||||
lastSignedInAt?: Scalars['DateTime']
|
||||
userWorkspaceId: Scalars['UUID']
|
||||
connectionProviderId?: Scalars['UUID']
|
||||
applicationId?: Scalars['UUID']
|
||||
name?: Scalars['String']
|
||||
visibility: Scalars['String']
|
||||
createdAt: Scalars['DateTime']
|
||||
updatedAt: Scalars['DateTime']
|
||||
connectionParameters?: PublicImapSmtpCaldavConnectionParameters
|
||||
__typename: 'ConnectedAccountPublicDTO'
|
||||
}
|
||||
|
||||
export interface SendEmailOutput {
|
||||
success: Scalars['Boolean']
|
||||
error?: Scalars['String']
|
||||
@@ -2516,7 +2496,7 @@ export interface CollectionHash {
|
||||
__typename: 'CollectionHash'
|
||||
}
|
||||
|
||||
export type AllMetadataName = 'fieldMetadata' | 'objectMetadata' | 'view' | 'viewField' | 'viewFieldGroup' | 'viewGroup' | 'viewSort' | 'rowLevelPermissionPredicate' | 'rowLevelPermissionPredicateGroup' | 'viewFilterGroup' | 'index' | 'logicFunction' | 'viewFilter' | 'role' | 'roleTarget' | 'agent' | 'skill' | 'pageLayout' | 'pageLayoutWidget' | 'pageLayoutTab' | 'commandMenuItem' | 'navigationMenuItem' | 'permissionFlag' | 'objectPermission' | 'fieldPermission' | 'frontComponent' | 'webhook' | 'applicationVariable' | 'connectionProvider'
|
||||
export type AllMetadataName = 'fieldMetadata' | 'objectMetadata' | 'view' | 'viewField' | 'viewFieldGroup' | 'viewGroup' | 'viewSort' | 'rowLevelPermissionPredicate' | 'rowLevelPermissionPredicateGroup' | 'viewFilterGroup' | 'index' | 'logicFunction' | 'viewFilter' | 'role' | 'roleTarget' | 'agent' | 'skill' | 'pageLayout' | 'pageLayoutWidget' | 'pageLayoutTab' | 'commandMenuItem' | 'navigationMenuItem' | 'rolePermissionFlag' | 'permissionFlag' | 'objectPermission' | 'fieldPermission' | 'frontComponent' | 'webhook' | 'applicationVariable' | 'connectionProvider'
|
||||
|
||||
export interface MinimalObjectMetadata {
|
||||
id: Scalars['UUID']
|
||||
@@ -2564,6 +2544,9 @@ export interface Webhook {
|
||||
export interface Query {
|
||||
navigationMenuItems: NavigationMenuItem[]
|
||||
navigationMenuItem?: NavigationMenuItem
|
||||
enterprisePortalSession?: Scalars['String']
|
||||
enterpriseCheckoutSession?: Scalars['String']
|
||||
enterpriseSubscriptionStatus?: EnterpriseSubscriptionStatusDTO
|
||||
getViewFilterGroups: ViewFilterGroup[]
|
||||
getViewFilterGroup?: ViewFilterGroup
|
||||
getViewFilters: ViewFilter[]
|
||||
@@ -2578,9 +2561,6 @@ export interface Query {
|
||||
getViewFieldGroup?: ViewFieldGroup
|
||||
apiKeys: ApiKey[]
|
||||
apiKey?: ApiKey
|
||||
enterprisePortalSession?: Scalars['String']
|
||||
enterpriseCheckoutSession?: Scalars['String']
|
||||
enterpriseSubscriptionStatus?: EnterpriseSubscriptionStatusDTO
|
||||
billingPortalSession: BillingSession
|
||||
listPlans: BillingPlan[]
|
||||
getResourceCreditUsage: BillingResourceCreditUsage[]
|
||||
@@ -2617,9 +2597,9 @@ export interface Query {
|
||||
getViewGroup?: ViewGroup
|
||||
myMessageFolders: MessageFolder[]
|
||||
myMessageChannels: MessageChannel[]
|
||||
myConnectedAccounts: ConnectedAccountDTO[]
|
||||
myConnectedAccounts: ConnectedAccountPublicDTO[]
|
||||
connectedAccountById?: ConnectedAccountPublicDTO
|
||||
connectedAccounts: ConnectedAccountDTO[]
|
||||
connectedAccounts: ConnectedAccountPublicDTO[]
|
||||
myCalendarChannels: CalendarChannel[]
|
||||
webhooks: Webhook[]
|
||||
webhook?: Webhook
|
||||
@@ -2679,6 +2659,8 @@ export interface Mutation {
|
||||
deleteManyNavigationMenuItems: NavigationMenuItem[]
|
||||
deleteNavigationMenuItem: NavigationMenuItem
|
||||
uploadEmailAttachmentFile: FileWithSignedUrl
|
||||
refreshEnterpriseValidityToken: Scalars['Boolean']
|
||||
setEnterpriseKey: EnterpriseLicenseInfoDTO
|
||||
uploadAiChatFile: FileWithSignedUrl
|
||||
uploadWorkflowFile: FileWithSignedUrl
|
||||
uploadWorkspaceLogo: FileWithSignedUrl
|
||||
@@ -2719,8 +2701,6 @@ export interface Mutation {
|
||||
assignRoleToApiKey: Scalars['Boolean']
|
||||
createObjectEvent: Analytics
|
||||
trackAnalytics: Analytics
|
||||
refreshEnterpriseValidityToken: Scalars['Boolean']
|
||||
setEnterpriseKey: EnterpriseLicenseInfoDTO
|
||||
skipSyncEmailOnboardingStep: OnboardingStepSuccess
|
||||
skipBookOnboardingStep: OnboardingStepSuccess
|
||||
checkoutSession: BillingSession
|
||||
@@ -2772,7 +2752,7 @@ export interface Mutation {
|
||||
updateOneRole: Role
|
||||
deleteOneRole: Scalars['String']
|
||||
upsertObjectPermissions: ObjectPermission[]
|
||||
upsertPermissionFlags: PermissionFlag[]
|
||||
upsertPermissionFlags: RolePermissionFlag[]
|
||||
upsertFieldPermissions: FieldPermission[]
|
||||
upsertRowLevelPermissionPredicates: UpsertRowLevelPermissionPredicatesResult
|
||||
assignRoleToAgent: Scalars['Boolean']
|
||||
@@ -2791,7 +2771,7 @@ export interface Mutation {
|
||||
updateMessageChannel: MessageChannel
|
||||
createEmailGroupChannel: CreateEmailGroupChannelOutput
|
||||
deleteEmailGroupChannel: MessageChannel
|
||||
deleteConnectedAccount: ConnectedAccountDTO
|
||||
deleteConnectedAccount: ConnectedAccountPublicDTO
|
||||
updateCalendarChannel: CalendarChannel
|
||||
createWebhook: Webhook
|
||||
updateWebhook: Webhook
|
||||
@@ -3072,7 +3052,7 @@ export interface FieldPermissionGenqlSelection{
|
||||
__scalar?: boolean | number
|
||||
}
|
||||
|
||||
export interface PermissionFlagGenqlSelection{
|
||||
export interface RolePermissionFlagGenqlSelection{
|
||||
id?: boolean | number
|
||||
roleId?: boolean | number
|
||||
flag?: boolean | number
|
||||
@@ -3108,7 +3088,7 @@ export interface RoleGenqlSelection{
|
||||
canUpdateAllObjectRecords?: boolean | number
|
||||
canSoftDeleteAllObjectRecords?: boolean | number
|
||||
canDestroyAllObjectRecords?: boolean | number
|
||||
permissionFlags?: PermissionFlagGenqlSelection
|
||||
permissionFlags?: RolePermissionFlagGenqlSelection
|
||||
objectPermissions?: ObjectPermissionGenqlSelection
|
||||
fieldPermissions?: FieldPermissionGenqlSelection
|
||||
rowLevelPermissionPredicates?: RowLevelPermissionPredicateGenqlSelection
|
||||
@@ -4004,6 +3984,7 @@ export interface PageLayoutGenqlSelection{
|
||||
objectMetadataId?: boolean | number
|
||||
tabs?: PageLayoutTabGenqlSelection
|
||||
defaultTabToFocusOnMobileAndSidePanelId?: boolean | number
|
||||
universalIdentifier?: boolean | number
|
||||
createdAt?: boolean | number
|
||||
updatedAt?: boolean | number
|
||||
deletedAt?: boolean | number
|
||||
@@ -4029,6 +4010,26 @@ export interface ApplicationConnectionProviderGenqlSelection{
|
||||
__scalar?: boolean | number
|
||||
}
|
||||
|
||||
export interface EnterpriseLicenseInfoDTOGenqlSelection{
|
||||
isValid?: boolean | number
|
||||
licensee?: boolean | number
|
||||
expiresAt?: boolean | number
|
||||
subscriptionId?: boolean | number
|
||||
__typename?: boolean | number
|
||||
__scalar?: boolean | number
|
||||
}
|
||||
|
||||
export interface EnterpriseSubscriptionStatusDTOGenqlSelection{
|
||||
status?: boolean | number
|
||||
licensee?: boolean | number
|
||||
expiresAt?: boolean | number
|
||||
cancelAt?: boolean | number
|
||||
currentPeriodEnd?: boolean | number
|
||||
isCancellationScheduled?: boolean | number
|
||||
__typename?: boolean | number
|
||||
__scalar?: boolean | number
|
||||
}
|
||||
|
||||
export interface AnalyticsGenqlSelection{
|
||||
/** Boolean that confirms query was dispatched */
|
||||
success?: boolean | number
|
||||
@@ -4055,26 +4056,6 @@ export interface FileWithSignedUrlGenqlSelection{
|
||||
__scalar?: boolean | number
|
||||
}
|
||||
|
||||
export interface EnterpriseLicenseInfoDTOGenqlSelection{
|
||||
isValid?: boolean | number
|
||||
licensee?: boolean | number
|
||||
expiresAt?: boolean | number
|
||||
subscriptionId?: boolean | number
|
||||
__typename?: boolean | number
|
||||
__scalar?: boolean | number
|
||||
}
|
||||
|
||||
export interface EnterpriseSubscriptionStatusDTOGenqlSelection{
|
||||
status?: boolean | number
|
||||
licensee?: boolean | number
|
||||
expiresAt?: boolean | number
|
||||
cancelAt?: boolean | number
|
||||
currentPeriodEnd?: boolean | number
|
||||
isCancellationScheduled?: boolean | number
|
||||
__typename?: boolean | number
|
||||
__scalar?: boolean | number
|
||||
}
|
||||
|
||||
export interface BillingSubscriptionSchedulePhaseItemGenqlSelection{
|
||||
price?: boolean | number
|
||||
quantity?: boolean | number
|
||||
@@ -5114,20 +5095,57 @@ export interface PlaceDetailsResultGenqlSelection{
|
||||
__scalar?: boolean | number
|
||||
}
|
||||
|
||||
export interface ConnectionParametersOutputGenqlSelection{
|
||||
export interface PublicConnectionParametersOutputGenqlSelection{
|
||||
host?: boolean | number
|
||||
port?: boolean | number
|
||||
username?: boolean | number
|
||||
password?: boolean | number
|
||||
secure?: boolean | number
|
||||
__typename?: boolean | number
|
||||
__scalar?: boolean | number
|
||||
}
|
||||
|
||||
export interface ImapSmtpCaldavConnectionParametersGenqlSelection{
|
||||
IMAP?: ConnectionParametersOutputGenqlSelection
|
||||
SMTP?: ConnectionParametersOutputGenqlSelection
|
||||
CALDAV?: ConnectionParametersOutputGenqlSelection
|
||||
export interface PublicImapSmtpCaldavConnectionParametersGenqlSelection{
|
||||
IMAP?: PublicConnectionParametersOutputGenqlSelection
|
||||
SMTP?: PublicConnectionParametersOutputGenqlSelection
|
||||
CALDAV?: PublicConnectionParametersOutputGenqlSelection
|
||||
__typename?: boolean | number
|
||||
__scalar?: boolean | number
|
||||
}
|
||||
|
||||
export interface ConnectedAccountPublicDTOGenqlSelection{
|
||||
id?: boolean | number
|
||||
handle?: boolean | number
|
||||
provider?: boolean | number
|
||||
lastCredentialsRefreshedAt?: boolean | number
|
||||
authFailedAt?: boolean | number
|
||||
handleAliases?: boolean | number
|
||||
scopes?: boolean | number
|
||||
lastSignedInAt?: boolean | number
|
||||
userWorkspaceId?: boolean | number
|
||||
connectionProviderId?: boolean | number
|
||||
applicationId?: boolean | number
|
||||
name?: boolean | number
|
||||
visibility?: boolean | number
|
||||
createdAt?: boolean | number
|
||||
updatedAt?: boolean | number
|
||||
connectionParameters?: PublicImapSmtpCaldavConnectionParametersGenqlSelection
|
||||
__typename?: boolean | number
|
||||
__scalar?: boolean | number
|
||||
}
|
||||
|
||||
export interface ImapSmtpCaldavPublicConnectionParamsGenqlSelection{
|
||||
host?: boolean | number
|
||||
port?: boolean | number
|
||||
username?: boolean | number
|
||||
secure?: boolean | number
|
||||
__typename?: boolean | number
|
||||
__scalar?: boolean | number
|
||||
}
|
||||
|
||||
export interface ImapSmtpCaldavPublicConnectionParametersGenqlSelection{
|
||||
IMAP?: ImapSmtpCaldavPublicConnectionParamsGenqlSelection
|
||||
SMTP?: ImapSmtpCaldavPublicConnectionParamsGenqlSelection
|
||||
CALDAV?: ImapSmtpCaldavPublicConnectionParamsGenqlSelection
|
||||
__typename?: boolean | number
|
||||
__scalar?: boolean | number
|
||||
}
|
||||
@@ -5137,7 +5155,7 @@ export interface ConnectedImapSmtpCaldavAccountGenqlSelection{
|
||||
handle?: boolean | number
|
||||
provider?: boolean | number
|
||||
userWorkspaceId?: boolean | number
|
||||
connectionParameters?: ImapSmtpCaldavConnectionParametersGenqlSelection
|
||||
connectionParameters?: ImapSmtpCaldavPublicConnectionParametersGenqlSelection
|
||||
__typename?: boolean | number
|
||||
__scalar?: boolean | number
|
||||
}
|
||||
@@ -5278,65 +5296,6 @@ export interface DuplicatedDashboardGenqlSelection{
|
||||
__scalar?: boolean | number
|
||||
}
|
||||
|
||||
export interface ConnectedAccountDTOGenqlSelection{
|
||||
id?: boolean | number
|
||||
handle?: boolean | number
|
||||
provider?: boolean | number
|
||||
lastCredentialsRefreshedAt?: boolean | number
|
||||
authFailedAt?: boolean | number
|
||||
handleAliases?: boolean | number
|
||||
scopes?: boolean | number
|
||||
connectionParameters?: ImapSmtpCaldavConnectionParametersGenqlSelection
|
||||
lastSignedInAt?: boolean | number
|
||||
userWorkspaceId?: boolean | number
|
||||
connectionProviderId?: boolean | number
|
||||
applicationId?: boolean | number
|
||||
name?: boolean | number
|
||||
visibility?: boolean | number
|
||||
createdAt?: boolean | number
|
||||
updatedAt?: boolean | number
|
||||
__typename?: boolean | number
|
||||
__scalar?: boolean | number
|
||||
}
|
||||
|
||||
export interface PublicConnectionParametersOutputGenqlSelection{
|
||||
host?: boolean | number
|
||||
port?: boolean | number
|
||||
username?: boolean | number
|
||||
secure?: boolean | number
|
||||
__typename?: boolean | number
|
||||
__scalar?: boolean | number
|
||||
}
|
||||
|
||||
export interface PublicImapSmtpCaldavConnectionParametersGenqlSelection{
|
||||
IMAP?: PublicConnectionParametersOutputGenqlSelection
|
||||
SMTP?: PublicConnectionParametersOutputGenqlSelection
|
||||
CALDAV?: PublicConnectionParametersOutputGenqlSelection
|
||||
__typename?: boolean | number
|
||||
__scalar?: boolean | number
|
||||
}
|
||||
|
||||
export interface ConnectedAccountPublicDTOGenqlSelection{
|
||||
id?: boolean | number
|
||||
handle?: boolean | number
|
||||
provider?: boolean | number
|
||||
lastCredentialsRefreshedAt?: boolean | number
|
||||
authFailedAt?: boolean | number
|
||||
handleAliases?: boolean | number
|
||||
scopes?: boolean | number
|
||||
lastSignedInAt?: boolean | number
|
||||
userWorkspaceId?: boolean | number
|
||||
connectionProviderId?: boolean | number
|
||||
applicationId?: boolean | number
|
||||
name?: boolean | number
|
||||
visibility?: boolean | number
|
||||
createdAt?: boolean | number
|
||||
updatedAt?: boolean | number
|
||||
connectionParameters?: PublicImapSmtpCaldavConnectionParametersGenqlSelection
|
||||
__typename?: boolean | number
|
||||
__scalar?: boolean | number
|
||||
}
|
||||
|
||||
export interface SendEmailOutputGenqlSelection{
|
||||
success?: boolean | number
|
||||
error?: boolean | number
|
||||
@@ -5600,6 +5559,9 @@ export interface WebhookGenqlSelection{
|
||||
export interface QueryGenqlSelection{
|
||||
navigationMenuItems?: NavigationMenuItemGenqlSelection
|
||||
navigationMenuItem?: (NavigationMenuItemGenqlSelection & { __args: {id: Scalars['UUID']} })
|
||||
enterprisePortalSession?: { __args: {returnUrlPath?: (Scalars['String'] | null)} } | boolean | number
|
||||
enterpriseCheckoutSession?: { __args: {billingInterval?: (Scalars['String'] | null)} } | boolean | number
|
||||
enterpriseSubscriptionStatus?: EnterpriseSubscriptionStatusDTOGenqlSelection
|
||||
getViewFilterGroups?: (ViewFilterGroupGenqlSelection & { __args?: {viewId?: (Scalars['String'] | null)} })
|
||||
getViewFilterGroup?: (ViewFilterGroupGenqlSelection & { __args: {id: Scalars['String']} })
|
||||
getViewFilters?: (ViewFilterGenqlSelection & { __args?: {viewId?: (Scalars['String'] | null)} })
|
||||
@@ -5614,9 +5576,6 @@ export interface QueryGenqlSelection{
|
||||
getViewFieldGroup?: (ViewFieldGroupGenqlSelection & { __args: {id: Scalars['String']} })
|
||||
apiKeys?: ApiKeyGenqlSelection
|
||||
apiKey?: (ApiKeyGenqlSelection & { __args: {input: GetApiKeyInput} })
|
||||
enterprisePortalSession?: { __args: {returnUrlPath?: (Scalars['String'] | null)} } | boolean | number
|
||||
enterpriseCheckoutSession?: { __args: {billingInterval?: (Scalars['String'] | null)} } | boolean | number
|
||||
enterpriseSubscriptionStatus?: EnterpriseSubscriptionStatusDTOGenqlSelection
|
||||
billingPortalSession?: (BillingSessionGenqlSelection & { __args?: {returnUrlPath?: (Scalars['String'] | null)} })
|
||||
listPlans?: BillingPlanGenqlSelection
|
||||
getResourceCreditUsage?: BillingResourceCreditUsageGenqlSelection
|
||||
@@ -5671,9 +5630,9 @@ export interface QueryGenqlSelection{
|
||||
getViewGroup?: (ViewGroupGenqlSelection & { __args: {id: Scalars['String']} })
|
||||
myMessageFolders?: (MessageFolderGenqlSelection & { __args?: {messageChannelId?: (Scalars['UUID'] | null)} })
|
||||
myMessageChannels?: (MessageChannelGenqlSelection & { __args?: {connectedAccountId?: (Scalars['UUID'] | null)} })
|
||||
myConnectedAccounts?: ConnectedAccountDTOGenqlSelection
|
||||
myConnectedAccounts?: ConnectedAccountPublicDTOGenqlSelection
|
||||
connectedAccountById?: (ConnectedAccountPublicDTOGenqlSelection & { __args: {id: Scalars['UUID']} })
|
||||
connectedAccounts?: ConnectedAccountDTOGenqlSelection
|
||||
connectedAccounts?: ConnectedAccountPublicDTOGenqlSelection
|
||||
myCalendarChannels?: (CalendarChannelGenqlSelection & { __args?: {connectedAccountId?: (Scalars['UUID'] | null)} })
|
||||
webhooks?: WebhookGenqlSelection
|
||||
webhook?: (WebhookGenqlSelection & { __args: {id: Scalars['UUID']} })
|
||||
@@ -5754,6 +5713,8 @@ export interface MutationGenqlSelection{
|
||||
deleteManyNavigationMenuItems?: (NavigationMenuItemGenqlSelection & { __args: {ids: Scalars['UUID'][]} })
|
||||
deleteNavigationMenuItem?: (NavigationMenuItemGenqlSelection & { __args: {id: Scalars['UUID']} })
|
||||
uploadEmailAttachmentFile?: (FileWithSignedUrlGenqlSelection & { __args: {file: Scalars['Upload']} })
|
||||
refreshEnterpriseValidityToken?: boolean | number
|
||||
setEnterpriseKey?: (EnterpriseLicenseInfoDTOGenqlSelection & { __args: {enterpriseKey: Scalars['String']} })
|
||||
uploadAiChatFile?: (FileWithSignedUrlGenqlSelection & { __args: {file: Scalars['Upload']} })
|
||||
uploadWorkflowFile?: (FileWithSignedUrlGenqlSelection & { __args: {file: Scalars['Upload']} })
|
||||
uploadWorkspaceLogo?: (FileWithSignedUrlGenqlSelection & { __args: {file: Scalars['Upload']} })
|
||||
@@ -5794,8 +5755,6 @@ export interface MutationGenqlSelection{
|
||||
assignRoleToApiKey?: { __args: {apiKeyId: Scalars['UUID'], roleId: Scalars['UUID']} }
|
||||
createObjectEvent?: (AnalyticsGenqlSelection & { __args: {event: Scalars['String'], recordId: Scalars['UUID'], objectMetadataId: Scalars['UUID'], properties?: (Scalars['JSON'] | null)} })
|
||||
trackAnalytics?: (AnalyticsGenqlSelection & { __args: {type: AnalyticsType, name?: (Scalars['String'] | null), event?: (Scalars['String'] | null), properties?: (Scalars['JSON'] | null)} })
|
||||
refreshEnterpriseValidityToken?: boolean | number
|
||||
setEnterpriseKey?: (EnterpriseLicenseInfoDTOGenqlSelection & { __args: {enterpriseKey: Scalars['String']} })
|
||||
skipSyncEmailOnboardingStep?: OnboardingStepSuccessGenqlSelection
|
||||
skipBookOnboardingStep?: OnboardingStepSuccessGenqlSelection
|
||||
checkoutSession?: (BillingSessionGenqlSelection & { __args: {recurringInterval: SubscriptionInterval, plan: BillingPlanKey, requirePaymentMethod: Scalars['Boolean'], successUrlPath?: (Scalars['String'] | null)} })
|
||||
@@ -5847,7 +5806,7 @@ export interface MutationGenqlSelection{
|
||||
updateOneRole?: (RoleGenqlSelection & { __args: {updateRoleInput: UpdateRoleInput} })
|
||||
deleteOneRole?: { __args: {roleId: Scalars['UUID']} }
|
||||
upsertObjectPermissions?: (ObjectPermissionGenqlSelection & { __args: {upsertObjectPermissionsInput: UpsertObjectPermissionsInput} })
|
||||
upsertPermissionFlags?: (PermissionFlagGenqlSelection & { __args: {upsertPermissionFlagsInput: UpsertPermissionFlagsInput} })
|
||||
upsertPermissionFlags?: (RolePermissionFlagGenqlSelection & { __args: {upsertPermissionFlagsInput: UpsertPermissionFlagsInput} })
|
||||
upsertFieldPermissions?: (FieldPermissionGenqlSelection & { __args: {upsertFieldPermissionsInput: UpsertFieldPermissionsInput} })
|
||||
upsertRowLevelPermissionPredicates?: (UpsertRowLevelPermissionPredicatesResultGenqlSelection & { __args: {input: UpsertRowLevelPermissionPredicatesInput} })
|
||||
assignRoleToAgent?: { __args: {agentId: Scalars['UUID'], roleId: Scalars['UUID']} }
|
||||
@@ -5866,13 +5825,13 @@ export interface MutationGenqlSelection{
|
||||
updateMessageChannel?: (MessageChannelGenqlSelection & { __args: {input: UpdateMessageChannelInput} })
|
||||
createEmailGroupChannel?: (CreateEmailGroupChannelOutputGenqlSelection & { __args: {input: CreateEmailGroupChannelInput} })
|
||||
deleteEmailGroupChannel?: (MessageChannelGenqlSelection & { __args: {id: Scalars['UUID']} })
|
||||
deleteConnectedAccount?: (ConnectedAccountDTOGenqlSelection & { __args: {id: Scalars['UUID']} })
|
||||
deleteConnectedAccount?: (ConnectedAccountPublicDTOGenqlSelection & { __args: {id: Scalars['UUID']} })
|
||||
updateCalendarChannel?: (CalendarChannelGenqlSelection & { __args: {input: UpdateCalendarChannelInput} })
|
||||
createWebhook?: (WebhookGenqlSelection & { __args: {input: CreateWebhookInput} })
|
||||
updateWebhook?: (WebhookGenqlSelection & { __args: {input: UpdateWebhookInput} })
|
||||
deleteWebhook?: (WebhookGenqlSelection & { __args: {id: Scalars['UUID']} })
|
||||
createChatThread?: AgentChatThreadGenqlSelection
|
||||
sendChatMessage?: (SendChatMessageResultGenqlSelection & { __args: {threadId: Scalars['UUID'], text: Scalars['String'], messageId: Scalars['UUID'], browsingContext?: (Scalars['JSON'] | null), modelId?: (Scalars['String'] | null), fileIds?: (Scalars['UUID'][] | null)} })
|
||||
sendChatMessage?: (SendChatMessageResultGenqlSelection & { __args: {threadId: Scalars['UUID'], text: Scalars['String'], messageId: Scalars['UUID'], browsingContext?: (Scalars['JSON'] | null), modelId?: (Scalars['String'] | null), fileAttachments?: (FileAttachmentInput[] | null)} })
|
||||
stopAgentChatStream?: { __args: {threadId: Scalars['UUID']} }
|
||||
renameChatThread?: (AgentChatThreadGenqlSelection & { __args: {id: Scalars['UUID'], title: Scalars['String']} })
|
||||
archiveChatThread?: (AgentChatThreadGenqlSelection & { __args: {id: Scalars['UUID']} })
|
||||
@@ -5935,7 +5894,7 @@ export interface MutationGenqlSelection{
|
||||
impersonate?: (ImpersonateGenqlSelection & { __args: {userId: Scalars['UUID'], workspaceId: Scalars['UUID']} })
|
||||
sendEmail?: (SendEmailOutputGenqlSelection & { __args: {input: SendEmailInput} })
|
||||
startChannelSync?: (ChannelSyncSuccessGenqlSelection & { __args: {connectedAccountId: Scalars['UUID']} })
|
||||
saveImapSmtpCaldavAccount?: (ImapSmtpCaldavConnectionSuccessGenqlSelection & { __args: {accountOwnerId: Scalars['UUID'], handle: Scalars['String'], connectionParameters: EmailAccountConnectionParameters, id?: (Scalars['UUID'] | null)} })
|
||||
saveImapSmtpCaldavAccount?: (ImapSmtpCaldavConnectionSuccessGenqlSelection & { __args: {handle: Scalars['String'], connectionParameters: EmailAccountConnectionParameters, id?: (Scalars['UUID'] | null)} })
|
||||
updateLabPublicFeatureFlag?: (FeatureFlagGenqlSelection & { __args: {input: UpdateLabPublicFeatureFlagInput} })
|
||||
createPublicDomain?: (PublicDomainGenqlSelection & { __args: {domain: Scalars['String'], applicationId?: (Scalars['String'] | null)} })
|
||||
updatePublicDomain?: (PublicDomainGenqlSelection & { __args: {domain: Scalars['String'], applicationId?: (Scalars['String'] | null)} })
|
||||
@@ -6259,6 +6218,8 @@ update: UpdateWebhookInputUpdates}
|
||||
|
||||
export interface UpdateWebhookInputUpdates {targetUrl?: (Scalars['String'] | null),operations?: (Scalars['String'][] | null),description?: (Scalars['String'] | null),secret?: (Scalars['String'] | null)}
|
||||
|
||||
export interface FileAttachmentInput {id: Scalars['UUID'],filename: Scalars['String']}
|
||||
|
||||
export interface CreateSkillInput {id?: (Scalars['UUID'] | null),name: Scalars['String'],label: Scalars['String'],icon?: (Scalars['String'] | null),description?: (Scalars['String'] | null),content: Scalars['String']}
|
||||
|
||||
export interface UpdateSkillInput {id: Scalars['UUID'],name?: (Scalars['String'] | null),label?: (Scalars['String'] | null),icon?: (Scalars['String'] | null),description?: (Scalars['String'] | null),content?: (Scalars['String'] | null),isActive?: (Scalars['Boolean'] | null)}
|
||||
@@ -6299,9 +6260,9 @@ export interface SendEmailInput {connectedAccountId: Scalars['String'],to: Scala
|
||||
|
||||
export interface SendEmailAttachmentInput {id: Scalars['String'],name: Scalars['String']}
|
||||
|
||||
export interface EmailAccountConnectionParameters {IMAP?: (ConnectionParameters | null),SMTP?: (ConnectionParameters | null),CALDAV?: (ConnectionParameters | null)}
|
||||
export interface EmailAccountConnectionParameters {IMAP?: (ConnectionParametersInput | null),SMTP?: (ConnectionParametersInput | null),CALDAV?: (ConnectionParametersInput | null)}
|
||||
|
||||
export interface ConnectionParameters {host: Scalars['String'],port: Scalars['Float'],username?: (Scalars['String'] | null),password: Scalars['String'],secure?: (Scalars['Boolean'] | null)}
|
||||
export interface ConnectionParametersInput {host: Scalars['String'],port: Scalars['Float'],username?: (Scalars['String'] | null),password?: (Scalars['String'] | null),secure?: (Scalars['Boolean'] | null)}
|
||||
|
||||
export interface UpdateLabPublicFeatureFlagInput {publicFeatureFlag: Scalars['String'],value: Scalars['Boolean']}
|
||||
|
||||
@@ -6426,10 +6387,10 @@ export interface LogicFunctionLogsInput {applicationId?: (Scalars['UUID'] | null
|
||||
|
||||
|
||||
|
||||
const PermissionFlag_possibleTypes: string[] = ['PermissionFlag']
|
||||
export const isPermissionFlag = (obj?: { __typename?: any } | null): obj is PermissionFlag => {
|
||||
if (!obj?.__typename) throw new Error('__typename is missing in "isPermissionFlag"')
|
||||
return PermissionFlag_possibleTypes.includes(obj.__typename)
|
||||
const RolePermissionFlag_possibleTypes: string[] = ['RolePermissionFlag']
|
||||
export const isRolePermissionFlag = (obj?: { __typename?: any } | null): obj is RolePermissionFlag => {
|
||||
if (!obj?.__typename) throw new Error('__typename is missing in "isRolePermissionFlag"')
|
||||
return RolePermissionFlag_possibleTypes.includes(obj.__typename)
|
||||
}
|
||||
|
||||
|
||||
@@ -6954,6 +6915,22 @@ export interface LogicFunctionLogsInput {applicationId?: (Scalars['UUID'] | null
|
||||
|
||||
|
||||
|
||||
const EnterpriseLicenseInfoDTO_possibleTypes: string[] = ['EnterpriseLicenseInfoDTO']
|
||||
export const isEnterpriseLicenseInfoDTO = (obj?: { __typename?: any } | null): obj is EnterpriseLicenseInfoDTO => {
|
||||
if (!obj?.__typename) throw new Error('__typename is missing in "isEnterpriseLicenseInfoDTO"')
|
||||
return EnterpriseLicenseInfoDTO_possibleTypes.includes(obj.__typename)
|
||||
}
|
||||
|
||||
|
||||
|
||||
const EnterpriseSubscriptionStatusDTO_possibleTypes: string[] = ['EnterpriseSubscriptionStatusDTO']
|
||||
export const isEnterpriseSubscriptionStatusDTO = (obj?: { __typename?: any } | null): obj is EnterpriseSubscriptionStatusDTO => {
|
||||
if (!obj?.__typename) throw new Error('__typename is missing in "isEnterpriseSubscriptionStatusDTO"')
|
||||
return EnterpriseSubscriptionStatusDTO_possibleTypes.includes(obj.__typename)
|
||||
}
|
||||
|
||||
|
||||
|
||||
const Analytics_possibleTypes: string[] = ['Analytics']
|
||||
export const isAnalytics = (obj?: { __typename?: any } | null): obj is Analytics => {
|
||||
if (!obj?.__typename) throw new Error('__typename is missing in "isAnalytics"')
|
||||
@@ -6978,22 +6955,6 @@ export interface LogicFunctionLogsInput {applicationId?: (Scalars['UUID'] | null
|
||||
|
||||
|
||||
|
||||
const EnterpriseLicenseInfoDTO_possibleTypes: string[] = ['EnterpriseLicenseInfoDTO']
|
||||
export const isEnterpriseLicenseInfoDTO = (obj?: { __typename?: any } | null): obj is EnterpriseLicenseInfoDTO => {
|
||||
if (!obj?.__typename) throw new Error('__typename is missing in "isEnterpriseLicenseInfoDTO"')
|
||||
return EnterpriseLicenseInfoDTO_possibleTypes.includes(obj.__typename)
|
||||
}
|
||||
|
||||
|
||||
|
||||
const EnterpriseSubscriptionStatusDTO_possibleTypes: string[] = ['EnterpriseSubscriptionStatusDTO']
|
||||
export const isEnterpriseSubscriptionStatusDTO = (obj?: { __typename?: any } | null): obj is EnterpriseSubscriptionStatusDTO => {
|
||||
if (!obj?.__typename) throw new Error('__typename is missing in "isEnterpriseSubscriptionStatusDTO"')
|
||||
return EnterpriseSubscriptionStatusDTO_possibleTypes.includes(obj.__typename)
|
||||
}
|
||||
|
||||
|
||||
|
||||
const BillingSubscriptionSchedulePhaseItem_possibleTypes: string[] = ['BillingSubscriptionSchedulePhaseItem']
|
||||
export const isBillingSubscriptionSchedulePhaseItem = (obj?: { __typename?: any } | null): obj is BillingSubscriptionSchedulePhaseItem => {
|
||||
if (!obj?.__typename) throw new Error('__typename is missing in "isBillingSubscriptionSchedulePhaseItem"')
|
||||
@@ -7922,18 +7883,42 @@ export interface LogicFunctionLogsInput {applicationId?: (Scalars['UUID'] | null
|
||||
|
||||
|
||||
|
||||
const ConnectionParametersOutput_possibleTypes: string[] = ['ConnectionParametersOutput']
|
||||
export const isConnectionParametersOutput = (obj?: { __typename?: any } | null): obj is ConnectionParametersOutput => {
|
||||
if (!obj?.__typename) throw new Error('__typename is missing in "isConnectionParametersOutput"')
|
||||
return ConnectionParametersOutput_possibleTypes.includes(obj.__typename)
|
||||
const PublicConnectionParametersOutput_possibleTypes: string[] = ['PublicConnectionParametersOutput']
|
||||
export const isPublicConnectionParametersOutput = (obj?: { __typename?: any } | null): obj is PublicConnectionParametersOutput => {
|
||||
if (!obj?.__typename) throw new Error('__typename is missing in "isPublicConnectionParametersOutput"')
|
||||
return PublicConnectionParametersOutput_possibleTypes.includes(obj.__typename)
|
||||
}
|
||||
|
||||
|
||||
|
||||
const ImapSmtpCaldavConnectionParameters_possibleTypes: string[] = ['ImapSmtpCaldavConnectionParameters']
|
||||
export const isImapSmtpCaldavConnectionParameters = (obj?: { __typename?: any } | null): obj is ImapSmtpCaldavConnectionParameters => {
|
||||
if (!obj?.__typename) throw new Error('__typename is missing in "isImapSmtpCaldavConnectionParameters"')
|
||||
return ImapSmtpCaldavConnectionParameters_possibleTypes.includes(obj.__typename)
|
||||
const PublicImapSmtpCaldavConnectionParameters_possibleTypes: string[] = ['PublicImapSmtpCaldavConnectionParameters']
|
||||
export const isPublicImapSmtpCaldavConnectionParameters = (obj?: { __typename?: any } | null): obj is PublicImapSmtpCaldavConnectionParameters => {
|
||||
if (!obj?.__typename) throw new Error('__typename is missing in "isPublicImapSmtpCaldavConnectionParameters"')
|
||||
return PublicImapSmtpCaldavConnectionParameters_possibleTypes.includes(obj.__typename)
|
||||
}
|
||||
|
||||
|
||||
|
||||
const ConnectedAccountPublicDTO_possibleTypes: string[] = ['ConnectedAccountPublicDTO']
|
||||
export const isConnectedAccountPublicDTO = (obj?: { __typename?: any } | null): obj is ConnectedAccountPublicDTO => {
|
||||
if (!obj?.__typename) throw new Error('__typename is missing in "isConnectedAccountPublicDTO"')
|
||||
return ConnectedAccountPublicDTO_possibleTypes.includes(obj.__typename)
|
||||
}
|
||||
|
||||
|
||||
|
||||
const ImapSmtpCaldavPublicConnectionParams_possibleTypes: string[] = ['ImapSmtpCaldavPublicConnectionParams']
|
||||
export const isImapSmtpCaldavPublicConnectionParams = (obj?: { __typename?: any } | null): obj is ImapSmtpCaldavPublicConnectionParams => {
|
||||
if (!obj?.__typename) throw new Error('__typename is missing in "isImapSmtpCaldavPublicConnectionParams"')
|
||||
return ImapSmtpCaldavPublicConnectionParams_possibleTypes.includes(obj.__typename)
|
||||
}
|
||||
|
||||
|
||||
|
||||
const ImapSmtpCaldavPublicConnectionParameters_possibleTypes: string[] = ['ImapSmtpCaldavPublicConnectionParameters']
|
||||
export const isImapSmtpCaldavPublicConnectionParameters = (obj?: { __typename?: any } | null): obj is ImapSmtpCaldavPublicConnectionParameters => {
|
||||
if (!obj?.__typename) throw new Error('__typename is missing in "isImapSmtpCaldavPublicConnectionParameters"')
|
||||
return ImapSmtpCaldavPublicConnectionParameters_possibleTypes.includes(obj.__typename)
|
||||
}
|
||||
|
||||
|
||||
@@ -8042,38 +8027,6 @@ export interface LogicFunctionLogsInput {applicationId?: (Scalars['UUID'] | null
|
||||
|
||||
|
||||
|
||||
const ConnectedAccountDTO_possibleTypes: string[] = ['ConnectedAccountDTO']
|
||||
export const isConnectedAccountDTO = (obj?: { __typename?: any } | null): obj is ConnectedAccountDTO => {
|
||||
if (!obj?.__typename) throw new Error('__typename is missing in "isConnectedAccountDTO"')
|
||||
return ConnectedAccountDTO_possibleTypes.includes(obj.__typename)
|
||||
}
|
||||
|
||||
|
||||
|
||||
const PublicConnectionParametersOutput_possibleTypes: string[] = ['PublicConnectionParametersOutput']
|
||||
export const isPublicConnectionParametersOutput = (obj?: { __typename?: any } | null): obj is PublicConnectionParametersOutput => {
|
||||
if (!obj?.__typename) throw new Error('__typename is missing in "isPublicConnectionParametersOutput"')
|
||||
return PublicConnectionParametersOutput_possibleTypes.includes(obj.__typename)
|
||||
}
|
||||
|
||||
|
||||
|
||||
const PublicImapSmtpCaldavConnectionParameters_possibleTypes: string[] = ['PublicImapSmtpCaldavConnectionParameters']
|
||||
export const isPublicImapSmtpCaldavConnectionParameters = (obj?: { __typename?: any } | null): obj is PublicImapSmtpCaldavConnectionParameters => {
|
||||
if (!obj?.__typename) throw new Error('__typename is missing in "isPublicImapSmtpCaldavConnectionParameters"')
|
||||
return PublicImapSmtpCaldavConnectionParameters_possibleTypes.includes(obj.__typename)
|
||||
}
|
||||
|
||||
|
||||
|
||||
const ConnectedAccountPublicDTO_possibleTypes: string[] = ['ConnectedAccountPublicDTO']
|
||||
export const isConnectedAccountPublicDTO = (obj?: { __typename?: any } | null): obj is ConnectedAccountPublicDTO => {
|
||||
if (!obj?.__typename) throw new Error('__typename is missing in "isConnectedAccountPublicDTO"')
|
||||
return ConnectedAccountPublicDTO_possibleTypes.includes(obj.__typename)
|
||||
}
|
||||
|
||||
|
||||
|
||||
const SendEmailOutput_possibleTypes: string[] = ['SendEmailOutput']
|
||||
export const isSendEmailOutput = (obj?: { __typename?: any } | null): obj is SendEmailOutput => {
|
||||
if (!obj?.__typename) throw new Error('__typename is missing in "isSendEmailOutput"')
|
||||
@@ -8899,6 +8852,7 @@ export const enumAllMetadataName = {
|
||||
pageLayoutTab: 'pageLayoutTab' as const,
|
||||
commandMenuItem: 'commandMenuItem' as const,
|
||||
navigationMenuItem: 'navigationMenuItem' as const,
|
||||
rolePermissionFlag: 'rolePermissionFlag' as const,
|
||||
permissionFlag: 'permissionFlag' as const,
|
||||
objectPermission: 'objectPermission' as const,
|
||||
fieldPermission: 'fieldPermission' as const,
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -9,7 +9,9 @@ TAG=latest
|
||||
SERVER_URL=http://localhost:3000
|
||||
|
||||
# Use openssl rand -base64 32 for each secret
|
||||
# APP_SECRET=replace_me_with_a_random_string
|
||||
# ENCRYPTION_KEY=replace_me_with_a_random_string
|
||||
# FALLBACK_ENCRYPTION_KEY= # set to the previous ENCRYPTION_KEY during a rotation
|
||||
# APP_SECRET= # legacy: only required for instances that pre-date ENCRYPTION_KEY
|
||||
|
||||
STORAGE_TYPE=local
|
||||
|
||||
|
||||
@@ -20,7 +20,9 @@ services:
|
||||
STORAGE_S3_NAME: ${STORAGE_S3_NAME}
|
||||
STORAGE_S3_ENDPOINT: ${STORAGE_S3_ENDPOINT}
|
||||
|
||||
APP_SECRET: ${APP_SECRET:-replace_me_with_a_random_string}
|
||||
ENCRYPTION_KEY: ${ENCRYPTION_KEY}
|
||||
FALLBACK_ENCRYPTION_KEY: ${FALLBACK_ENCRYPTION_KEY}
|
||||
APP_SECRET: ${APP_SECRET:-}
|
||||
# MESSAGING_PROVIDER_GMAIL_ENABLED: ${MESSAGING_PROVIDER_GMAIL_ENABLED}
|
||||
# CALENDAR_PROVIDER_GOOGLE_ENABLED: ${CALENDAR_PROVIDER_GOOGLE_ENABLED}
|
||||
# AUTH_GOOGLE_CLIENT_ID: ${AUTH_GOOGLE_CLIENT_ID}
|
||||
@@ -73,7 +75,9 @@ services:
|
||||
STORAGE_S3_NAME: ${STORAGE_S3_NAME}
|
||||
STORAGE_S3_ENDPOINT: ${STORAGE_S3_ENDPOINT}
|
||||
|
||||
APP_SECRET: ${APP_SECRET:-replace_me_with_a_random_string}
|
||||
ENCRYPTION_KEY: ${ENCRYPTION_KEY}
|
||||
FALLBACK_ENCRYPTION_KEY: ${FALLBACK_ENCRYPTION_KEY}
|
||||
APP_SECRET: ${APP_SECRET:-}
|
||||
# MESSAGING_PROVIDER_GMAIL_ENABLED: ${MESSAGING_PROVIDER_GMAIL_ENABLED}
|
||||
# CALENDAR_PROVIDER_GOOGLE_ENABLED: ${CALENDAR_PROVIDER_GOOGLE_ENABLED}
|
||||
# AUTH_GOOGLE_CLIENT_ID: ${AUTH_GOOGLE_CLIENT_ID}
|
||||
|
||||
@@ -91,7 +91,7 @@ fi
|
||||
|
||||
# Generate random strings for secrets
|
||||
echo "# === Randomly generated secret ===" >> .env
|
||||
echo "APP_SECRET=$(openssl rand -base64 32)" >> .env
|
||||
echo "ENCRYPTION_KEY=$(openssl rand -base64 32)" >> .env
|
||||
|
||||
echo "" >> .env
|
||||
echo "PG_DATABASE_PASSWORD=$(openssl rand -hex 32)" >> .env
|
||||
|
||||
@@ -22,7 +22,8 @@ Use the `getPublicAssetUrl` helper from `twenty-sdk` to get the full URL of a fi
|
||||
**In a logic function:**
|
||||
|
||||
```ts src/logic-functions/send-invoice.ts
|
||||
import { defineLogicFunction, getPublicAssetUrl } from 'twenty-sdk/define';
|
||||
import { defineLogicFunction } from 'twenty-sdk/define';
|
||||
import { getPublicAssetUrl } from 'twenty-sdk/utils';
|
||||
|
||||
const handler = async (): Promise<any> => {
|
||||
const logoUrl = getPublicAssetUrl('logo.png');
|
||||
@@ -47,12 +48,19 @@ export default defineLogicFunction({
|
||||
**In a front component:**
|
||||
|
||||
```tsx src/front-components/company-card.tsx
|
||||
import { defineFrontComponent, getPublicAssetUrl } from 'twenty-sdk/define';
|
||||
import { defineFrontComponent } from 'twenty-sdk/define';
|
||||
import { getPublicAssetUrl } from 'twenty-sdk/utils';
|
||||
|
||||
export default defineFrontComponent(() => {
|
||||
const CompanyCard = () => {
|
||||
const logoUrl = getPublicAssetUrl('logo.png');
|
||||
|
||||
return <img src={logoUrl} alt="App logo" />;
|
||||
};
|
||||
|
||||
export default defineFrontComponent({
|
||||
universalIdentifier: '...',
|
||||
name: 'company-card',
|
||||
component: CompanyCard,
|
||||
});
|
||||
```
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
---
|
||||
title: Roles & Permissions
|
||||
description: Declare what objects and fields your app's logic functions and front components can read and write.
|
||||
icon: "shield-halved"
|
||||
icon: 'shield-halved'
|
||||
---
|
||||
|
||||
A **role** is a permission set: which objects an app can read or write, which fields it can see, and which platform-level capabilities it can use. Every app's logic functions and front components inherit the permissions of the role marked with `defineApplicationRole()` (see [The default function role](#the-default-function-role) below).
|
||||
@@ -40,7 +40,8 @@ export default defineRole({
|
||||
objectUniversalIdentifier:
|
||||
STANDARD_OBJECT_UNIVERSAL_IDENTIFIERS.company.universalIdentifier,
|
||||
fieldUniversalIdentifier:
|
||||
STANDARD_OBJECT_UNIVERSAL_IDENTIFIERS.company.fields.name.universalIdentifier,
|
||||
STANDARD_OBJECT_UNIVERSAL_IDENTIFIERS.company.fields.name
|
||||
.universalIdentifier,
|
||||
canReadFieldValue: false,
|
||||
canUpdateFieldValue: false,
|
||||
},
|
||||
@@ -80,6 +81,7 @@ export default defineApplicationRole({
|
||||
`defineApplicationRole()` is a thin wrapper around `defineRole()` that flags **the** role used as your application's default at install time. Validation is identical to `defineRole`, but the build pipeline auto-wires its `universalIdentifier` into the application manifest's `defaultRoleUniversalIdentifier` — so you do not need to reference it from [`defineApplication`](/developers/extend/apps/config/application) yourself.
|
||||
|
||||
Notes:
|
||||
|
||||
- Exactly **one** `defineApplicationRole(...)` is allowed per app — the manifest build will fail if it finds more than one.
|
||||
- Use `defineRole()` (not `defineApplicationRole()`) for any **additional** roles your app ships.
|
||||
- Setting `defaultRoleUniversalIdentifier` explicitly on `defineApplication()` is still supported for backward compatibility, but is deprecated in favor of `defineApplicationRole()`.
|
||||
|
||||
@@ -138,20 +138,6 @@ Both modes need an authenticated remote.
|
||||
| `--debounceMs <ms>` | Set the file-change debounce delay in milliseconds (default: `2000`). |
|
||||
| `--verbose` / `--debug` | Show detailed build logs, sync requests, and error traces. |
|
||||
|
||||
---
|
||||
|
||||
## Starting from an example
|
||||
|
||||
Use `--example` to start with a more complete project (custom objects, fields, logic functions, front components):
|
||||
|
||||
```bash filename="Terminal"
|
||||
npx create-twenty-app@latest my-twenty-app --example postcard
|
||||
```
|
||||
|
||||
Examples live in [twenty-apps/examples](https://github.com/twentyhq/twenty/tree/main/packages/twenty-apps/examples). You can also scaffold individual entities into an existing project with `yarn twenty add` — see [Scaffolding](/developers/extend/apps/getting-started/scaffolding).
|
||||
|
||||
---
|
||||
|
||||
## What you can build
|
||||
|
||||
Apps are composed of **entities** — each defined as a TypeScript file with a single `export default`:
|
||||
|
||||
@@ -390,7 +390,8 @@ export default defineFrontComponent({
|
||||
Front components can access files from the app's `public/` directory using `getPublicAssetUrl`:
|
||||
|
||||
```tsx
|
||||
import { defineFrontComponent, getPublicAssetUrl } from 'twenty-sdk/define';
|
||||
import { defineFrontComponent } from 'twenty-sdk/define';
|
||||
import { getPublicAssetUrl } from 'twenty-sdk/utils';
|
||||
|
||||
const Logo = () => <img src={getPublicAssetUrl('logo.png')} alt="Logo" />;
|
||||
|
||||
|
||||
@@ -47,22 +47,25 @@ Follow these steps for a manual setup.
|
||||
curl -o .env https://raw.githubusercontent.com/twentyhq/twenty/refs/heads/main/packages/twenty-docker/.env.example
|
||||
```
|
||||
|
||||
2. **Generate Secret Tokens**
|
||||
2. **Generate an Encryption Key**
|
||||
|
||||
Run the following command to generate a unique random string:
|
||||
|
||||
```bash
|
||||
openssl rand -base64 32
|
||||
```
|
||||
**Important:** Keep this value secret / do not share it.
|
||||
**Important:** Keep this value secret / do not share it. Losing `ENCRYPTION_KEY` means losing access to every secret stored in the database (OAuth tokens, application variables, TOTP secrets, etc.).
|
||||
|
||||
3. **Update the `.env`**
|
||||
|
||||
Replace the placeholder value in your .env file with the generated token:
|
||||
|
||||
```ini
|
||||
APP_SECRET=first_random_string
|
||||
ENCRYPTION_KEY=random_string
|
||||
```
|
||||
|
||||
See the [Key rotation guide](/developers/self-host/capabilities/key-rotation) for instructions on rotating it without downtime.
|
||||
|
||||
4. **Set the Postgres Password**
|
||||
|
||||
Update the `PG_DATABASE_PASSWORD` value in the .env file with a strong password without special characters.
|
||||
|
||||
@@ -0,0 +1,57 @@
|
||||
---
|
||||
title: Key rotation
|
||||
icon: "rotate"
|
||||
---
|
||||
|
||||
Twenty has two independent key families:
|
||||
|
||||
- **JWT signing keys** — asymmetric ES256 keypairs (`kid`-tagged) stored in `core."signingKey"`, used to sign and verify access / refresh tokens.
|
||||
- **At-rest encryption key** — `ENCRYPTION_KEY`, used to encrypt OAuth tokens, application variables, signing-key private keys, sensitive config values and TOTP secrets inside an `enc:v2:` envelope.
|
||||
|
||||
`APP_SECRET` is a legacy secret kept for backward compatibility: when `ENCRYPTION_KEY` is unset it acts as the at-rest encryption / session cookie fallback, and it still verifies pre-existing HS256 access tokens. It will be deprecated.
|
||||
|
||||
## JWT signing keys
|
||||
|
||||
Each key carries a `publicKey` (kept indefinitely so it can verify previously issued tokens), an encrypted `privateKey` (used only while the key is current), an `isCurrent` flag (exactly one row at a time) and an optional `revokedAt`.
|
||||
|
||||
### Rotate the current key
|
||||
|
||||
- **Manual** — **Settings → Admin Panel → Signing keys → Revoke** on the current row. Revoking wipes its encrypted private material and demotes it; the next sign call automatically mints a fresh ES256 keypair as the new current. Tokens signed under any other (non-revoked) `kid` keep verifying until they expire.
|
||||
- **Enterprise (automatic)** — a daily cron (`'15 3 * * *'` UTC) issues a new current key once the existing one has been current for `SIGNING_KEY_ROTATION_DAYS` (default `90`). The previous key is *not* revoked, so tokens signed under it keep verifying. Register it once with `yarn command:prod cron:register:all`.
|
||||
|
||||
<Note>The Enterprise cron and `SIGNING_KEY_ROTATION_DAYS` ship in v2.6+.</Note>
|
||||
|
||||
### Revoke a key (leak / emergency only)
|
||||
|
||||
**Settings → Admin Panel → Signing keys → Revoke** on a non-current row. Wipes the encrypted private material, sets `revokedAt`, and rejects every existing token signed under that `kid`.
|
||||
|
||||
## Rotate `ENCRYPTION_KEY`
|
||||
|
||||
<Note>The `secret-encryption:rotate` command described below ships in v2.6+.</Note>
|
||||
|
||||
Every encrypted value is wrapped as `enc:v2:<keyId>:<payload>`, where `<keyId>` is an 8-hex prefix derived from the raw key. Rotation is online and resumable.
|
||||
|
||||
1. **Generate a new key**: `openssl rand -base64 32`.
|
||||
2. **Configure both keys side-by-side** in `.env`, then restart:
|
||||
```ini
|
||||
ENCRYPTION_KEY=NEW_VALUE
|
||||
FALLBACK_ENCRYPTION_KEY=OLD_VALUE
|
||||
```
|
||||
New writes use the new key, existing rows still decrypt via the fallback.
|
||||
3. **Re-encrypt existing rows**:
|
||||
```bash
|
||||
docker exec -it {server_container} yarn command:prod secret-encryption:rotate
|
||||
```
|
||||
The command walks six sites (`connected-account-tokens`, `application-variable`, `application-registration-variable`, `signing-key-private-keys`, `sensitive-config-storage`, `totp-secrets`). A SQL filter skips rows already on the new `<keyId>`, so the command is idempotent: interrupt and re-run as needed. Exits non-zero if any row fails — re-run to retry.
|
||||
|
||||
| Flag | Description |
|
||||
| --- | --- |
|
||||
| `-s, --site <site>` | Limit to a single site. |
|
||||
| `-b, --batch-size <n>` | Rows per batch (default `200`, max `5000`). |
|
||||
| `-d, --dry-run` | Decrypt + re-encrypt in memory, skip the `UPDATE`. |
|
||||
|
||||
4. **Drop the fallback** once `--dry-run` shows zero remaining rows: remove `FALLBACK_ENCRYPTION_KEY` and restart.
|
||||
|
||||
## Legacy `APP_SECRET` support
|
||||
|
||||
Older instances that never set `ENCRYPTION_KEY` use `APP_SECRET` as the at-rest encryption key (and as the session-cookie secret, derived from it). This path is preserved for backward compatibility but is **deprecated** — set a dedicated `ENCRYPTION_KEY` and follow the rotation procedure above to migrate off it. `APP_SECRET` itself stays in use to verify legacy HS256 access tokens.
|
||||
@@ -42,11 +42,26 @@ IS_CONFIG_VARIABLES_IN_DB_ENABLED=true # default
|
||||
|
||||
<Warning>
|
||||
Each variable is documented with descriptions in your admin panel at **Settings → Admin Panel → Configuration Variables**.
|
||||
Some infrastructure settings like database connections (`PG_DATABASE_URL`), server URLs (`SERVER_URL`), and app secrets (`APP_SECRET`) can only be configured via `.env` file.
|
||||
Some infrastructure settings like database connections (`PG_DATABASE_URL`), server URLs (`SERVER_URL`), and secrets (`ENCRYPTION_KEY`, `FALLBACK_ENCRYPTION_KEY`) can only be configured via `.env` file.
|
||||
|
||||
[Complete technical reference →](https://github.com/twentyhq/twenty/blob/main/packages/twenty-server/src/engine/core-modules/twenty-config/config-variables.ts)
|
||||
</Warning>
|
||||
|
||||
## Encryption keys
|
||||
|
||||
Twenty uses two env-only encryption keys:
|
||||
|
||||
| Variable | Purpose | Required |
|
||||
| --- | --- | --- |
|
||||
| `ENCRYPTION_KEY` | Primary key used to encrypt secrets at rest (OAuth tokens, application variables, signing-key private keys, TOTP secrets, sensitive config values). | Yes for new installs (legacy installs may instead rely on `APP_SECRET` — see below) |
|
||||
| `FALLBACK_ENCRYPTION_KEY` | Verification-only key. Set during a rotation to the *previous* `ENCRYPTION_KEY` so existing rows remain decryptable. | Only during rotation |
|
||||
|
||||
For backward compatibility, if `ENCRYPTION_KEY` is unset, Twenty falls back to `APP_SECRET` for at-rest encryption — matching the legacy behaviour for older deployments. New installs should always set a dedicated `ENCRYPTION_KEY`.
|
||||
|
||||
Generate values with `openssl rand -base64 32` and store them somewhere safe (a secrets manager, sealed config, etc.). Losing `ENCRYPTION_KEY` means losing access to every secret stored in the database.
|
||||
|
||||
To rotate `ENCRYPTION_KEY` without downtime, see the [Key rotation guide](/developers/self-host/capabilities/key-rotation).
|
||||
|
||||
## 2. Environment-Only Configuration
|
||||
|
||||
```bash
|
||||
|
||||
@@ -31,6 +31,18 @@ Starting from **v1.22**, Twenty supports cross-version upgrades. You can jump di
|
||||
|
||||
For example, upgrading from v1.22 straight to v2.0 is fully supported.
|
||||
|
||||
## Upgrading to v2.5+ — at-rest encryption envelope
|
||||
|
||||
Starting in **v2.5**, Twenty stores at-rest secrets (OAuth tokens, application variables, signing-key private keys, sensitive config values, TOTP secrets) inside a versioned `enc:v2:` envelope encrypted with `ENCRYPTION_KEY` (or `APP_SECRET` if `ENCRYPTION_KEY` is unset).
|
||||
|
||||
The first boot on v2.5 runs slow upgrade commands that **backfill** existing rows into the new envelope. They are idempotent — interrupting and restarting the server resumes from where it left off — but they can take a while on large databases. You can monitor progress with `upgrade:status`.
|
||||
|
||||
You should set a dedicated `ENCRYPTION_KEY` **before** the v2.5 upgrade so the backfill writes rows under it from the start. Switching keys after the backfill requires a [rotation](/developers/self-host/capabilities/key-rotation).
|
||||
|
||||
## Rotating secrets and signing keys
|
||||
|
||||
For day-to-day operational tasks like rotating `ENCRYPTION_KEY`, rotating the JWT signing key, or revoking a leaked signing key, see the dedicated [Key rotation guide](/developers/self-host/capabilities/key-rotation).
|
||||
|
||||
## Checking upgrade status
|
||||
|
||||
The `upgrade:status` command lets you inspect the current state of your instance and workspace migrations. It is useful for debugging upgrade issues or when filing a support request.
|
||||
|
||||
@@ -40,7 +40,8 @@ export default defineRole({
|
||||
objectUniversalIdentifier:
|
||||
STANDARD_OBJECT_UNIVERSAL_IDENTIFIERS.company.universalIdentifier,
|
||||
fieldUniversalIdentifier:
|
||||
STANDARD_OBJECT_UNIVERSAL_IDENTIFIERS.company.fields.name.universalIdentifier,
|
||||
STANDARD_OBJECT_UNIVERSAL_IDENTIFIERS.company.fields.name
|
||||
.universalIdentifier,
|
||||
canReadFieldValue: false,
|
||||
canUpdateFieldValue: false,
|
||||
},
|
||||
|
||||
@@ -51,7 +51,7 @@ Befolgen Sie diese Schritte für eine manuelle Einrichtung.
|
||||
curl -o .env https://raw.githubusercontent.com/twentyhq/twenty/refs/heads/main/packages/twenty-docker/.env.example
|
||||
```
|
||||
|
||||
2. **Erstellen Sie geheime Tokens**
|
||||
2. **Einen Verschlüsselungsschlüssel generieren**
|
||||
|
||||
Führen Sie den folgenden Befehl aus, um eine eindeutige Zufallszeichenfolge zu generieren:
|
||||
|
||||
@@ -59,16 +59,18 @@ Befolgen Sie diese Schritte für eine manuelle Einrichtung.
|
||||
openssl rand -base64 32
|
||||
```
|
||||
|
||||
**Wichtig:** Bewahren Sie diesen Wert geheim auf / teilen Sie ihn nicht.
|
||||
**Wichtig:** Bewahren Sie diesen Wert geheim auf / teilen Sie ihn nicht. Der Verlust von `ENCRYPTION_KEY` bedeutet den Verlust des Zugriffs auf alle in der Datenbank gespeicherten Secrets (OAuth-Tokens, Anwendungsvariablen, TOTP-Secrets usw.).
|
||||
|
||||
3. **Aktualisieren Sie die `.env`**
|
||||
|
||||
Ersetzen Sie den Platzhalterwert in Ihrer .env-Datei durch das generierte Token:
|
||||
|
||||
```ini
|
||||
APP_SECRET=erster_zufälliger_string
|
||||
ENCRYPTION_KEY=random_string
|
||||
```
|
||||
|
||||
Weitere Informationen findest du im [Leitfaden zur Schlüsselrotation](/l/de/developers/self-host/capabilities/key-rotation) mit Anweisungen zur Rotation ohne Ausfallzeit.
|
||||
|
||||
4. **Setzen Sie das Postgres-Passwort**
|
||||
|
||||
Aktualisieren Sie den Wert `PG_DATABASE_PASSWORD` in der .env-Datei mit einem starken Passwort ohne Sonderzeichen.
|
||||
|
||||
@@ -0,0 +1,61 @@
|
||||
---
|
||||
title: Schlüsselrotation
|
||||
icon: rotate
|
||||
---
|
||||
|
||||
Twenty hat zwei unabhängige Schlüsselfamilien:
|
||||
|
||||
* **JWT-Signaturschlüssel** — asymmetrische ES256-Schlüsselpaare (`kid`-markiert), die in `core."signingKey"` gespeichert sind und zum Signieren und Verifizieren von Access-/Refresh-Tokens verwendet werden.
|
||||
* **Verschlüsselungsschlüssel im Ruhezustand** — `ENCRYPTION_KEY`, wird verwendet, um OAuth-Tokens, Anwendungsvariablen, die privaten Schlüssel der Signierschlüssel, sensible Konfigurationswerte und TOTP-Geheimnisse innerhalb eines `enc:v2:`-Umschlags zu verschlüsseln.
|
||||
|
||||
`APP_SECRET` ist ein veraltetes Secret, das aus Gründen der Abwärtskompatibilität beibehalten wird: Wenn `ENCRYPTION_KEY` nicht gesetzt ist, fungiert es als Fallback für At-Rest-Verschlüsselung / Session-Cookie und verifiziert weiterhin bereits bestehende HS256-Access-Tokens. Es wird als veraltet markiert werden.
|
||||
|
||||
## JWT-Signierschlüssel
|
||||
|
||||
Jeder Schlüssel enthält einen `publicKey` (unbefristet aufbewahrt, damit er zuvor ausgestellte Tokens verifizieren kann), einen verschlüsselten `privateKey` (nur verwendet, solange der Schlüssel aktuell ist), ein `isCurrent`-Flag (zu jedem Zeitpunkt genau eine Zeile) und ein optionales `revokedAt`.
|
||||
|
||||
### 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`.
|
||||
|
||||
<Note>Der Enterprise-Cron-Job und `SIGNING_KEY_ROTATION_DAYS` sind ab v2.6+ enthalten.</Note>
|
||||
|
||||
### Einen Schlüssel widerrufen (nur bei Leak / Notfall)
|
||||
|
||||
**Settings → Admin Panel → Signing keys → Revoke** in einer nicht aktuellen Zeile. Löscht das verschlüsselte private Material, setzt `revokedAt` und weist jedes vorhandene Token zurück, das unter diesem `kid` signiert wurde.
|
||||
|
||||
## `ENCRYPTION_KEY` rotieren
|
||||
|
||||
<Note>Der unten beschriebene Befehl `secret-encryption:rotate` ist ab v2.6+ enthalten.</Note>
|
||||
|
||||
Jeder verschlüsselte Wert wird als `enc:v2:\<keyId>:\<payload>` verpackt, wobei `\<keyId>` ein 8-stelliges Hex-Präfix ist, das aus dem Rohschlüssel abgeleitet wird. Die Rotation erfolgt online und ist fortsetzbar.
|
||||
|
||||
1. **Einen neuen Schlüssel generieren**: `openssl rand -base64 32`.
|
||||
|
||||
2. **Beide Schlüssel nebeneinander konfigurieren** in `.env` und dann neu starten:
|
||||
```ini
|
||||
ENCRYPTION_KEY=NEW_VALUE
|
||||
FALLBACK_ENCRYPTION_KEY=OLD_VALUE
|
||||
```
|
||||
Neue Schreibvorgänge verwenden den neuen Schlüssel, vorhandene Zeilen werden weiterhin über den Fallback entschlüsselt.
|
||||
|
||||
3. **Vorhandene Zeilen erneut verschlüsseln**:
|
||||
|
||||
```bash
|
||||
docker exec -it {server_container} yarn command:prod secret-encryption:rotate
|
||||
```
|
||||
|
||||
Der Befehl durchläuft sechs Sites (`connected-account-tokens`, `application-variable`, `application-registration-variable`, `signing-key-private-keys`, `sensitive-config-storage`, `totp-secrets`). Ein SQL-Filter überspringt Zeilen, die bereits auf dem neuen `\<keyId>` sind, sodass der Befehl idempotent ist: Unterbrechen und bei Bedarf erneut ausführen. Beendet sich mit einem von Null verschiedenen Exit-Code, wenn irgendeine Zeile fehlschlägt — zum Wiederholen erneut ausführen.
|
||||
|
||||
| Flag | Beschreibung |
|
||||
| ---------------------------------------- | ---------------------------------------------------------------------------- |
|
||||
| `-s, --site \<site>` | Auf eine einzelne Site beschränken. |
|
||||
| `-b, --batch-size \<n>` | Zeilen pro Batch (Standard `200`, Maximum `5000`). |
|
||||
| `-d, --dry-run` | Entschlüsseln + erneut im Speicher verschlüsseln, das `UPDATE` überspringen. |
|
||||
|
||||
4. **Den Fallback entfernen**, sobald `--dry-run` null verbleibende Zeilen anzeigt: `FALLBACK_ENCRYPTION_KEY` entfernen und neu starten.
|
||||
|
||||
## Legacy-Unterstützung für `APP_SECRET`
|
||||
|
||||
Ältere Instanzen, die niemals `ENCRYPTION_KEY` gesetzt haben, verwenden `APP_SECRET` als At-Rest-Verschlüsselungsschlüssel (und als Session-Cookie-Secret, das daraus abgeleitet wird). Dieser Pfad wird aus Gründen der Abwärtskompatibilität beibehalten, ist aber **veraltet** — setze einen dedizierten `ENCRYPTION_KEY` und folge dem oben beschriebenen Rotationsverfahren, um davon zu migrieren. `APP_SECRET` selbst bleibt in Verwendung, um Legacy-HS256-Access-Tokens zu verifizieren.
|
||||
@@ -43,11 +43,26 @@ IS_CONFIG_VARIABLES_IN_DB_ENABLED=true # Standard
|
||||
|
||||
<Warning>
|
||||
Jede Variable ist mit Beschreibungen in Ihrem Admin-Panel dokumentiert unter **Einstellungen → Admin-Panel → Konfigurationsvariablen**.
|
||||
Einige Infrastruktureinstellungen wie Datenbankverbindungen (`PG_DATABASE_URL`), Server-URLs (`SERVER_URL`) und Anwendungsgeheimnisse (`APP_SECRET`) können nur über die `.env`-Datei konfiguriert werden.
|
||||
Einige Infrastruktureinstellungen wie Datenbankverbindungen (`PG_DATABASE_URL`), Server-URLs (`SERVER_URL`) und Secrets (`ENCRYPTION_KEY`, `FALLBACK_ENCRYPTION_KEY`) können nur über die `.env`-Datei konfiguriert werden.
|
||||
|
||||
[Vollständige technische Referenz →](https://github.com/twentyhq/twenty/blob/main/packages/twenty-server/src/engine/core-modules/twenty-config/config-variables.ts)
|
||||
</Warning>
|
||||
|
||||
## Verschlüsselungsschlüssel
|
||||
|
||||
Twenty verwendet zwei ausschließlich über Umgebungsvariablen konfigurierbare Verschlüsselungsschlüssel:
|
||||
|
||||
| Variable | Zweck | Erforderlich |
|
||||
| ------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------- |
|
||||
| `ENCRYPTION_KEY` | Primärer Schlüssel, der für die Verschlüsselung von Secrets im Ruhezustand verwendet wird (OAuth-Tokens, Anwendungsvariablen, private Schlüssel von Signierschlüsseln, TOTP-Secrets, sensible Konfigurationswerte). | Ja, für neue Installationen (Legacy-Installationen können stattdessen auf `APP_SECRET` zurückgreifen – siehe unten). |
|
||||
| `FALLBACK_ENCRYPTION_KEY` | Nur zur Verifizierung verwendeter Schlüssel. Während einer Rotation auf den *vorherigen* `ENCRYPTION_KEY` gesetzt, damit vorhandene Zeilen weiterhin entschlüsselt werden können. | Nur während der Rotation |
|
||||
|
||||
Aus Gründen der Abwärtskompatibilität greift Twenty, falls `ENCRYPTION_KEY` nicht gesetzt ist, für die Verschlüsselung im Ruhezustand auf `APP_SECRET` zurück – entsprechend dem Legacy-Verhalten älterer Deployments. Neue Installationen sollten immer einen eigenen `ENCRYPTION_KEY` setzen.
|
||||
|
||||
Werte mit `openssl rand -base64 32` generieren und sie an einem sicheren Ort aufbewahren (einen Secrets-Manager, eine versiegelte Konfiguration usw.). Der Verlust des `ENCRYPTION_KEY` bedeutet den Verlust des Zugriffs auf jedes in der Datenbank gespeicherte Secret.
|
||||
|
||||
Um `ENCRYPTION_KEY` ohne Downtime zu rotieren, siehe die [Anleitung zur Schlüsselrotation](/l/de/developers/self-host/capabilities/key-rotation).
|
||||
|
||||
## 2. Nur-Umgebungs-Konfiguration
|
||||
|
||||
```bash
|
||||
|
||||
@@ -31,6 +31,18 @@ Ab **v1.22** unterstützt Twenty versionsübergreifende Upgrades. Sie können di
|
||||
|
||||
Zum Beispiel wird ein Upgrade direkt von v1.22 auf v2.0 vollständig unterstützt.
|
||||
|
||||
## Upgrade auf v2.5+ – Verschlüsselungsumschlag für ruhende Daten
|
||||
|
||||
Ab **v2.5** speichert Twenty Secrets im Ruhezustand (OAuth-Tokens, Anwendungsvariablen, private Signaturschlüssel, sensible Konfigurationswerte, TOTP-Secrets) in einem versionierten Umschlag `enc:v2:`, der mit `ENCRYPTION_KEY` (oder `APP_SECRET`, falls `ENCRYPTION_KEY` nicht gesetzt ist) verschlüsselt wird.
|
||||
|
||||
Beim ersten Start mit v2.5 werden langsame Upgrade-Befehle ausgeführt, die den neuen Umschlag mit bestehenden Zeilen befüllen. Sie sind idempotent – ein Unterbrechen und Neustarten des Servers setzt an der Stelle fort, an der er aufgehört hat –, aber sie können bei großen Datenbanken eine Weile dauern. Sie können den Fortschritt mit `upgrade:status` überwachen.
|
||||
|
||||
Sie sollten **vor** dem Upgrade auf v2.5 einen dedizierten `ENCRYPTION_KEY` setzen, damit beim Nachbefüllen die Zeilen von Anfang an unter diesem Schlüssel geschrieben werden. Ein Schlüsselwechsel nach der Nachbefüllung erfordert eine [Rotation](/l/de/developers/self-host/capabilities/key-rotation).
|
||||
|
||||
## Rotation von Secrets und Signaturschlüsseln
|
||||
|
||||
Für alltägliche operative Aufgaben wie das Rotieren von `ENCRYPTION_KEY`, das Rotieren des JWT-Signaturschlüssels oder das Widerrufen eines kompromittierten Signaturschlüssels siehe den dedizierten [Leitfaden zur Schlüsselrotation](/l/de/developers/self-host/capabilities/key-rotation).
|
||||
|
||||
## Upgrade-Status prüfen
|
||||
|
||||
Mit dem Befehl `upgrade:status` können Sie den aktuellen Status Ihrer Instanz und der Workspace-Migrationen prüfen. Er ist nützlich zum Debuggen von Upgrade-Problemen oder beim Erstellen einer Support-Anfrage.
|
||||
|
||||
@@ -40,7 +40,8 @@ export default defineRole({
|
||||
objectUniversalIdentifier:
|
||||
STANDARD_OBJECT_UNIVERSAL_IDENTIFIERS.company.universalIdentifier,
|
||||
fieldUniversalIdentifier:
|
||||
STANDARD_OBJECT_UNIVERSAL_IDENTIFIERS.company.fields.name.universalIdentifier,
|
||||
STANDARD_OBJECT_UNIVERSAL_IDENTIFIERS.company.fields.name
|
||||
.universalIdentifier,
|
||||
canReadFieldValue: false,
|
||||
canUpdateFieldValue: false,
|
||||
},
|
||||
|
||||
@@ -51,7 +51,7 @@ Siga estas etapas para uma configuração manual.
|
||||
curl -o .env https://raw.githubusercontent.com/twentyhq/twenty/refs/heads/main/packages/twenty-docker/.env.example
|
||||
```
|
||||
|
||||
2. **Gere Tokens Secretos**
|
||||
2. **Gerar uma chave de criptografia**
|
||||
|
||||
Execute o seguinte comando para gerar uma string aleatória única:
|
||||
|
||||
@@ -59,16 +59,18 @@ Siga estas etapas para uma configuração manual.
|
||||
openssl rand -base64 32
|
||||
```
|
||||
|
||||
**Importante:** Mantenha este valor em segredo / não o compartilhe.
|
||||
**Importante:** Mantenha este valor em segredo / não o compartilhe. Perder `ENCRYPTION_KEY` significa perder o acesso a todos os segredos armazenados no banco de dados (tokens OAuth, variáveis de aplicação, segredos TOTP, etc.).
|
||||
|
||||
3. **Atualize o `.env`**
|
||||
|
||||
Substitua o valor do espaço reservado no seu arquivo .env pelo token gerado:
|
||||
|
||||
```ini
|
||||
APP_SECRET=first_random_string
|
||||
ENCRYPTION_KEY=random_string
|
||||
```
|
||||
|
||||
Consulte o [guia de rotação de chaves](/l/pt/developers/self-host/capabilities/key-rotation) para obter instruções sobre como fazer a rotação sem tempo de inatividade.
|
||||
|
||||
4. **Defina a Senha do Postgres**
|
||||
|
||||
Atualize o valor `PG_DATABASE_PASSWORD` no arquivo .env com uma senha forte sem caracteres especiais.
|
||||
|
||||
@@ -0,0 +1,61 @@
|
||||
---
|
||||
title: Rotação de chaves
|
||||
icon: rotate
|
||||
---
|
||||
|
||||
Twenty possui duas famílias de chaves independentes:
|
||||
|
||||
* **Chaves de assinatura JWT** — pares de chaves assimétricas ES256 (com `kid`) armazenados em `core."signingKey"`, usados para assinar e verificar tokens de acesso/atualização.
|
||||
* **Chave de criptografia em repouso** — `ENCRYPTION_KEY`, usada para criptografar tokens OAuth, variáveis de aplicação, chaves privadas de chaves de assinatura, valores confidenciais de configuração e segredos TOTP dentro de um envelope `enc:v2:`.
|
||||
|
||||
`APP_SECRET` é um segredo legado mantido para compatibilidade retroativa: quando `ENCRYPTION_KEY` não está definido, ele funciona como fallback de criptografia em repouso / cookie de sessão e ainda verifica tokens de acesso HS256 já existentes. Ele será descontinuado.
|
||||
|
||||
## Chaves de assinatura JWT
|
||||
|
||||
Cada chave carrega uma `publicKey` (mantida indefinidamente para que possa verificar tokens emitidos anteriormente), uma `privateKey` criptografada (usada apenas enquanto a chave é atual), um sinalizador `isCurrent` (exatamente uma linha por vez) e um `revokedAt` opcional.
|
||||
|
||||
### 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`.
|
||||
|
||||
<Note>O cron do Enterprise e `SIGNING_KEY_ROTATION_DAYS` são disponibilizados a partir da v2.6+.</Note>
|
||||
|
||||
### Revogar uma chave (apenas vazamento / emergência)
|
||||
|
||||
**Settings → Admin Panel → Signing keys → Revoke** em uma linha que não seja a atual. Apaga o material privado criptografado, define `revokedAt` e rejeita todos os tokens existentes assinados sob aquele `kid`.
|
||||
|
||||
## Rotacionar `ENCRYPTION_KEY`
|
||||
|
||||
<Note>O comando `secret-encryption:rotate` descrito abaixo é disponibilizado a partir da v2.6+.</Note>
|
||||
|
||||
Cada valor criptografado é encapsulado como `enc:v2:\<keyId>:\<payload>`, em que `\<keyId>` é um prefixo hexadecimal de 8 caracteres derivado da chave bruta. A rotação é online e retomável.
|
||||
|
||||
1. **Gerar uma nova chave**: `openssl rand -base64 32`.
|
||||
|
||||
2. **Configurar ambas as chaves lado a lado** em `.env`, depois reinicie:
|
||||
```ini
|
||||
ENCRYPTION_KEY=NEW_VALUE
|
||||
FALLBACK_ENCRYPTION_KEY=OLD_VALUE
|
||||
```
|
||||
Novas gravações usam a nova chave; linhas existentes ainda são descriptografadas por meio do fallback.
|
||||
|
||||
3. **Recriptografar linhas existentes**:
|
||||
|
||||
```bash
|
||||
docker exec -it {server_container} yarn command:prod secret-encryption:rotate
|
||||
```
|
||||
|
||||
O comando percorre seis sites (`connected-account-tokens`, `application-variable`, `application-registration-variable`, `signing-key-private-keys`, `sensitive-config-storage`, `totp-secrets`). Um filtro SQL ignora as linhas que já estão no novo `\<keyId>`, portanto o comando é idempotente: interrompa e execute novamente conforme necessário. Sai com código diferente de zero se alguma linha falhar — execute novamente para tentar de novo.
|
||||
|
||||
| Opção | Descrição |
|
||||
| ---------------------------------------- | -------------------------------------------------------------- |
|
||||
| `-s, --site \<site>` | Limitar a um único site. |
|
||||
| `-b, --batch-size \<n>` | Linhas por lote (padrão `200`, máximo `5000`). |
|
||||
| `-d, --dry-run` | Descriptografar + recriptografar em memória, pular o `UPDATE`. |
|
||||
|
||||
4. **Remova o fallback** assim que `--dry-run` mostrar zero linhas restantes: remova `FALLBACK_ENCRYPTION_KEY` e reinicie.
|
||||
|
||||
## Suporte legado a `APP_SECRET`
|
||||
|
||||
Instâncias antigas que nunca definiram `ENCRYPTION_KEY` usam `APP_SECRET` como chave de criptografia em repouso (e como segredo de cookie de sessão, derivado dela). Esse caminho é mantido para compatibilidade retroativa, mas está **descontinuado** — defina uma `ENCRYPTION_KEY` dedicada e siga o procedimento de rotação acima para migrar para fora dele. O próprio `APP_SECRET` continua em uso para verificar tokens de acesso HS256 legados.
|
||||
@@ -43,11 +43,26 @@ IS_CONFIG_VARIABLES_IN_DB_ENABLED=true # default
|
||||
|
||||
<Warning>
|
||||
Cada variável é documentada com descrições no seu painel de administração em **Configurações → Painel de Administração → Variáveis de Configuração**.
|
||||
Algumas configurações de infraestrutura como conexões de banco de dados (`PG_DATABASE_URL`), URLs de servidor (`SERVER_URL`) e segredos do app (`APP_SECRET`) só podem ser configuradas via arquivo `.env`.
|
||||
Algumas configurações de infraestrutura como conexões de banco de dados (`PG_DATABASE_URL`), URLs de servidor (`SERVER_URL`) e segredos (`ENCRYPTION_KEY`, `FALLBACK_ENCRYPTION_KEY`) só podem ser configuradas via arquivo `.env`.
|
||||
|
||||
[Referência técnica completa →](https://github.com/twentyhq/twenty/blob/main/packages/twenty-server/src/engine/core-modules/twenty-config/config-variables.ts)
|
||||
</Warning>
|
||||
|
||||
## Chaves de criptografia
|
||||
|
||||
A Twenty usa duas chaves de criptografia definidas apenas por variáveis de ambiente:
|
||||
|
||||
| Variável | Finalidade | Obrigatório |
|
||||
| ------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | ------------------------------------------------------------------------------------------------------------ |
|
||||
| `ENCRYPTION_KEY` | Chave primária usada para criptografar segredos em repouso (tokens OAuth, variáveis do aplicativo, chaves privadas de assinatura, segredos TOTP, valores de configuração sensíveis). | Sim para novas instalações (instalações legadas podem, em vez disso, depender de `APP_SECRET` — veja abaixo) |
|
||||
| `FALLBACK_ENCRYPTION_KEY` | Chave somente para verificação. Definida durante uma rotação para a *chave `ENCRYPTION_KEY` anterior* para que as linhas existentes permaneçam descriptografáveis. | Apenas durante a rotação |
|
||||
|
||||
Para compatibilidade retroativa, se `ENCRYPTION_KEY` não estiver definida, Twenty recorre a `APP_SECRET` para criptografia em repouso — correspondendo ao comportamento legado de implantações mais antigas. Novas instalações devem sempre definir uma `ENCRYPTION_KEY` dedicada.
|
||||
|
||||
Gere valores com `openssl rand -base64 32` e armazene-os em um local seguro (um gerenciador de segredos, configuração selada, etc.). Perder a `ENCRYPTION_KEY` significa perder o acesso a todos os segredos armazenados no banco de dados.
|
||||
|
||||
Para rotacionar a `ENCRYPTION_KEY` sem tempo de inatividade, consulte o [guia de rotação de chaves](/l/pt/developers/self-host/capabilities/key-rotation).
|
||||
|
||||
## 2. Configuração Somente por Ambiente
|
||||
|
||||
```bash
|
||||
|
||||
@@ -31,6 +31,18 @@ A partir da **v1.22**, o Twenty oferece suporte a atualizações entre versões.
|
||||
|
||||
Por exemplo, a atualização da v1.22 diretamente para a v2.0 é totalmente suportada.
|
||||
|
||||
## Atualizando para a v2.5+ — envelope de criptografia em repouso
|
||||
|
||||
A partir da **v2.5**, Twenty armazena segredos em repouso (tokens OAuth, variáveis de aplicação, chaves privadas de chaves de assinatura, valores de configuração sigilosos, segredos TOTP) dentro de um envelope versionado `enc:v2:` criptografado com `ENCRYPTION_KEY` (ou `APP_SECRET` se `ENCRYPTION_KEY` não estiver definido).
|
||||
|
||||
A primeira inicialização na v2.5 executa comandos de atualização lentos que **preenchem retroativamente** as linhas existentes no novo envelope. Eles são idempotentes — interromper e reiniciar o servidor retoma do ponto em que parou —, mas podem demorar em bancos de dados grandes. Você pode monitorar o progresso com `upgrade:status`.
|
||||
|
||||
Você deve definir uma `ENCRYPTION_KEY` dedicada **antes** da atualização para a v2.5, para que o preenchimento retroativo grave as linhas sob ela desde o início. Trocar as chaves após o preenchimento retroativo exige uma [rotação](/l/pt/developers/self-host/capabilities/key-rotation).
|
||||
|
||||
## Rotação de segredos e chaves de assinatura
|
||||
|
||||
Para tarefas operacionais do dia a dia, como rotacionar a `ENCRYPTION_KEY`, rotacionar a chave de assinatura JWT ou revogar uma chave de assinatura vazada, consulte o [guia de rotação de chaves](/l/pt/developers/self-host/capabilities/key-rotation) dedicado.
|
||||
|
||||
## Verificando o status da atualização
|
||||
|
||||
O comando `upgrade:status` permite inspecionar o estado atual da sua instância e das migrações dos espaços de trabalho. É útil para depurar problemas de atualização ou ao abrir uma solicitação de suporte.
|
||||
|
||||
@@ -40,7 +40,8 @@ export default defineRole({
|
||||
objectUniversalIdentifier:
|
||||
STANDARD_OBJECT_UNIVERSAL_IDENTIFIERS.company.universalIdentifier,
|
||||
fieldUniversalIdentifier:
|
||||
STANDARD_OBJECT_UNIVERSAL_IDENTIFIERS.company.fields.name.universalIdentifier,
|
||||
STANDARD_OBJECT_UNIVERSAL_IDENTIFIERS.company.fields.name
|
||||
.universalIdentifier,
|
||||
canReadFieldValue: false,
|
||||
canUpdateFieldValue: false,
|
||||
},
|
||||
|
||||
@@ -51,7 +51,7 @@ Urmați acești pași pentru o configurare manuală.
|
||||
curl -o .env https://raw.githubusercontent.com/twentyhq/twenty/refs/heads/main/packages/twenty-docker/.env.example
|
||||
```
|
||||
|
||||
2. **Generați Tokenuri Secrete**
|
||||
2. **Generează o cheie de criptare**
|
||||
|
||||
Rulați următoarea comandă pentru a genera un șir unic aleatoriu:
|
||||
|
||||
@@ -59,16 +59,18 @@ Urmați acești pași pentru o configurare manuală.
|
||||
openssl rand -base64 32
|
||||
```
|
||||
|
||||
**Important:** Păstrați această valoare secretă / nu o împărtășiți.
|
||||
**Important:** Păstrați această valoare secretă / nu o împărtășiți. Pierderea `ENCRYPTION_KEY` înseamnă pierderea accesului la toate secretele stocate în baza de date (tokenuri OAuth, variabile de aplicație, secrete TOTP etc.).
|
||||
|
||||
3. **Actualizați `.env`**
|
||||
|
||||
Înlocuiți valoarea substituentului în fișierul dvs. .env cu tokenul generat:
|
||||
|
||||
```ini
|
||||
APP_SECRET=first_random_string
|
||||
ENCRYPTION_KEY=random_string
|
||||
```
|
||||
|
||||
Vezi [ghidul pentru rotația cheii](/l/ro/developers/self-host/capabilities/key-rotation) pentru instrucțiuni despre cum să o rotești fără întreruperi.
|
||||
|
||||
4. **Setați Parola pentru Postgres**
|
||||
|
||||
Actualizați valoarea `PG_DATABASE_PASSWORD` în fișierul .env cu o parolă puternică fără caractere speciale.
|
||||
|
||||
@@ -0,0 +1,61 @@
|
||||
---
|
||||
title: Rotirea cheilor
|
||||
icon: rotate
|
||||
---
|
||||
|
||||
Twenty are două familii de chei independente:
|
||||
|
||||
* **Chei de semnare JWT** — perechi de chei asimetrice ES256 (etichetate cu `kid`) stocate în `core."signingKey"`, folosite pentru a semna și verifica tokenurile de acces / reîmprospătare.
|
||||
* **Cheia de criptare pentru date în repaus** — `ENCRYPTION_KEY`, folosită pentru a cripta token-urile OAuth, variabilele aplicației, cheile private de semnare, valorile de configurare sensibile și secretele TOTP în interiorul unui înveliș `enc:v2:`.
|
||||
|
||||
`APP_SECRET` este un secret vechi păstrat pentru compatibilitate retroactivă: când `ENCRYPTION_KEY` nu este setată, acționează ca mecanism de rezervă pentru criptarea datelor în repaus / cookie-urile de sesiune și încă verifică token-urile de acces HS256 existente anterior. Va fi marcat ca învechit.
|
||||
|
||||
## Chei de semnare JWT
|
||||
|
||||
Fiecare cheie are un `publicKey` (păstrat pe termen nelimitat pentru a putea verifica token-urile emise anterior), un `privateKey` criptat (folosit doar cât timp cheia este curentă), un indicator `isCurrent` (exact un rând la un moment dat) și un câmp opțional `revokedAt`.
|
||||
|
||||
### 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`.
|
||||
|
||||
<Note>Cron-ul Enterprise și `SIGNING_KEY_ROTATION_DAYS` sunt disponibile în v2.6+.</Note>
|
||||
|
||||
### Revocă o cheie (numai în caz de scurgere / urgență)
|
||||
|
||||
**Settings → Admin Panel → Signing keys → Revoke** pe un rând care nu este curent. Șterge materialul privat criptat, setează `revokedAt` și respinge fiecare token existent semnat sub acel `kid`.
|
||||
|
||||
## Rotește `ENCRYPTION_KEY`
|
||||
|
||||
<Note>Comanda `secret-encryption:rotate` descrisă mai jos este disponibilă în v2.6+.</Note>
|
||||
|
||||
Fiecare valoare criptată este încapsulată ca `enc:v2:\<keyId>:\<payload>`, unde `\<keyId>` este un prefix hexazecimal de 8 caractere derivat din cheia brută. Rotirea are loc online și poate fi reluată.
|
||||
|
||||
1. **Generează o cheie nouă**: `openssl rand -base64 32`.
|
||||
|
||||
2. **Configurează ambele chei în paralel** în `.env`, apoi repornește:
|
||||
```ini
|
||||
ENCRYPTION_KEY=NEW_VALUE
|
||||
FALLBACK_ENCRYPTION_KEY=OLD_VALUE
|
||||
```
|
||||
Noile scrieri folosesc noua cheie, rândurile existente încă sunt decriptate prin mecanismul de rezervă.
|
||||
|
||||
3. **Re-criptează rândurile existente**:
|
||||
|
||||
```bash
|
||||
docker exec -it {server_container} yarn command:prod secret-encryption:rotate
|
||||
```
|
||||
|
||||
Comanda parcurge șase locații (`connected-account-tokens`, `application-variable`, `application-registration-variable`, `signing-key-private-keys`, `sensitive-config-storage`, `totp-secrets`). Un filtru SQL omite rândurile care sunt deja pe noul `\<keyId>`, astfel încât comanda este idempotentă: întrerupe și rulează din nou după nevoie. Iese cu un cod de eroare nenul dacă vreun rând eșuează — rulează din nou pentru a reîncerca.
|
||||
|
||||
| Opțiune | Descriere |
|
||||
| ---------------------------------------- | ------------------------------------------------------ |
|
||||
| `-s, --site \<site>` | Limitează la o singură locație. |
|
||||
| `-b, --batch-size \<n>` | Rânduri per lot (implicit `200`, maxim `5000`). |
|
||||
| `-d, --dry-run` | Decriptează + re-criptează în memorie, omite `UPDATE`. |
|
||||
|
||||
4. **Elimină mecanismul de rezervă** odată ce `--dry-run` arată zero rânduri rămase: elimină `FALLBACK_ENCRYPTION_KEY` și repornește.
|
||||
|
||||
## Compatibilitate veche pentru `APP_SECRET`
|
||||
|
||||
Instanțele mai vechi care nu au setat niciodată `ENCRYPTION_KEY` folosesc `APP_SECRET` ca cheie de criptare pentru date în repaus (și ca secret pentru cookie-urile de sesiune, derivat din aceasta). Acest mecanism este păstrat pentru compatibilitate retroactivă, dar este **învechit** — setează o `ENCRYPTION_KEY` dedicată și urmează procedura de rotire de mai sus pentru a migra de la el. `APP_SECRET` în sine rămâne în uz pentru a verifica token-urile de acces HS256 vechi.
|
||||
@@ -43,11 +43,26 @@ IS_CONFIG_VARIABLES_IN_DB_ENABLED=true # implicit
|
||||
|
||||
<Warning>
|
||||
Fiecare variabilă este documentată cu descrieri în panoul dvs. de administrare la **Setări → Panou Admin → Variabile de Configurare**.
|
||||
Unele setări de infrastructură, cum ar fi conexiunile la baze de date (`PG_DATABASE_URL`), URL-urile serverului (`SERVER_URL`), și secretele aplicației (`APP_SECRET`) pot fi configurate doar prin fișierul `.env`.
|
||||
Unele setări de infrastructură, cum ar fi conexiunile la baze de date (`PG_DATABASE_URL`), URL-urile serverului (`SERVER_URL`) și secretele (`ENCRYPTION_KEY`, `FALLBACK_ENCRYPTION_KEY`) pot fi configurate doar prin fișierul `.env`.
|
||||
|
||||
[Referință tehnică completă →](https://github.com/twentyhq/twenty/blob/main/packages/twenty-server/src/engine/core-modules/twenty-config/config-variables.ts)
|
||||
</Warning>
|
||||
|
||||
## Chei de criptare
|
||||
|
||||
Twenty folosește două chei de criptare disponibile doar prin variabile de mediu:
|
||||
|
||||
| Variabilă | Scop | Obligatoriu |
|
||||
| ------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------- |
|
||||
| `ENCRYPTION_KEY` | Cheia principală folosită pentru a cripta secretele în repaus (token-uri OAuth, variabile de aplicație, chei private de semnare, secrete TOTP, valori de configurare sensibile). | Da, pentru instalările noi (instalările vechi se pot baza în schimb pe `APP_SECRET` — vezi mai jos) |
|
||||
| `FALLBACK_ENCRYPTION_KEY` | Cheie folosită doar pentru verificare. Setată în timpul unei rotații la *precedenta* `ENCRYPTION_KEY` astfel încât rândurile existente să rămână decriptabile. | Doar în timpul rotației |
|
||||
|
||||
Pentru compatibilitate cu versiunile anterioare, dacă `ENCRYPTION_KEY` nu este setată, Twenty revine la `APP_SECRET` pentru criptarea în repaus — respectând comportamentul moștenit pentru implementările mai vechi. Instalările noi ar trebui să seteze întotdeauna o `ENCRYPTION_KEY` dedicată.
|
||||
|
||||
Generează valori cu `openssl rand -base64 32` și stochează-le într-un loc sigur (un manager de secrete, configurație sigilată etc.). Pierderea `ENCRYPTION_KEY` înseamnă pierderea accesului la fiecare secret stocat în baza de date.
|
||||
|
||||
Pentru a roti `ENCRYPTION_KEY` fără timp de nefuncționare, vezi [Ghidul de rotație a cheilor](/l/ro/developers/self-host/capabilities/key-rotation).
|
||||
|
||||
## 2. Configurare Doar Mediu
|
||||
|
||||
```bash
|
||||
|
||||
@@ -31,6 +31,18 @@ Serverul rulează automat toate migrațiile de actualizare necesare la pornire.
|
||||
|
||||
De exemplu, actualizarea de la v1.22 direct la v2.0 este pe deplin acceptată.
|
||||
|
||||
## Actualizare la v2.5+ — înveliș de criptare pentru date în repaus
|
||||
|
||||
Începând cu **v2.5**, Twenty stochează secretele aflate în repaus (tokenuri OAuth, variabile de aplicație, chei private de semnare, valori de configurare sensibile, secrete TOTP) într-un înveliș versionat `enc:v2:` criptat cu `ENCRYPTION_KEY` (sau `APP_SECRET` dacă `ENCRYPTION_KEY` nu este setată).
|
||||
|
||||
Prima pornire în v2.5 rulează comenzi lente de upgrade care populează retroactiv rândurile existente în noul înveliș. Aceste comenzi sunt idempotente — întreruperea și repornirea serverului reiau procesul de unde a rămas — dar pot dura ceva timp pe baze de date mari. Poți monitoriza progresul cu `upgrade:status`.
|
||||
|
||||
Ar trebui să setezi un `ENCRYPTION_KEY` dedicat **înainte** de upgrade-ul la v2.5, astfel încât procesul de populare retroactivă să scrie rândurile sub acesta încă de la început. Schimbarea cheilor după popularea retroactivă necesită o [rotație](/l/ro/developers/self-host/capabilities/key-rotation).
|
||||
|
||||
## Rotația secretelor și a cheilor de semnare
|
||||
|
||||
Pentru sarcini operaționale de zi cu zi, precum rotația `ENCRYPTION_KEY`, rotația cheii de semnare JWT sau revocarea unei chei de semnare compromise, consultă [Ghidul de rotație a cheilor](/l/ro/developers/self-host/capabilities/key-rotation).
|
||||
|
||||
## Verificarea stării actualizării
|
||||
|
||||
Comanda `upgrade:status` vă permite să inspectați starea curentă a instanței și a migrațiilor spațiilor de lucru. Este utilă pentru depanarea problemelor de actualizare sau când trimiteți o solicitare de asistență.
|
||||
|
||||
@@ -40,7 +40,8 @@ export default defineRole({
|
||||
objectUniversalIdentifier:
|
||||
STANDARD_OBJECT_UNIVERSAL_IDENTIFIERS.company.universalIdentifier,
|
||||
fieldUniversalIdentifier:
|
||||
STANDARD_OBJECT_UNIVERSAL_IDENTIFIERS.company.fields.name.universalIdentifier,
|
||||
STANDARD_OBJECT_UNIVERSAL_IDENTIFIERS.company.fields.name
|
||||
.universalIdentifier,
|
||||
canReadFieldValue: false,
|
||||
canUpdateFieldValue: false,
|
||||
},
|
||||
|
||||
@@ -51,7 +51,7 @@ VERSION=vx.y.z BRANCH=branch-name bash <(curl -sL https://raw.githubusercontent.
|
||||
curl -o .env https://raw.githubusercontent.com/twentyhq/twenty/refs/heads/main/packages/twenty-docker/.env.example
|
||||
```
|
||||
|
||||
2. **Создайте секретные токены**
|
||||
2. **Сгенерируйте ключ шифрования**
|
||||
|
||||
Выполните следующую команду для генерации уникальной случайной строки:
|
||||
|
||||
@@ -59,16 +59,18 @@ VERSION=vx.y.z BRANCH=branch-name bash <(curl -sL https://raw.githubusercontent.
|
||||
openssl rand -base64 32
|
||||
```
|
||||
|
||||
**Внимание:** держите это значение в секрете / не делитесь им.
|
||||
**Внимание:** держите это значение в секрете / не делитесь им. Потеря `ENCRYPTION_KEY` означает потерю доступа ко всем секретам, хранящимся в базе данных (OAuth-токены, переменные приложения, TOTP-секреты и т. д.).
|
||||
|
||||
3. **Обновите файл `.env`**
|
||||
|
||||
Замените значение плейсхолдера в вашем файле .env на сгенерированный токен:
|
||||
|
||||
```ini
|
||||
APP_SECRET=first_random_string
|
||||
ENCRYPTION_KEY=random_string
|
||||
```
|
||||
|
||||
См. [руководство по ротации ключей](/l/ru/developers/self-host/capabilities/key-rotation) для получения инструкций по их ротации без простоя.
|
||||
|
||||
4. **Установите пароль для Postgres**
|
||||
|
||||
Обновите значение `PG_DATABASE_PASSWORD` в файле .env, используя надежный пароль без специальных символов.
|
||||
|
||||
@@ -0,0 +1,61 @@
|
||||
---
|
||||
title: Ротация ключей
|
||||
icon: rotate
|
||||
---
|
||||
|
||||
У Twenty есть два независимых семейства ключей:
|
||||
|
||||
* **Ключи подписи JWT** — асимметричные пары ключей ES256 (с тегами `kid`), хранятся в `core."signingKey"` и используются для подписи и проверки access/refresh-токенов.
|
||||
* **Ключ шифрования данных на диске** — `ENCRYPTION_KEY`, используется для шифрования OAuth-токенов, переменных приложения, закрытых ключей подписи, конфиденциальных значений конфигурации и TOTP-секретов внутри оболочки `enc:v2:`.
|
||||
|
||||
`APP_SECRET` — устаревший секрет, сохранённый для обратной совместимости: когда `ENCRYPTION_KEY` не установлен, он используется как резерв для шифрования данных на диске/сеансовых cookie и по-прежнему проверяет ранее выданные HS256 access-токены. Он будет объявлен устаревшим.
|
||||
|
||||
## Ключи подписи JWT
|
||||
|
||||
Каждый ключ содержит `publicKey` (хранится бессрочно, чтобы можно было проверять ранее выданные токены), зашифрованный `privateKey` (используется только пока ключ является текущим), флаг `isCurrent` (в каждый момент времени ровно одна строка) и необязательное поле `revokedAt`.
|
||||
|
||||
### Выполнить ротацию текущего ключа
|
||||
|
||||
* **Вручную** — **Settings → Admin Panel → Signing keys → Revoke** в текущей строке. Отзыв стирает его зашифрованный закрытый материал и понижает его; следующий вызов подписи автоматически создает новую пару ключей ES256 как новый текущий ключ. Токены, подписанные любым другим (не отозванным) `kid`, продолжают успешно проходить проверку до истечения срока действия.
|
||||
* **Enterprise (автоматически)** — ежедневный cron (`'15 3 * * *'` UTC) выпускает новый текущий ключ, как только существующий ключ находится в статусе текущего в течение `SIGNING_KEY_ROTATION_DAYS` (по умолчанию `90`). Предыдущий ключ *не* отзывается, поэтому токены, подписанные им, продолжают успешно проходить проверку. Зарегистрируйте его один раз с помощью `yarn command:prod cron:register:all`.
|
||||
|
||||
<Note>Enterprise-cron и `SIGNING_KEY_ROTATION_DAYS` поставляются начиная с v2.6+.</Note>
|
||||
|
||||
### Отозвать ключ (только при утечке / в экстренных случаях)
|
||||
|
||||
**Settings → Admin Panel → Signing keys → Revoke** в нетекущей строке. Стирает зашифрованный закрытый материал, устанавливает `revokedAt` и отклоняет все существующие токены, подписанные с этим `kid`.
|
||||
|
||||
## Выполнить ротацию `ENCRYPTION_KEY`
|
||||
|
||||
<Note>Команда `secret-encryption:rotate`, описанная ниже, поставляется начиная с v2.6+.</Note>
|
||||
|
||||
Каждое зашифрованное значение оборачивается как `enc:v2:\<keyId>:\<payload>`, где `\<keyId>` — это 8-символьный шестнадцатеричный префикс, полученный из исходного ключа. Ротация выполняется онлайн и может быть возобновлена.
|
||||
|
||||
1. **Сгенерировать новый ключ**: `openssl rand -base64 32`.
|
||||
|
||||
2. **Настройте оба ключа параллельно** в `.env`, затем перезапустите:
|
||||
```ini
|
||||
ENCRYPTION_KEY=NEW_VALUE
|
||||
FALLBACK_ENCRYPTION_KEY=OLD_VALUE
|
||||
```
|
||||
Новые записи используют новый ключ, существующие строки по-прежнему расшифровываются через резервный ключ.
|
||||
|
||||
3. **Повторно зашифровать существующие строки**:
|
||||
|
||||
```bash
|
||||
docker exec -it {server_container} yarn command:prod secret-encryption:rotate
|
||||
```
|
||||
|
||||
Команда обходит шесть разделов (`connected-account-tokens`, `application-variable`, `application-registration-variable`, `signing-key-private-keys`, `sensitive-config-storage`, `totp-secrets`). SQL-фильтр пропускает строки, которые уже находятся на новом `\<keyId>`, поэтому команда идемпотентна: прерывайте и запускайте повторно по мере необходимости. Завершает работу с ненулевым кодом, если какая-либо строка завершилась с ошибкой — запустите повторно, чтобы повторить попытку.
|
||||
|
||||
| Флаг | Описание |
|
||||
| ---------------------------------------- | ------------------------------------------------------------------ |
|
||||
| `-s, --site \<site>` | Ограничить одним сайтом. |
|
||||
| `-b, --batch-size \<n>` | Строк в партии (по умолчанию `200`, максимум `5000`). |
|
||||
| `-d, --dry-run` | Расшифровать + повторно зашифровать в памяти, пропустить `UPDATE`. |
|
||||
|
||||
4. **Удалите резервный ключ** после того, как `--dry-run` покажет ноль оставшихся строк: удалите `FALLBACK_ENCRYPTION_KEY` и перезапустите.
|
||||
|
||||
## Устаревшая поддержка `APP_SECRET`
|
||||
|
||||
Более старые инстансы, в которых никогда не был установлен `ENCRYPTION_KEY`, используют `APP_SECRET` как ключ шифрования данных на диске (и как секрет для сеансовых cookie, производный от него). Этот путь сохранен для обратной совместимости, но **устарел** — установите отдельный `ENCRYPTION_KEY` и выполните описанную выше процедуру ротации, чтобы с него мигрировать. Сам `APP_SECRET` продолжает использоваться для проверки устаревших HS256 access-токенов.
|
||||
@@ -43,11 +43,26 @@ IS_CONFIG_VARIABLES_IN_DB_ENABLED=true # по умолчанию
|
||||
|
||||
<Warning>
|
||||
Каждая переменная документирована с описаниями в вашей панели администратора в **Настройки → Панель администратора → Переменные конфигурации**.
|
||||
Некоторые инфраструктурные настройки, такие как соединения с базой данных (`PG_DATABASE_URL`), URL сервера (`SERVER_URL`) и секреты приложения (`APP_SECRET`), могут быть настроены только через файл `.env`.
|
||||
Некоторые инфраструктурные настройки, такие как соединения с базой данных (`PG_DATABASE_URL`), URL сервера (`SERVER_URL`) и секреты (`ENCRYPTION_KEY`, `FALLBACK_ENCRYPTION_KEY`), могут быть настроены только через файл `.env`.
|
||||
|
||||
[Полная техническая ссылка →](https://github.com/twentyhq/twenty/blob/main/packages/twenty-server/src/engine/core-modules/twenty-config/config-variables.ts)
|
||||
</Warning>
|
||||
|
||||
## Ключи шифрования
|
||||
|
||||
Twenty использует два ключа шифрования, задаваемых только через переменные окружения:
|
||||
|
||||
| Переменная | Назначение | Обязательно |
|
||||
| ------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------- |
|
||||
| `ENCRYPTION_KEY` | Основной ключ, используемый для шифрования секретов в состоянии покоя (OAuth-токены, переменные приложения, закрытые ключи для подписания, TOTP-секреты, конфиденциальные значения конфигурации). | Да, для новых установок (устаревшие установки могут вместо этого полагаться на `APP_SECRET` — см. ниже) |
|
||||
| `FALLBACK_ENCRYPTION_KEY` | Ключ, используемый только для проверки. Во время ротации установите значение равным *предыдущему* `ENCRYPTION_KEY`, чтобы существующие строки оставались доступными для расшифровки. | Только во время ротации |
|
||||
|
||||
Для обеспечения обратной совместимости, если `ENCRYPTION_KEY` не задан, Twenty использует `APP_SECRET` для шифрования данных в состоянии покоя — что соответствует устаревшему поведению для более старых развертываний. Для новых установок всегда следует задавать отдельный `ENCRYPTION_KEY`.
|
||||
|
||||
Генерируйте значения с помощью `openssl rand -base64 32` и храните их в безопасном месте (менеджер секретов, защищённая конфигурация и т. п.). Потеря `ENCRYPTION_KEY` означает потерю доступа ко всем секретам, хранящимся в базе данных.
|
||||
|
||||
Чтобы выполнить ротацию `ENCRYPTION_KEY` без простоя, см. [руководство по ротации ключей](/l/ru/developers/self-host/capabilities/key-rotation).
|
||||
|
||||
## 2. Конфигурация только для среды
|
||||
|
||||
```bash
|
||||
|
||||
@@ -31,6 +31,18 @@ cat databases_backup.sql | docker exec -i {db_container_name_or_id} psql -U {pos
|
||||
|
||||
Например, полностью поддерживается обновление с v1.22 сразу до v2.0.
|
||||
|
||||
## Переход на v2.5+ — конверт шифрования данных в состоянии покоя
|
||||
|
||||
Начиная с **v2.5**, Twenty хранит секреты в состоянии покоя (токены OAuth, переменные приложения, закрытые ключи подписи, конфиденциальные значения конфигурации, TOTP‑секреты) внутри версионируемого конверта `enc:v2:`, зашифрованного с помощью `ENCRYPTION_KEY` (или `APP_SECRET`, если `ENCRYPTION_KEY` не задан).
|
||||
|
||||
Первый запуск на v2.5 выполняет медленные команды обновления, которые **дополняют** существующие строки, помещая их в новый конверт. Они идемпотентны — при прерывании и перезапуске сервера выполнение продолжается с того места, где остановилось, — но на больших базах данных это может занять продолжительное время. Вы можете отслеживать прогресс с помощью `upgrade:status`.
|
||||
|
||||
Вам следует задать отдельный `ENCRYPTION_KEY` **перед** обновлением до v2.5, чтобы процедура дополнения с самого начала записывала строки под этим ключом. Смена ключей после завершения дополнения требует [ротации](/l/ru/developers/self-host/capabilities/key-rotation).
|
||||
|
||||
## Ротация секретов и ключей подписи
|
||||
|
||||
Для повседневных операционных задач, таких как ротация `ENCRYPTION_KEY`, ротация ключа подписи JWT или отзыв скомпрометированного ключа подписи, см. специализированное руководство [Key rotation guide](/l/ru/developers/self-host/capabilities/key-rotation).
|
||||
|
||||
## Проверка статуса обновления
|
||||
|
||||
Команда `upgrade:status` позволяет просмотреть текущее состояние вашего экземпляра и миграций рабочих пространств. Это полезно для отладки проблем с обновлением или при обращении в поддержку.
|
||||
|
||||
@@ -40,7 +40,8 @@ export default defineRole({
|
||||
objectUniversalIdentifier:
|
||||
STANDARD_OBJECT_UNIVERSAL_IDENTIFIERS.company.universalIdentifier,
|
||||
fieldUniversalIdentifier:
|
||||
STANDARD_OBJECT_UNIVERSAL_IDENTIFIERS.company.fields.name.universalIdentifier,
|
||||
STANDARD_OBJECT_UNIVERSAL_IDENTIFIERS.company.fields.name
|
||||
.universalIdentifier,
|
||||
canReadFieldValue: false,
|
||||
canUpdateFieldValue: false,
|
||||
},
|
||||
|
||||
@@ -51,7 +51,7 @@ Manuel kurulum için bu adımları uygulayın.
|
||||
curl -o .env https://raw.githubusercontent.com/twentyhq/twenty/refs/heads/main/packages/twenty-docker/.env.example
|
||||
```
|
||||
|
||||
2. **Gizli Jetonlar Oluşturun**
|
||||
2. **Bir Şifreleme Anahtarı Oluşturun**
|
||||
|
||||
Benzersiz bir rastgele karakter dizisi oluşturmak için aşağıdaki komutu çalıştırın:
|
||||
|
||||
@@ -59,16 +59,18 @@ Manuel kurulum için bu adımları uygulayın.
|
||||
openssl rand -base64 32
|
||||
```
|
||||
|
||||
**Önemli:** Bu değeri gizli tutun / paylaşmayın.
|
||||
**Önemli:** Bu değeri gizli tutun / paylaşmayın. `ENCRYPTION_KEY` kaybedilirse, veritabanında depolanan her gizli veriye (OAuth belirteçleri, uygulama değişkenleri, TOTP gizli anahtarları vb.) erişim de kaybedilir.
|
||||
|
||||
3. **`.env` Güncelleyin**
|
||||
|
||||
.env dosyanızdaki yer tutucu değeri oluşturulan jetonla değiştirin:
|
||||
|
||||
```ini
|
||||
APP_SECRET=birinci_rastgele_dize
|
||||
ENCRYPTION_KEY=random_string
|
||||
```
|
||||
|
||||
Kesinti olmadan anahtarı döndürmeye ilişkin talimatlar için [Anahtar döndürme kılavuzuna](/l/tr/developers/self-host/capabilities/key-rotation) bakın.
|
||||
|
||||
4. **Postgres Şifresini Ayarlayın**
|
||||
|
||||
.env dosyasındaki `PG_DATABASE_PASSWORD` değerini özel karakter içermeyen güçlü bir şifre ile güncelleyin.
|
||||
|
||||
@@ -0,0 +1,61 @@
|
||||
---
|
||||
title: Anahtar döndürme
|
||||
icon: rotate
|
||||
---
|
||||
|
||||
Twenty'nin iki bağımsız anahtar ailesi vardır:
|
||||
|
||||
* **JWT imzalama anahtarları** — asimetrik ES256 anahtar çiftleri (`kid` etiketli) `core."signingKey"` içinde saklanır ve erişim/yenileme belirteçlerini imzalamak ve doğrulamak için kullanılır.
|
||||
* **Bekleme hâlindeki şifreleme anahtarı** — OAuth belirteçlerini, uygulama değişkenlerini, imzalama anahtarlarının gizli anahtarlarını, hassas yapılandırma değerlerini ve `enc:v2:` zarfı içindeki TOTP gizli anahtarlarını şifrelemek için kullanılan `ENCRYPTION_KEY`.
|
||||
|
||||
`APP_SECRET`, geriye dönük uyumluluk için tutulan eski bir gizlidir: `ENCRYPTION_KEY` ayarlanmamış olduğunda bekleme hâlindeki şifreleme / oturum çerezi geri dönüşü olarak davranır ve hâlâ önceden var olan HS256 erişim belirteçlerini doğrular. Kullanımdan kaldırılacaktır.
|
||||
|
||||
## JWT imzalama anahtarları
|
||||
|
||||
Her anahtar bir `publicKey` (önceden verilmiş belirteçleri doğrulayabilmesi için süresiz olarak saklanır), şifrelenmiş bir `privateKey` (yalnızca anahtar geçerli iken kullanılır), bir `isCurrent` bayrağı (aynı anda tam olarak bir satır) ve isteğe bağlı bir `revokedAt` taşır.
|
||||
|
||||
### 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.
|
||||
|
||||
<Note>Kurumsal cron ve `SIGNING_KEY_ROTATION_DAYS` v2.6+ ile birlikte gelir.</Note>
|
||||
|
||||
### Bir anahtarı iptal et (yalnızca sızıntı / acil durum için)
|
||||
|
||||
Geçerli olmayan bir satırda **Settings → Admin Panel → Signing keys → Revoke**. Şifrelenmiş özel materyali siler, `revokedAt` değerini ayarlar ve o `kid` ile imzalanmış mevcut tüm belirteçleri reddeder.
|
||||
|
||||
## `ENCRYPTION_KEY` anahtarını döndür
|
||||
|
||||
<Note>Aşağıda açıklanan `secret-encryption:rotate` komutu v2.6+ ile birlikte gelir.</Note>
|
||||
|
||||
Her şifrelenmiş değer `enc:v2:\<keyId>:\<payload>` olarak sarılır; burada `\<keyId>`, ham anahtardan türetilen 8 onaltılık önekidir. Döndürme çevrimiçi ve kaldığı yerden devam ettirilebilir.
|
||||
|
||||
1. **Yeni bir anahtar oluşturun**: `openssl rand -base64 32`.
|
||||
|
||||
2. `.env` içinde **her iki anahtarı yan yana yapılandırın**, ardından yeniden başlatın:
|
||||
```ini
|
||||
ENCRYPTION_KEY=NEW_VALUE
|
||||
FALLBACK_ENCRYPTION_KEY=OLD_VALUE
|
||||
```
|
||||
Yeni yazmalar yeni anahtarı kullanır, mevcut satırlar hâlâ geri dönüş anahtarıyla şifresi çözülerek okunur.
|
||||
|
||||
3. **Mevcut satırları yeniden şifreleyin**:
|
||||
|
||||
```bash
|
||||
docker exec -it {server_container} yarn command:prod secret-encryption:rotate
|
||||
```
|
||||
|
||||
Komut altı siteyi dolaşır (`connected-account-tokens`, `application-variable`, `application-registration-variable`, `signing-key-private-keys`, `sensitive-config-storage`, `totp-secrets`). Bir SQL filtresi, zaten yeni `\<keyId>` üzerinde olan satırları atlar, bu nedenle komut idempotenttir: gerektiğinde yarıda kesip yeniden çalıştırın. Herhangi bir satır başarısız olursa sıfır olmayan kodla çıkar — yeniden denemek için tekrar çalıştırın.
|
||||
|
||||
| Bayrak | Açıklama |
|
||||
| ---------------------------------------- | ------------------------------------------------------------------ |
|
||||
| `-s, --site \<site>` | Yalnızca tek bir siteyle sınırla. |
|
||||
| `-b, --batch-size \<n>` | Her yığın başına satır sayısı (varsayılan `200`, en fazla `5000`). |
|
||||
| `-d, --dry-run` | Şifresini çöz + bellekte yeniden şifrele, `UPDATE` işlemini atla. |
|
||||
|
||||
4. `--dry-run` sıfır kalan satır gösterdiğinde **geri dönüş anahtarını bırakın**: `FALLBACK_ENCRYPTION_KEY` değerini kaldırın ve yeniden başlatın.
|
||||
|
||||
## Eski `APP_SECRET` desteği
|
||||
|
||||
Hiç `ENCRYPTION_KEY` ayarlamamış eski örnekler, `APP_SECRET` değerini bekleme hâlindeki şifreleme anahtarı (ve ondan türetilen oturum çerezi gizli anahtarı) olarak kullanır. Bu yol, geriye dönük uyumluluk için korunmuştur ancak **kullanımdan kaldırılmıştır** — özel bir `ENCRYPTION_KEY` ayarlayın ve ondan çıkmak için yukarıdaki döndürme prosedürünü izleyin. `APP_SECRET`in kendisi eski HS256 erişim belirteçlerini doğrulamak için kullanılmaya devam eder.
|
||||
@@ -43,11 +43,26 @@ IS_CONFIG_VARIABLES_IN_DB_ENABLED=true # varsayılan
|
||||
|
||||
<Warning>
|
||||
Her bir değişken, yönetici panelinizde **Ayarlar → Yönetici Paneli → Konfigürasyon Değişkenleri** altında açıklamalarla belgelenir.
|
||||
Veritabanı bağlantıları (`PG_DATABASE_URL`), sunucu URL'leri (`SERVER_URL`) ve uygulama gizli anahtarları (`APP_SECRET`) gibi bazı altyapı ayarları yalnızca `.env` dosyası aracılığıyla yapılandırılabilir.
|
||||
Veritabanı bağlantıları (`PG_DATABASE_URL`), sunucu URL'leri (`SERVER_URL`) ve gizli anahtarlar (`ENCRYPTION_KEY`, `FALLBACK_ENCRYPTION_KEY`) gibi bazı altyapı ayarları yalnızca `.env` dosyası aracılığıyla yapılandırılabilir.
|
||||
|
||||
[Tam teknik referans →](https://github.com/twentyhq/twenty/blob/main/packages/twenty-server/src/engine/core-modules/twenty-config/config-variables.ts)
|
||||
</Warning>
|
||||
|
||||
## Şifreleme anahtarları
|
||||
|
||||
Twenty yalnızca ortam değişkenleriyle yapılandırılan iki şifreleme anahtarı kullanır:
|
||||
|
||||
| Değişken | Amaç | Zorunlu |
|
||||
| ------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------- |
|
||||
| `ENCRYPTION_KEY` | Dinlenik durumdaki gizli verileri şifrelemek için kullanılan birincil anahtar (OAuth belirteçleri, uygulama değişkenleri, imzalama anahtarlarının özel anahtarları, TOTP sırları, hassas yapılandırma değerleri). | Yeni kurulumlar için evet (eski kurulumlar bunun yerine aşağıda açıklandığı gibi `APP_SECRET`'e dayanıyor olabilir) |
|
||||
| `FALLBACK_ENCRYPTION_KEY` | Yalnızca doğrulama için kullanılan anahtar. Döndürme sırasında, mevcut satırların şifresinin çözülebilir kalması için *önceki* `ENCRYPTION_KEY` değerine ayarlanır. | Yalnızca döndürme sırasında |
|
||||
|
||||
Geriye dönük uyumluluk için, `ENCRYPTION_KEY` ayarlanmadıysa, Twenty dinlenik durumdaki şifreleme için `APP_SECRET`'e geri döner — bu, daha eski dağıtımlardaki eski davranışla eşleşir. Yeni kurulumlarda her zaman özel bir `ENCRYPTION_KEY` ayarlanmalıdır.
|
||||
|
||||
Değerleri `openssl rand -base64 32` ile oluşturun ve onları güvenli bir yerde saklayın (bir gizli yönetim sistemi, mühürlü yapılandırma vb.). `ENCRYPTION_KEY`'in kaybedilmesi, veritabanında saklanan tüm gizli bilgilere erişimin kaybedilmesi anlamına gelir.
|
||||
|
||||
`ENCRYPTION_KEY`'i kesinti olmadan döndürmek için [Anahtar döndürme rehberi](/l/tr/developers/self-host/capabilities/key-rotation) bölümüne bakın.
|
||||
|
||||
## 2. Yalnızca Ortam Değişkenleriyle Yapılandırma
|
||||
|
||||
```bash
|
||||
|
||||
@@ -31,6 +31,18 @@ Twenty, **v1.22** sürümünden itibaren sürümler arası yükseltmeleri destek
|
||||
|
||||
Örneğin, v1.22'den doğrudan v2.0'a yükseltme tamamen desteklenir.
|
||||
|
||||
## v2.5+ sürümüne yükseltme — bekleme hâlindeki şifreleme zarfı
|
||||
|
||||
**v2.5** ile birlikte, Twenty beklemede olan gizli verileri (OAuth belirteçleri, uygulama değişkenleri, imzalama için kullanılan özel anahtarlar, hassas yapılandırma değerleri, TOTP sırları) sürümlendirilmiş ve `ENCRYPTION_KEY` (veya `ENCRYPTION_KEY` ayarlı değilse `APP_SECRET`) ile şifrelenmiş bir `enc:v2:` zarfının içinde saklar.
|
||||
|
||||
v2.5 sürümündeki ilk açılış, mevcut satırları yeni zarfa **geri dolduran** yavaş yükseltme komutlarını çalıştırır. Bunlar idempotandır — sunucuyu durdurup yeniden başlattığınızda kaldığı yerden devam eder — ancak büyük veritabanlarında biraz zaman alabilirler. İlerlemeyi `upgrade:status` ile izleyebilirsiniz.
|
||||
|
||||
Geri doldurmanın, en başından itibaren satırları onun altında yazabilmesi için, v2.5 yükseltmesinden **önce** özel bir `ENCRYPTION_KEY` ayarlamalısınız. Geri doldurmadan sonra anahtarları değiştirmek bir [anahtar döndürme](/l/tr/developers/self-host/capabilities/key-rotation) gerektirir.
|
||||
|
||||
## Gizli verilerin ve imzalama anahtarlarının döndürülmesi
|
||||
|
||||
`ENCRYPTION_KEY` döndürme, JWT imzalama anahtarını döndürme veya sızdırılmış bir imzalama anahtarını iptal etme gibi günlük operasyonel görevler için özel [Anahtar döndürme kılavuzu](/l/tr/developers/self-host/capabilities/key-rotation) bölümüne bakın.
|
||||
|
||||
## Yükseltme durumunu kontrol etme
|
||||
|
||||
`upgrade:status` komutu, örneğinizin ve çalışma alanı migrasyonlarının mevcut durumunu incelemenizi sağlar. Yükseltme sorunlarında hata ayıklamak veya bir destek talebi oluştururken kullanışlıdır.
|
||||
|
||||
@@ -40,7 +40,8 @@ export default defineRole({
|
||||
objectUniversalIdentifier:
|
||||
STANDARD_OBJECT_UNIVERSAL_IDENTIFIERS.company.universalIdentifier,
|
||||
fieldUniversalIdentifier:
|
||||
STANDARD_OBJECT_UNIVERSAL_IDENTIFIERS.company.fields.name.universalIdentifier,
|
||||
STANDARD_OBJECT_UNIVERSAL_IDENTIFIERS.company.fields.name
|
||||
.universalIdentifier,
|
||||
canReadFieldValue: false,
|
||||
canUpdateFieldValue: false,
|
||||
},
|
||||
|
||||
@@ -51,7 +51,7 @@ VERSION=vx.y.z BRANCH=branch-name bash <(curl -sL https://raw.githubusercontent.
|
||||
curl -o .env https://raw.githubusercontent.com/twentyhq/twenty/refs/heads/main/packages/twenty-docker/.env.example
|
||||
```
|
||||
|
||||
2. **生成密钥令牌**
|
||||
2. **生成加密密钥**
|
||||
|
||||
运行以下命令以生成唯一的随机字符串:
|
||||
|
||||
@@ -59,16 +59,18 @@ VERSION=vx.y.z BRANCH=branch-name bash <(curl -sL https://raw.githubusercontent.
|
||||
openssl rand -base64 32
|
||||
```
|
||||
|
||||
**重要:** 保持该值秘密/不要共享。
|
||||
**重要:** 保持该值秘密/不要共享。 丢失 `ENCRYPTION_KEY` 意味着将失去对数据库中存储的所有机密信息(OAuth 令牌、应用程序变量、TOTP 机密等)的访问权限。
|
||||
|
||||
3. **更新`.env`**
|
||||
|
||||
用生成的令牌替换.env文件中的占位符值:
|
||||
|
||||
```ini
|
||||
APP_SECRET=first_random_string
|
||||
ENCRYPTION_KEY=random_string
|
||||
```
|
||||
|
||||
有关在不停机的情况下轮换密钥的说明,请参阅[密钥轮换指南](/l/zh/developers/self-host/capabilities/key-rotation)。
|
||||
|
||||
4. **设置Postgres密码**
|
||||
|
||||
用不含特殊字符的强密码更新.env文件中的`PG_DATABASE_PASSWORD`值。
|
||||
|
||||
@@ -0,0 +1,61 @@
|
||||
---
|
||||
title: 密钥轮换
|
||||
icon: rotate
|
||||
---
|
||||
|
||||
Twenty 具有两个相互独立的密钥族:
|
||||
|
||||
* **JWT 签名密钥** — 存储在 `core."signingKey"` 中、带有 `kid` 标签的非对称 ES256 密钥对,用于对访问令牌 / 刷新令牌进行签名和验证。
|
||||
* **静态加密密钥** — `ENCRYPTION_KEY`,用于在 `enc:v2:` 封装中加密 OAuth 令牌、应用变量、签名密钥私钥、敏感配置值和 TOTP 密钥。
|
||||
|
||||
`APP_SECRET` 是为向后兼容而保留的旧版密钥:当未设置 `ENCRYPTION_KEY` 时,它充当静态加密 / 会话 Cookie 的后备密钥,并且仍会验证已有的 HS256 访问令牌。 它将被弃用。
|
||||
|
||||
## JWT 签名密钥
|
||||
|
||||
每个密钥都包含一个 `publicKey`(无限期保留,以便验证之前签发的令牌)、一个加密的 `privateKey`(仅在该密钥为当前密钥时使用)、一个 `isCurrent` 标志(同一时间只有一行为当前),以及一个可选的 `revokedAt`。
|
||||
|
||||
### 轮换当前密钥
|
||||
|
||||
* **手动** — 在当前行上执行 **Settings → Admin Panel → Signing keys → Revoke**。 吊销操作会清除其加密的私有材料并将其降级;下一次签名调用会自动生成一个新的 ES256 密钥对,作为新的当前密钥。 在任何其他(未被吊销的)`kid` 下签名的令牌会一直验证,直到过期。
|
||||
* **Enterprise(自动)** — 每日 cron(`'15 3 * * *'` UTC)会在现有密钥已作为当前密钥使用 `SIGNING_KEY_ROTATION_DAYS`(默认 `90`)后签发一个新的当前密钥。 先前的密钥*不会*被吊销,因此在其下签名的令牌仍然可以通过验证。 使用 `yarn command:prod cron:register:all` 注册一次。
|
||||
|
||||
<Note>Enterprise 的 cron 和 `SIGNING_KEY_ROTATION_DAYS` 从 v2.6+ 版本开始提供。</Note>
|
||||
|
||||
### 吊销密钥(仅限泄露 / 紧急情况)
|
||||
|
||||
在非当前行上执行 **Settings → Admin Panel → Signing keys → Revoke**。 会清除加密的私有材料,设置 `revokedAt`,并拒绝在该 `kid` 下签名的所有现有令牌。
|
||||
|
||||
## 轮换 `ENCRYPTION_KEY`
|
||||
|
||||
<Note>下面描述的 `secret-encryption:rotate` 命令从 v2.6+ 版本开始提供。</Note>
|
||||
|
||||
每个加密值都封装为 `enc:v2:\<keyId>:\<payload>`,其中 `\<keyId>` 是从原始密钥派生出的 8 位十六进制前缀。 轮换是在线且可恢复的。
|
||||
|
||||
1. **生成新密钥**:`openssl rand -base64 32`。
|
||||
|
||||
2. **在 `.env` 中并排配置两个密钥**,然后重启:
|
||||
```ini
|
||||
ENCRYPTION_KEY=NEW_VALUE
|
||||
FALLBACK_ENCRYPTION_KEY=OLD_VALUE
|
||||
```
|
||||
新的写入使用新密钥,现有行仍通过后备密钥解密。
|
||||
|
||||
3. **重新加密现有行**:
|
||||
|
||||
```bash
|
||||
docker exec -it {server_container} yarn command:prod secret-encryption:rotate
|
||||
```
|
||||
|
||||
该命令会遍历六个位置(`connected-account-tokens`、`application-variable`、`application-registration-variable`、`signing-key-private-keys`、`sensitive-config-storage`、`totp-secrets`)。 SQL 过滤器会跳过已使用新 `\<keyId>` 的行,因此该命令是幂等的:可根据需要中断并重新运行。 如果任意一行失败则以非零状态退出 — 重新运行以重试。
|
||||
|
||||
| 标志 | 描述 |
|
||||
| ---------------------------------------- | ---------------------------- |
|
||||
| `-s, --site \<site>` | 仅限单个位置。 |
|
||||
| `-b, --batch-size \<n>` | 每批处理的行数(默认 `200`,最大 `5000`)。 |
|
||||
| `-d, --dry-run` | 在内存中解密并重新加密,跳过 `UPDATE`。 |
|
||||
|
||||
4. 一旦 `--dry-run` 显示没有剩余行,就**删除后备密钥**:移除 `FALLBACK_ENCRYPTION_KEY` 并重启。
|
||||
|
||||
## 旧版 `APP_SECRET` 支持
|
||||
|
||||
从未设置 `ENCRYPTION_KEY` 的旧实例会使用 `APP_SECRET` 作为静态加密密钥(以及会话 Cookie 密钥,从中派生)。 此路径为向后兼容而保留,但已被**弃用** — 请设置专用的 `ENCRYPTION_KEY`,并按照上述轮换流程迁移离开它。 `APP_SECRET` 本身仍用于验证旧版 HS256 访问令牌。
|
||||
@@ -43,11 +43,26 @@ IS_CONFIG_VARIABLES_IN_DB_ENABLED=true # default
|
||||
|
||||
<Warning>
|
||||
每个变量都在您的管理面板的**设置 → 管理员面板 → 配置变量**中记录了说明。
|
||||
某些基础设施设置如数据库连接 (`PG_DATABASE_URL`)、服务器 URL (`SERVER_URL`) 和应用程序密钥 (`APP_SECRET`) 只能通过 `.env` 文件配置。
|
||||
某些基础设施设置如数据库连接 (`PG_DATABASE_URL`)、服务器 URL (`SERVER_URL`) 和密钥(`ENCRYPTION_KEY`、`FALLBACK_ENCRYPTION_KEY`)只能通过 `.env` 文件配置。
|
||||
|
||||
[完整技术参考→](https://github.com/twentyhq/twenty/blob/main/packages/twenty-server/src/engine/core-modules/twenty-config/config-variables.ts)
|
||||
</Warning>
|
||||
|
||||
## 加密密钥
|
||||
|
||||
Twenty 使用两个仅通过环境变量配置的加密密钥:
|
||||
|
||||
| 变量 | 目的 | 必填 |
|
||||
| ------------------------- | ------------------------------------------------------------ | ------------------------------------- |
|
||||
| `ENCRYPTION_KEY` | 用于对静态存储的机密信息(OAuth 令牌、应用程序变量、签名密钥的私钥、TOTP 密钥、敏感配置值)进行加密的主密钥。 | 新安装必须设置(旧安装可能依赖于 `APP_SECRET` —— 见下文) |
|
||||
| `FALLBACK_ENCRYPTION_KEY` | 仅用于验证的密钥。 在轮换期间,将其设置为*先前的* `ENCRYPTION_KEY`,以便现有行仍可解密。 | 仅在轮换期间 |
|
||||
|
||||
为保持向后兼容性,如果未设置 `ENCRYPTION_KEY`,Twenty 将回退为使用 `APP_SECRET` 进行静态数据加密——与旧部署的旧版行为一致。 新的安装应始终设置专用的 `ENCRYPTION_KEY`。
|
||||
|
||||
使用 `openssl rand -base64 32` 生成值,并将其安全存储(如密钥管理器、加密配置等)。 丢失 `ENCRYPTION_KEY` 意味着将失去对数据库中存储的所有机密的访问权限。
|
||||
|
||||
要在不停机的情况下轮换 `ENCRYPTION_KEY`,请参阅[密钥轮换指南](/l/zh/developers/self-host/capabilities/key-rotation)。
|
||||
|
||||
## 2. 仅限环境配置
|
||||
|
||||
```bash
|
||||
|
||||
@@ -31,6 +31,18 @@ cat databases_backup.sql | docker exec -i {db_container_name_or_id} psql -U {pos
|
||||
|
||||
例如,从 v1.22 直接升级到 v2.0 完全受支持。
|
||||
|
||||
## 升级到 v2.5+ — 静态数据加密信封
|
||||
|
||||
从 **v2.5** 开始,Twenty 会将静态存储的机密(OAuth 令牌、应用程序变量、签名密钥的私钥、敏感配置值、TOTP 机密)存储在带版本的 `enc:v2:` 加密信封中,并使用 `ENCRYPTION_KEY`(如果未设置 `ENCRYPTION_KEY`,则使用 `APP_SECRET`)进行加密。
|
||||
|
||||
在 v2.5 的首次启动时会运行较慢的升级命令,将现有行**回填**到新的加密信封中。 这些命令是幂等的——中断并重新启动服务器后会从上次停止的位置恢复——但在大型数据库上可能需要一段时间才能完成。 你可以使用 `upgrade:status` 来监控进度。
|
||||
|
||||
你应该在 v2.5 升级**之前**设置专用的 `ENCRYPTION_KEY`,这样回填从一开始就会在该密钥下写入行。 在回填完成后再更换密钥,则需要进行一次[轮换](/l/zh/developers/self-host/capabilities/key-rotation)。
|
||||
|
||||
## 轮换机密和签名密钥
|
||||
|
||||
对于日常运维任务,例如轮换 `ENCRYPTION_KEY`、轮换 JWT 签名密钥或吊销泄露的签名密钥,请参阅专门的[密钥轮换指南](/l/zh/developers/self-host/capabilities/key-rotation)。
|
||||
|
||||
## 检查升级状态
|
||||
|
||||
`upgrade:status` 命令可用于查看您的实例和工作区迁移的当前状态。 这在调试升级问题或提交支持请求时非常有用。
|
||||
|
||||
File diff suppressed because one or more lines are too long
@@ -533,13 +533,6 @@ msgstr "{workspaceMemberName} sal ontkoppel word van die volgende rol:"
|
||||
msgid "#"
|
||||
msgstr ""
|
||||
|
||||
#. js-lingui-id: CwstSL
|
||||
#: src/modules/settings/accounts/components/SettingsAccountsConnectionForm.tsx
|
||||
#: src/modules/settings/accounts/components/SettingsAccountsConnectionForm.tsx
|
||||
#: src/modules/settings/accounts/components/SettingsAccountsConnectionForm.tsx
|
||||
msgid "••••••••"
|
||||
msgstr "••••••••"
|
||||
|
||||
#. js-lingui-id: n9nj+X
|
||||
#. placeholder {0}: aiChatThreadPendingDelete?.threadTitle ?? ''
|
||||
#: src/modules/ai/components/AiChatThreadDeleteConfirmationModal.tsx
|
||||
@@ -2652,11 +2645,6 @@ msgstr "Terug na {linkText}"
|
||||
msgid "Back to content"
|
||||
msgstr "Terug na inhoud"
|
||||
|
||||
#. js-lingui-id: OvBnOM
|
||||
#: src/modules/ui/navigation/bread-crumb/components/MobileBreadcrumb.tsx
|
||||
msgid "Back to Settings"
|
||||
msgstr "Terug na Instellings"
|
||||
|
||||
#. js-lingui-id: GtJbUa
|
||||
#: src/modules/error-handler/components/AppRootErrorFallback.tsx
|
||||
msgid "Background"
|
||||
@@ -3184,6 +3172,11 @@ msgstr "Verander e-pos"
|
||||
msgid "Change node type"
|
||||
msgstr "Verander nodustipe"
|
||||
|
||||
#. js-lingui-id: GptGxg
|
||||
#: src/modules/settings/accounts/components/SettingsAccountsPasswordController.tsx
|
||||
msgid "Change password"
|
||||
msgstr ""
|
||||
|
||||
#. js-lingui-id: VhMDMg
|
||||
#: src/pages/auth/PasswordReset.tsx
|
||||
#: src/modules/settings/profile/components/SetOrChangePassword.tsx
|
||||
@@ -3467,6 +3460,11 @@ msgstr "Maak toe"
|
||||
msgid "Close banner"
|
||||
msgstr "Sluit banier"
|
||||
|
||||
#. js-lingui-id: vcpc5o
|
||||
#: src/modules/blocknote-editor/components/BlockEditor.tsx
|
||||
msgid "Close menu"
|
||||
msgstr ""
|
||||
|
||||
#. js-lingui-id: saip/v
|
||||
#: src/modules/side-panel/components/SidePanelToggleButton.tsx
|
||||
msgid "Close side panel"
|
||||
@@ -13178,6 +13176,11 @@ msgstr "Rolnaam"
|
||||
msgid "Role name cannot be empty"
|
||||
msgstr "Rolnaam kan nie leeg wees nie"
|
||||
|
||||
#. js-lingui-id: VMX1Oy
|
||||
#: src/modules/metadata-error-handler/hooks/useMetadataErrorHandler.ts
|
||||
msgid "role permission flag"
|
||||
msgstr ""
|
||||
|
||||
#. js-lingui-id: jLNGne
|
||||
#: src/modules/metadata-error-handler/hooks/useMetadataErrorHandler.ts
|
||||
msgid "role target"
|
||||
|
||||
@@ -533,13 +533,6 @@ msgstr "{workspaceMemberName} سيتم إلغاء تعيينه من الدور
|
||||
msgid "#"
|
||||
msgstr ""
|
||||
|
||||
#. js-lingui-id: CwstSL
|
||||
#: src/modules/settings/accounts/components/SettingsAccountsConnectionForm.tsx
|
||||
#: src/modules/settings/accounts/components/SettingsAccountsConnectionForm.tsx
|
||||
#: src/modules/settings/accounts/components/SettingsAccountsConnectionForm.tsx
|
||||
msgid "••••••••"
|
||||
msgstr "••••••••"
|
||||
|
||||
#. js-lingui-id: n9nj+X
|
||||
#. placeholder {0}: aiChatThreadPendingDelete?.threadTitle ?? ''
|
||||
#: src/modules/ai/components/AiChatThreadDeleteConfirmationModal.tsx
|
||||
@@ -2652,11 +2645,6 @@ msgstr "العودة إلى {linkText}"
|
||||
msgid "Back to content"
|
||||
msgstr "العودة إلى المحتوى"
|
||||
|
||||
#. js-lingui-id: OvBnOM
|
||||
#: src/modules/ui/navigation/bread-crumb/components/MobileBreadcrumb.tsx
|
||||
msgid "Back to Settings"
|
||||
msgstr "العودة إلى الإعدادات"
|
||||
|
||||
#. js-lingui-id: GtJbUa
|
||||
#: src/modules/error-handler/components/AppRootErrorFallback.tsx
|
||||
msgid "Background"
|
||||
@@ -3184,6 +3172,11 @@ msgstr "تغيير البريد الإلكتروني"
|
||||
msgid "Change node type"
|
||||
msgstr "تغيير نوع العقدة"
|
||||
|
||||
#. js-lingui-id: GptGxg
|
||||
#: src/modules/settings/accounts/components/SettingsAccountsPasswordController.tsx
|
||||
msgid "Change password"
|
||||
msgstr ""
|
||||
|
||||
#. js-lingui-id: VhMDMg
|
||||
#: src/pages/auth/PasswordReset.tsx
|
||||
#: src/modules/settings/profile/components/SetOrChangePassword.tsx
|
||||
@@ -3467,6 +3460,11 @@ msgstr "إغلاق"
|
||||
msgid "Close banner"
|
||||
msgstr "إغلاق اللافتة"
|
||||
|
||||
#. js-lingui-id: vcpc5o
|
||||
#: src/modules/blocknote-editor/components/BlockEditor.tsx
|
||||
msgid "Close menu"
|
||||
msgstr ""
|
||||
|
||||
#. js-lingui-id: saip/v
|
||||
#: src/modules/side-panel/components/SidePanelToggleButton.tsx
|
||||
msgid "Close side panel"
|
||||
@@ -13178,6 +13176,11 @@ msgstr "اسم الدور"
|
||||
msgid "Role name cannot be empty"
|
||||
msgstr "لا يمكن أن يكون اسم الدور فارغًا"
|
||||
|
||||
#. js-lingui-id: VMX1Oy
|
||||
#: src/modules/metadata-error-handler/hooks/useMetadataErrorHandler.ts
|
||||
msgid "role permission flag"
|
||||
msgstr ""
|
||||
|
||||
#. js-lingui-id: jLNGne
|
||||
#: src/modules/metadata-error-handler/hooks/useMetadataErrorHandler.ts
|
||||
msgid "role target"
|
||||
|
||||
@@ -533,13 +533,6 @@ msgstr "{workspaceMemberName} serà desassignat del següent rol:"
|
||||
msgid "#"
|
||||
msgstr ""
|
||||
|
||||
#. js-lingui-id: CwstSL
|
||||
#: src/modules/settings/accounts/components/SettingsAccountsConnectionForm.tsx
|
||||
#: src/modules/settings/accounts/components/SettingsAccountsConnectionForm.tsx
|
||||
#: src/modules/settings/accounts/components/SettingsAccountsConnectionForm.tsx
|
||||
msgid "••••••••"
|
||||
msgstr "••••••••"
|
||||
|
||||
#. js-lingui-id: n9nj+X
|
||||
#. placeholder {0}: aiChatThreadPendingDelete?.threadTitle ?? ''
|
||||
#: src/modules/ai/components/AiChatThreadDeleteConfirmationModal.tsx
|
||||
@@ -2652,11 +2645,6 @@ msgstr "Torna a {linkText}"
|
||||
msgid "Back to content"
|
||||
msgstr "Tornar al contingut"
|
||||
|
||||
#. js-lingui-id: OvBnOM
|
||||
#: src/modules/ui/navigation/bread-crumb/components/MobileBreadcrumb.tsx
|
||||
msgid "Back to Settings"
|
||||
msgstr "Torna a Configuració"
|
||||
|
||||
#. js-lingui-id: GtJbUa
|
||||
#: src/modules/error-handler/components/AppRootErrorFallback.tsx
|
||||
msgid "Background"
|
||||
@@ -3184,6 +3172,11 @@ msgstr "Canviar el correu electrònic"
|
||||
msgid "Change node type"
|
||||
msgstr "Canvia el tipus de node"
|
||||
|
||||
#. js-lingui-id: GptGxg
|
||||
#: src/modules/settings/accounts/components/SettingsAccountsPasswordController.tsx
|
||||
msgid "Change password"
|
||||
msgstr ""
|
||||
|
||||
#. js-lingui-id: VhMDMg
|
||||
#: src/pages/auth/PasswordReset.tsx
|
||||
#: src/modules/settings/profile/components/SetOrChangePassword.tsx
|
||||
@@ -3467,6 +3460,11 @@ msgstr "Tanca"
|
||||
msgid "Close banner"
|
||||
msgstr "Tanca el bàner"
|
||||
|
||||
#. js-lingui-id: vcpc5o
|
||||
#: src/modules/blocknote-editor/components/BlockEditor.tsx
|
||||
msgid "Close menu"
|
||||
msgstr ""
|
||||
|
||||
#. js-lingui-id: saip/v
|
||||
#: src/modules/side-panel/components/SidePanelToggleButton.tsx
|
||||
msgid "Close side panel"
|
||||
@@ -13178,6 +13176,11 @@ msgstr "Nom del rol"
|
||||
msgid "Role name cannot be empty"
|
||||
msgstr "El nom del rol no pot estar buit"
|
||||
|
||||
#. js-lingui-id: VMX1Oy
|
||||
#: src/modules/metadata-error-handler/hooks/useMetadataErrorHandler.ts
|
||||
msgid "role permission flag"
|
||||
msgstr ""
|
||||
|
||||
#. js-lingui-id: jLNGne
|
||||
#: src/modules/metadata-error-handler/hooks/useMetadataErrorHandler.ts
|
||||
msgid "role target"
|
||||
|
||||
@@ -533,13 +533,6 @@ msgstr "{workspaceMemberName} bude odebrán z následující role:"
|
||||
msgid "#"
|
||||
msgstr ""
|
||||
|
||||
#. js-lingui-id: CwstSL
|
||||
#: src/modules/settings/accounts/components/SettingsAccountsConnectionForm.tsx
|
||||
#: src/modules/settings/accounts/components/SettingsAccountsConnectionForm.tsx
|
||||
#: src/modules/settings/accounts/components/SettingsAccountsConnectionForm.tsx
|
||||
msgid "••••••••"
|
||||
msgstr "••••••••"
|
||||
|
||||
#. js-lingui-id: n9nj+X
|
||||
#. placeholder {0}: aiChatThreadPendingDelete?.threadTitle ?? ''
|
||||
#: src/modules/ai/components/AiChatThreadDeleteConfirmationModal.tsx
|
||||
@@ -2652,11 +2645,6 @@ msgstr "Zpět na {linkText}"
|
||||
msgid "Back to content"
|
||||
msgstr "Zpět k obsahu"
|
||||
|
||||
#. js-lingui-id: OvBnOM
|
||||
#: src/modules/ui/navigation/bread-crumb/components/MobileBreadcrumb.tsx
|
||||
msgid "Back to Settings"
|
||||
msgstr "Zpět do nastavení"
|
||||
|
||||
#. js-lingui-id: GtJbUa
|
||||
#: src/modules/error-handler/components/AppRootErrorFallback.tsx
|
||||
msgid "Background"
|
||||
@@ -3184,6 +3172,11 @@ msgstr "Změnit email"
|
||||
msgid "Change node type"
|
||||
msgstr "Změnit typ uzlu"
|
||||
|
||||
#. js-lingui-id: GptGxg
|
||||
#: src/modules/settings/accounts/components/SettingsAccountsPasswordController.tsx
|
||||
msgid "Change password"
|
||||
msgstr ""
|
||||
|
||||
#. js-lingui-id: VhMDMg
|
||||
#: src/pages/auth/PasswordReset.tsx
|
||||
#: src/modules/settings/profile/components/SetOrChangePassword.tsx
|
||||
@@ -3467,6 +3460,11 @@ msgstr "Zavřít"
|
||||
msgid "Close banner"
|
||||
msgstr "Zavřít banner"
|
||||
|
||||
#. js-lingui-id: vcpc5o
|
||||
#: src/modules/blocknote-editor/components/BlockEditor.tsx
|
||||
msgid "Close menu"
|
||||
msgstr ""
|
||||
|
||||
#. js-lingui-id: saip/v
|
||||
#: src/modules/side-panel/components/SidePanelToggleButton.tsx
|
||||
msgid "Close side panel"
|
||||
@@ -13178,6 +13176,11 @@ msgstr "Název role"
|
||||
msgid "Role name cannot be empty"
|
||||
msgstr "Název role nesmí být prázdný"
|
||||
|
||||
#. js-lingui-id: VMX1Oy
|
||||
#: src/modules/metadata-error-handler/hooks/useMetadataErrorHandler.ts
|
||||
msgid "role permission flag"
|
||||
msgstr ""
|
||||
|
||||
#. js-lingui-id: jLNGne
|
||||
#: src/modules/metadata-error-handler/hooks/useMetadataErrorHandler.ts
|
||||
msgid "role target"
|
||||
|
||||
@@ -533,13 +533,6 @@ msgstr "{workspaceMemberName} vil blive fjernet fra følgende rolle:"
|
||||
msgid "#"
|
||||
msgstr ""
|
||||
|
||||
#. js-lingui-id: CwstSL
|
||||
#: src/modules/settings/accounts/components/SettingsAccountsConnectionForm.tsx
|
||||
#: src/modules/settings/accounts/components/SettingsAccountsConnectionForm.tsx
|
||||
#: src/modules/settings/accounts/components/SettingsAccountsConnectionForm.tsx
|
||||
msgid "••••••••"
|
||||
msgstr ""
|
||||
|
||||
#. js-lingui-id: n9nj+X
|
||||
#. placeholder {0}: aiChatThreadPendingDelete?.threadTitle ?? ''
|
||||
#: src/modules/ai/components/AiChatThreadDeleteConfirmationModal.tsx
|
||||
@@ -2652,11 +2645,6 @@ msgstr "Tilbage til {linkText}"
|
||||
msgid "Back to content"
|
||||
msgstr "Tilbage til indhold"
|
||||
|
||||
#. js-lingui-id: OvBnOM
|
||||
#: src/modules/ui/navigation/bread-crumb/components/MobileBreadcrumb.tsx
|
||||
msgid "Back to Settings"
|
||||
msgstr "Tilbage til Indstillinger"
|
||||
|
||||
#. js-lingui-id: GtJbUa
|
||||
#: src/modules/error-handler/components/AppRootErrorFallback.tsx
|
||||
msgid "Background"
|
||||
@@ -3184,6 +3172,11 @@ msgstr "Skift e-mail"
|
||||
msgid "Change node type"
|
||||
msgstr "Skift nodetype"
|
||||
|
||||
#. js-lingui-id: GptGxg
|
||||
#: src/modules/settings/accounts/components/SettingsAccountsPasswordController.tsx
|
||||
msgid "Change password"
|
||||
msgstr ""
|
||||
|
||||
#. js-lingui-id: VhMDMg
|
||||
#: src/pages/auth/PasswordReset.tsx
|
||||
#: src/modules/settings/profile/components/SetOrChangePassword.tsx
|
||||
@@ -3467,6 +3460,11 @@ msgstr "Luk"
|
||||
msgid "Close banner"
|
||||
msgstr "Luk banner"
|
||||
|
||||
#. js-lingui-id: vcpc5o
|
||||
#: src/modules/blocknote-editor/components/BlockEditor.tsx
|
||||
msgid "Close menu"
|
||||
msgstr ""
|
||||
|
||||
#. js-lingui-id: saip/v
|
||||
#: src/modules/side-panel/components/SidePanelToggleButton.tsx
|
||||
msgid "Close side panel"
|
||||
@@ -13178,6 +13176,11 @@ msgstr "Rollenavn"
|
||||
msgid "Role name cannot be empty"
|
||||
msgstr "Rollens navn kan ikke være tomt"
|
||||
|
||||
#. js-lingui-id: VMX1Oy
|
||||
#: src/modules/metadata-error-handler/hooks/useMetadataErrorHandler.ts
|
||||
msgid "role permission flag"
|
||||
msgstr ""
|
||||
|
||||
#. js-lingui-id: jLNGne
|
||||
#: src/modules/metadata-error-handler/hooks/useMetadataErrorHandler.ts
|
||||
msgid "role target"
|
||||
|
||||
@@ -533,13 +533,6 @@ msgstr "{workspaceMemberName} wird von folgender Rolle abgezogen:"
|
||||
msgid "#"
|
||||
msgstr ""
|
||||
|
||||
#. js-lingui-id: CwstSL
|
||||
#: src/modules/settings/accounts/components/SettingsAccountsConnectionForm.tsx
|
||||
#: src/modules/settings/accounts/components/SettingsAccountsConnectionForm.tsx
|
||||
#: src/modules/settings/accounts/components/SettingsAccountsConnectionForm.tsx
|
||||
msgid "••••••••"
|
||||
msgstr "••••••••"
|
||||
|
||||
#. js-lingui-id: n9nj+X
|
||||
#. placeholder {0}: aiChatThreadPendingDelete?.threadTitle ?? ''
|
||||
#: src/modules/ai/components/AiChatThreadDeleteConfirmationModal.tsx
|
||||
@@ -2652,11 +2645,6 @@ msgstr "Zurück zu {linkText}"
|
||||
msgid "Back to content"
|
||||
msgstr "Zurück zum Inhalt"
|
||||
|
||||
#. js-lingui-id: OvBnOM
|
||||
#: src/modules/ui/navigation/bread-crumb/components/MobileBreadcrumb.tsx
|
||||
msgid "Back to Settings"
|
||||
msgstr "Zurück zu den Einstellungen"
|
||||
|
||||
#. js-lingui-id: GtJbUa
|
||||
#: src/modules/error-handler/components/AppRootErrorFallback.tsx
|
||||
msgid "Background"
|
||||
@@ -3184,6 +3172,11 @@ msgstr "E-Mail ändern"
|
||||
msgid "Change node type"
|
||||
msgstr "Knotentyp ändern"
|
||||
|
||||
#. js-lingui-id: GptGxg
|
||||
#: src/modules/settings/accounts/components/SettingsAccountsPasswordController.tsx
|
||||
msgid "Change password"
|
||||
msgstr ""
|
||||
|
||||
#. js-lingui-id: VhMDMg
|
||||
#: src/pages/auth/PasswordReset.tsx
|
||||
#: src/modules/settings/profile/components/SetOrChangePassword.tsx
|
||||
@@ -3467,6 +3460,11 @@ msgstr "Schließen"
|
||||
msgid "Close banner"
|
||||
msgstr "Banner schließen"
|
||||
|
||||
#. js-lingui-id: vcpc5o
|
||||
#: src/modules/blocknote-editor/components/BlockEditor.tsx
|
||||
msgid "Close menu"
|
||||
msgstr ""
|
||||
|
||||
#. js-lingui-id: saip/v
|
||||
#: src/modules/side-panel/components/SidePanelToggleButton.tsx
|
||||
msgid "Close side panel"
|
||||
@@ -13178,6 +13176,11 @@ msgstr "Rollenname"
|
||||
msgid "Role name cannot be empty"
|
||||
msgstr "Rollenname darf nicht leer sein"
|
||||
|
||||
#. js-lingui-id: VMX1Oy
|
||||
#: src/modules/metadata-error-handler/hooks/useMetadataErrorHandler.ts
|
||||
msgid "role permission flag"
|
||||
msgstr ""
|
||||
|
||||
#. js-lingui-id: jLNGne
|
||||
#: src/modules/metadata-error-handler/hooks/useMetadataErrorHandler.ts
|
||||
msgid "role target"
|
||||
|
||||
@@ -533,13 +533,6 @@ msgstr "Ο χρήστης {workspaceMemberName} θα αποδεσμευτεί α
|
||||
msgid "#"
|
||||
msgstr ""
|
||||
|
||||
#. js-lingui-id: CwstSL
|
||||
#: src/modules/settings/accounts/components/SettingsAccountsConnectionForm.tsx
|
||||
#: src/modules/settings/accounts/components/SettingsAccountsConnectionForm.tsx
|
||||
#: src/modules/settings/accounts/components/SettingsAccountsConnectionForm.tsx
|
||||
msgid "••••••••"
|
||||
msgstr "••••••••"
|
||||
|
||||
#. js-lingui-id: n9nj+X
|
||||
#. placeholder {0}: aiChatThreadPendingDelete?.threadTitle ?? ''
|
||||
#: src/modules/ai/components/AiChatThreadDeleteConfirmationModal.tsx
|
||||
@@ -2652,11 +2645,6 @@ msgstr "Πίσω στο {linkText}"
|
||||
msgid "Back to content"
|
||||
msgstr "Επιστροφή στο περιεχόμενο"
|
||||
|
||||
#. js-lingui-id: OvBnOM
|
||||
#: src/modules/ui/navigation/bread-crumb/components/MobileBreadcrumb.tsx
|
||||
msgid "Back to Settings"
|
||||
msgstr "Πίσω στις Ρυθμίσεις"
|
||||
|
||||
#. js-lingui-id: GtJbUa
|
||||
#: src/modules/error-handler/components/AppRootErrorFallback.tsx
|
||||
msgid "Background"
|
||||
@@ -3184,6 +3172,11 @@ msgstr "Αλλαγή email"
|
||||
msgid "Change node type"
|
||||
msgstr "Αλλαγή τύπου κόμβου"
|
||||
|
||||
#. js-lingui-id: GptGxg
|
||||
#: src/modules/settings/accounts/components/SettingsAccountsPasswordController.tsx
|
||||
msgid "Change password"
|
||||
msgstr ""
|
||||
|
||||
#. js-lingui-id: VhMDMg
|
||||
#: src/pages/auth/PasswordReset.tsx
|
||||
#: src/modules/settings/profile/components/SetOrChangePassword.tsx
|
||||
@@ -3467,6 +3460,11 @@ msgstr "Κλείσιμο"
|
||||
msgid "Close banner"
|
||||
msgstr "Κλείσιμο banner"
|
||||
|
||||
#. js-lingui-id: vcpc5o
|
||||
#: src/modules/blocknote-editor/components/BlockEditor.tsx
|
||||
msgid "Close menu"
|
||||
msgstr ""
|
||||
|
||||
#. js-lingui-id: saip/v
|
||||
#: src/modules/side-panel/components/SidePanelToggleButton.tsx
|
||||
msgid "Close side panel"
|
||||
@@ -13178,6 +13176,11 @@ msgstr "Όνομα ρόλου"
|
||||
msgid "Role name cannot be empty"
|
||||
msgstr "Το όνομα του ρόλου δεν μπορεί να είναι κενό"
|
||||
|
||||
#. js-lingui-id: VMX1Oy
|
||||
#: src/modules/metadata-error-handler/hooks/useMetadataErrorHandler.ts
|
||||
msgid "role permission flag"
|
||||
msgstr ""
|
||||
|
||||
#. js-lingui-id: jLNGne
|
||||
#: src/modules/metadata-error-handler/hooks/useMetadataErrorHandler.ts
|
||||
msgid "role target"
|
||||
|
||||
@@ -528,13 +528,6 @@ msgstr "{workspaceMemberName} will be unassigned from the following role:"
|
||||
msgid "#"
|
||||
msgstr "#"
|
||||
|
||||
#. js-lingui-id: CwstSL
|
||||
#: src/modules/settings/accounts/components/SettingsAccountsConnectionForm.tsx
|
||||
#: src/modules/settings/accounts/components/SettingsAccountsConnectionForm.tsx
|
||||
#: src/modules/settings/accounts/components/SettingsAccountsConnectionForm.tsx
|
||||
msgid "••••••••"
|
||||
msgstr "••••••••"
|
||||
|
||||
#. js-lingui-id: n9nj+X
|
||||
#. placeholder {0}: aiChatThreadPendingDelete?.threadTitle ?? ''
|
||||
#: src/modules/ai/components/AiChatThreadDeleteConfirmationModal.tsx
|
||||
@@ -2647,11 +2640,6 @@ msgstr "Back to {linkText}"
|
||||
msgid "Back to content"
|
||||
msgstr "Back to content"
|
||||
|
||||
#. js-lingui-id: OvBnOM
|
||||
#: src/modules/ui/navigation/bread-crumb/components/MobileBreadcrumb.tsx
|
||||
msgid "Back to Settings"
|
||||
msgstr "Back to Settings"
|
||||
|
||||
#. js-lingui-id: GtJbUa
|
||||
#: src/modules/error-handler/components/AppRootErrorFallback.tsx
|
||||
msgid "Background"
|
||||
@@ -3179,6 +3167,11 @@ msgstr "Change email"
|
||||
msgid "Change node type"
|
||||
msgstr "Change node type"
|
||||
|
||||
#. js-lingui-id: GptGxg
|
||||
#: src/modules/settings/accounts/components/SettingsAccountsPasswordController.tsx
|
||||
msgid "Change password"
|
||||
msgstr "Change password"
|
||||
|
||||
#. js-lingui-id: VhMDMg
|
||||
#: src/pages/auth/PasswordReset.tsx
|
||||
#: src/modules/settings/profile/components/SetOrChangePassword.tsx
|
||||
@@ -3462,6 +3455,11 @@ msgstr "Close"
|
||||
msgid "Close banner"
|
||||
msgstr "Close banner"
|
||||
|
||||
#. js-lingui-id: vcpc5o
|
||||
#: src/modules/blocknote-editor/components/BlockEditor.tsx
|
||||
msgid "Close menu"
|
||||
msgstr "Close menu"
|
||||
|
||||
#. js-lingui-id: saip/v
|
||||
#: src/modules/side-panel/components/SidePanelToggleButton.tsx
|
||||
msgid "Close side panel"
|
||||
@@ -13196,6 +13194,11 @@ msgstr "Role name"
|
||||
msgid "Role name cannot be empty"
|
||||
msgstr "Role name cannot be empty"
|
||||
|
||||
#. js-lingui-id: VMX1Oy
|
||||
#: src/modules/metadata-error-handler/hooks/useMetadataErrorHandler.ts
|
||||
msgid "role permission flag"
|
||||
msgstr "role permission flag"
|
||||
|
||||
#. js-lingui-id: jLNGne
|
||||
#: src/modules/metadata-error-handler/hooks/useMetadataErrorHandler.ts
|
||||
msgid "role target"
|
||||
|
||||
@@ -533,13 +533,6 @@ msgstr "{workspaceMemberName} dejará de estar asignado al siguiente rol:"
|
||||
msgid "#"
|
||||
msgstr ""
|
||||
|
||||
#. js-lingui-id: CwstSL
|
||||
#: src/modules/settings/accounts/components/SettingsAccountsConnectionForm.tsx
|
||||
#: src/modules/settings/accounts/components/SettingsAccountsConnectionForm.tsx
|
||||
#: src/modules/settings/accounts/components/SettingsAccountsConnectionForm.tsx
|
||||
msgid "••••••••"
|
||||
msgstr "••••••••"
|
||||
|
||||
#. js-lingui-id: n9nj+X
|
||||
#. placeholder {0}: aiChatThreadPendingDelete?.threadTitle ?? ''
|
||||
#: src/modules/ai/components/AiChatThreadDeleteConfirmationModal.tsx
|
||||
@@ -2652,11 +2645,6 @@ msgstr "Volver a {linkText}"
|
||||
msgid "Back to content"
|
||||
msgstr "Volver al contenido"
|
||||
|
||||
#. js-lingui-id: OvBnOM
|
||||
#: src/modules/ui/navigation/bread-crumb/components/MobileBreadcrumb.tsx
|
||||
msgid "Back to Settings"
|
||||
msgstr "Volver a Configuración"
|
||||
|
||||
#. js-lingui-id: GtJbUa
|
||||
#: src/modules/error-handler/components/AppRootErrorFallback.tsx
|
||||
msgid "Background"
|
||||
@@ -3184,6 +3172,11 @@ msgstr "Cambiar correo electrónico"
|
||||
msgid "Change node type"
|
||||
msgstr "Cambiar tipo de nodo"
|
||||
|
||||
#. js-lingui-id: GptGxg
|
||||
#: src/modules/settings/accounts/components/SettingsAccountsPasswordController.tsx
|
||||
msgid "Change password"
|
||||
msgstr ""
|
||||
|
||||
#. js-lingui-id: VhMDMg
|
||||
#: src/pages/auth/PasswordReset.tsx
|
||||
#: src/modules/settings/profile/components/SetOrChangePassword.tsx
|
||||
@@ -3467,6 +3460,11 @@ msgstr "Cerrar"
|
||||
msgid "Close banner"
|
||||
msgstr "Cerrar banner"
|
||||
|
||||
#. js-lingui-id: vcpc5o
|
||||
#: src/modules/blocknote-editor/components/BlockEditor.tsx
|
||||
msgid "Close menu"
|
||||
msgstr ""
|
||||
|
||||
#. js-lingui-id: saip/v
|
||||
#: src/modules/side-panel/components/SidePanelToggleButton.tsx
|
||||
msgid "Close side panel"
|
||||
@@ -13178,6 +13176,11 @@ msgstr "Nombre del rol"
|
||||
msgid "Role name cannot be empty"
|
||||
msgstr "El nombre del rol no puede estar vacío"
|
||||
|
||||
#. js-lingui-id: VMX1Oy
|
||||
#: src/modules/metadata-error-handler/hooks/useMetadataErrorHandler.ts
|
||||
msgid "role permission flag"
|
||||
msgstr ""
|
||||
|
||||
#. js-lingui-id: jLNGne
|
||||
#: src/modules/metadata-error-handler/hooks/useMetadataErrorHandler.ts
|
||||
msgid "role target"
|
||||
|
||||
@@ -533,13 +533,6 @@ msgstr "{workspaceMemberName} poistetaan seuraavasta roolista:"
|
||||
msgid "#"
|
||||
msgstr ""
|
||||
|
||||
#. js-lingui-id: CwstSL
|
||||
#: src/modules/settings/accounts/components/SettingsAccountsConnectionForm.tsx
|
||||
#: src/modules/settings/accounts/components/SettingsAccountsConnectionForm.tsx
|
||||
#: src/modules/settings/accounts/components/SettingsAccountsConnectionForm.tsx
|
||||
msgid "••••••••"
|
||||
msgstr "••••••••"
|
||||
|
||||
#. js-lingui-id: n9nj+X
|
||||
#. placeholder {0}: aiChatThreadPendingDelete?.threadTitle ?? ''
|
||||
#: src/modules/ai/components/AiChatThreadDeleteConfirmationModal.tsx
|
||||
@@ -2652,11 +2645,6 @@ msgstr "Takaisin kohteeseen {linkText}"
|
||||
msgid "Back to content"
|
||||
msgstr "Takaisin sisältöön"
|
||||
|
||||
#. js-lingui-id: OvBnOM
|
||||
#: src/modules/ui/navigation/bread-crumb/components/MobileBreadcrumb.tsx
|
||||
msgid "Back to Settings"
|
||||
msgstr "Takaisin asetuksiin"
|
||||
|
||||
#. js-lingui-id: GtJbUa
|
||||
#: src/modules/error-handler/components/AppRootErrorFallback.tsx
|
||||
msgid "Background"
|
||||
@@ -3184,6 +3172,11 @@ msgstr "Vaihda sähköpostia"
|
||||
msgid "Change node type"
|
||||
msgstr "Vaihda solmun tyyppi"
|
||||
|
||||
#. js-lingui-id: GptGxg
|
||||
#: src/modules/settings/accounts/components/SettingsAccountsPasswordController.tsx
|
||||
msgid "Change password"
|
||||
msgstr ""
|
||||
|
||||
#. js-lingui-id: VhMDMg
|
||||
#: src/pages/auth/PasswordReset.tsx
|
||||
#: src/modules/settings/profile/components/SetOrChangePassword.tsx
|
||||
@@ -3467,6 +3460,11 @@ msgstr "Sulje"
|
||||
msgid "Close banner"
|
||||
msgstr "Sulje banneri"
|
||||
|
||||
#. js-lingui-id: vcpc5o
|
||||
#: src/modules/blocknote-editor/components/BlockEditor.tsx
|
||||
msgid "Close menu"
|
||||
msgstr ""
|
||||
|
||||
#. js-lingui-id: saip/v
|
||||
#: src/modules/side-panel/components/SidePanelToggleButton.tsx
|
||||
msgid "Close side panel"
|
||||
@@ -13178,6 +13176,11 @@ msgstr "Roolin nimi"
|
||||
msgid "Role name cannot be empty"
|
||||
msgstr "Roolinimi ei saa olla tyhjä"
|
||||
|
||||
#. js-lingui-id: VMX1Oy
|
||||
#: src/modules/metadata-error-handler/hooks/useMetadataErrorHandler.ts
|
||||
msgid "role permission flag"
|
||||
msgstr ""
|
||||
|
||||
#. js-lingui-id: jLNGne
|
||||
#: src/modules/metadata-error-handler/hooks/useMetadataErrorHandler.ts
|
||||
msgid "role target"
|
||||
|
||||
@@ -533,13 +533,6 @@ msgstr "{workspaceMemberName} sera retiré du rôle suivant :"
|
||||
msgid "#"
|
||||
msgstr ""
|
||||
|
||||
#. js-lingui-id: CwstSL
|
||||
#: src/modules/settings/accounts/components/SettingsAccountsConnectionForm.tsx
|
||||
#: src/modules/settings/accounts/components/SettingsAccountsConnectionForm.tsx
|
||||
#: src/modules/settings/accounts/components/SettingsAccountsConnectionForm.tsx
|
||||
msgid "••••••••"
|
||||
msgstr "••••••••"
|
||||
|
||||
#. js-lingui-id: n9nj+X
|
||||
#. placeholder {0}: aiChatThreadPendingDelete?.threadTitle ?? ''
|
||||
#: src/modules/ai/components/AiChatThreadDeleteConfirmationModal.tsx
|
||||
@@ -2652,11 +2645,6 @@ msgstr "Retour à {linkText}"
|
||||
msgid "Back to content"
|
||||
msgstr "Retour au contenu"
|
||||
|
||||
#. js-lingui-id: OvBnOM
|
||||
#: src/modules/ui/navigation/bread-crumb/components/MobileBreadcrumb.tsx
|
||||
msgid "Back to Settings"
|
||||
msgstr "Retour aux paramètres"
|
||||
|
||||
#. js-lingui-id: GtJbUa
|
||||
#: src/modules/error-handler/components/AppRootErrorFallback.tsx
|
||||
msgid "Background"
|
||||
@@ -3184,6 +3172,11 @@ msgstr "Changer l'adresse e-mail"
|
||||
msgid "Change node type"
|
||||
msgstr "Changer le type de nœud"
|
||||
|
||||
#. js-lingui-id: GptGxg
|
||||
#: src/modules/settings/accounts/components/SettingsAccountsPasswordController.tsx
|
||||
msgid "Change password"
|
||||
msgstr ""
|
||||
|
||||
#. js-lingui-id: VhMDMg
|
||||
#: src/pages/auth/PasswordReset.tsx
|
||||
#: src/modules/settings/profile/components/SetOrChangePassword.tsx
|
||||
@@ -3467,6 +3460,11 @@ msgstr "Fermer"
|
||||
msgid "Close banner"
|
||||
msgstr "Fermer la bannière"
|
||||
|
||||
#. js-lingui-id: vcpc5o
|
||||
#: src/modules/blocknote-editor/components/BlockEditor.tsx
|
||||
msgid "Close menu"
|
||||
msgstr ""
|
||||
|
||||
#. js-lingui-id: saip/v
|
||||
#: src/modules/side-panel/components/SidePanelToggleButton.tsx
|
||||
msgid "Close side panel"
|
||||
@@ -13178,6 +13176,11 @@ msgstr "Nom du rôle"
|
||||
msgid "Role name cannot be empty"
|
||||
msgstr "Le nom du rôle ne peut pas être vide"
|
||||
|
||||
#. js-lingui-id: VMX1Oy
|
||||
#: src/modules/metadata-error-handler/hooks/useMetadataErrorHandler.ts
|
||||
msgid "role permission flag"
|
||||
msgstr ""
|
||||
|
||||
#. js-lingui-id: jLNGne
|
||||
#: src/modules/metadata-error-handler/hooks/useMetadataErrorHandler.ts
|
||||
msgid "role target"
|
||||
|
||||
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user