Compare commits
22
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
28f836200b | ||
|
|
db4a05301a | ||
|
|
f9e3683518 | ||
|
|
fecea1bfae | ||
|
|
3281d37bdf | ||
|
|
291ce5ccdb | ||
|
|
a9634b027e | ||
|
|
ae41751a8c | ||
|
|
11d8679f65 | ||
|
|
05f31c1837 | ||
|
|
2a92f34d06 | ||
|
|
d9b125efc5 | ||
|
|
e6b7f31fea | ||
|
|
bad1f20012 | ||
|
|
ce8ef261c1 | ||
|
|
1d3d3999e2 | ||
|
|
89579f5225 | ||
|
|
132d997474 | ||
|
|
7c252ff233 | ||
|
|
4ba9c0ca0b | ||
|
|
d03480472c | ||
|
|
8f3c336e62 |
@@ -3263,7 +3263,7 @@ type Mutation {
|
||||
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!
|
||||
@@ -4228,6 +4228,11 @@ input UpdateWebhookInputUpdates {
|
||||
secret: String
|
||||
}
|
||||
|
||||
input FileAttachmentInput {
|
||||
id: UUID!
|
||||
filename: String!
|
||||
}
|
||||
|
||||
input CreateSkillInput {
|
||||
id: UUID
|
||||
name: String!
|
||||
|
||||
@@ -5872,7 +5872,7 @@ export interface MutationGenqlSelection{
|
||||
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']} })
|
||||
@@ -6259,6 +6259,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)}
|
||||
|
||||
@@ -78,8 +78,8 @@ export default {
|
||||
335,
|
||||
342,
|
||||
378,
|
||||
454,
|
||||
466
|
||||
455,
|
||||
467
|
||||
],
|
||||
"types": {
|
||||
"BillingProductDTO": {
|
||||
@@ -7876,9 +7876,9 @@ export default {
|
||||
"modelId": [
|
||||
1
|
||||
],
|
||||
"fileIds": [
|
||||
3,
|
||||
"[UUID!]"
|
||||
"fileAttachments": [
|
||||
440,
|
||||
"[FileAttachmentInput!]"
|
||||
]
|
||||
}
|
||||
],
|
||||
@@ -7944,7 +7944,7 @@ export default {
|
||||
290,
|
||||
{
|
||||
"input": [
|
||||
440,
|
||||
441,
|
||||
"CreateSkillInput!"
|
||||
]
|
||||
}
|
||||
@@ -7953,7 +7953,7 @@ export default {
|
||||
290,
|
||||
{
|
||||
"input": [
|
||||
441,
|
||||
442,
|
||||
"UpdateSkillInput!"
|
||||
]
|
||||
}
|
||||
@@ -8011,7 +8011,7 @@ export default {
|
||||
238,
|
||||
{
|
||||
"input": [
|
||||
442,
|
||||
443,
|
||||
"GetAuthorizationUrlForSSOInput!"
|
||||
]
|
||||
}
|
||||
@@ -8265,7 +8265,7 @@ export default {
|
||||
196,
|
||||
{
|
||||
"input": [
|
||||
443,
|
||||
444,
|
||||
"CreateApplicationRegistrationInput!"
|
||||
]
|
||||
}
|
||||
@@ -8274,7 +8274,7 @@ export default {
|
||||
7,
|
||||
{
|
||||
"input": [
|
||||
444,
|
||||
445,
|
||||
"UpdateApplicationRegistrationInput!"
|
||||
]
|
||||
}
|
||||
@@ -8301,7 +8301,7 @@ export default {
|
||||
5,
|
||||
{
|
||||
"input": [
|
||||
446,
|
||||
447,
|
||||
"CreateApplicationRegistrationVariableInput!"
|
||||
]
|
||||
}
|
||||
@@ -8310,7 +8310,7 @@ export default {
|
||||
5,
|
||||
{
|
||||
"input": [
|
||||
447,
|
||||
448,
|
||||
"UpdateApplicationRegistrationVariableInput!"
|
||||
]
|
||||
}
|
||||
@@ -8399,7 +8399,7 @@ export default {
|
||||
6,
|
||||
{
|
||||
"input": [
|
||||
449,
|
||||
450,
|
||||
"UpdateWorkspaceMemberSettingsInput!"
|
||||
]
|
||||
}
|
||||
@@ -8433,7 +8433,7 @@ export default {
|
||||
75,
|
||||
{
|
||||
"data": [
|
||||
450,
|
||||
451,
|
||||
"ActivateWorkspaceInput!"
|
||||
]
|
||||
}
|
||||
@@ -8442,7 +8442,7 @@ export default {
|
||||
75,
|
||||
{
|
||||
"data": [
|
||||
451,
|
||||
452,
|
||||
"UpdateWorkspaceInput!"
|
||||
]
|
||||
}
|
||||
@@ -8469,7 +8469,7 @@ export default {
|
||||
6,
|
||||
{
|
||||
"workspaceMigration": [
|
||||
452,
|
||||
453,
|
||||
"WorkspaceMigrationInput!"
|
||||
]
|
||||
}
|
||||
@@ -8487,7 +8487,7 @@ export default {
|
||||
220,
|
||||
{
|
||||
"input": [
|
||||
455,
|
||||
456,
|
||||
"SetupOIDCSsoInput!"
|
||||
]
|
||||
}
|
||||
@@ -8496,7 +8496,7 @@ export default {
|
||||
220,
|
||||
{
|
||||
"input": [
|
||||
456,
|
||||
457,
|
||||
"SetupSAMLSsoInput!"
|
||||
]
|
||||
}
|
||||
@@ -8505,7 +8505,7 @@ export default {
|
||||
216,
|
||||
{
|
||||
"input": [
|
||||
457,
|
||||
458,
|
||||
"DeleteSsoInput!"
|
||||
]
|
||||
}
|
||||
@@ -8514,7 +8514,7 @@ export default {
|
||||
217,
|
||||
{
|
||||
"input": [
|
||||
458,
|
||||
459,
|
||||
"EditSsoInput!"
|
||||
]
|
||||
}
|
||||
@@ -8545,7 +8545,7 @@ export default {
|
||||
286,
|
||||
{
|
||||
"input": [
|
||||
459,
|
||||
460,
|
||||
"SendEmailInput!"
|
||||
]
|
||||
}
|
||||
@@ -8571,7 +8571,7 @@ export default {
|
||||
"String!"
|
||||
],
|
||||
"connectionParameters": [
|
||||
461,
|
||||
462,
|
||||
"EmailAccountConnectionParameters!"
|
||||
],
|
||||
"id": [
|
||||
@@ -8583,7 +8583,7 @@ export default {
|
||||
168,
|
||||
{
|
||||
"input": [
|
||||
463,
|
||||
464,
|
||||
"UpdateLabPublicFeatureFlagInput!"
|
||||
]
|
||||
}
|
||||
@@ -8665,7 +8665,7 @@ export default {
|
||||
77,
|
||||
{
|
||||
"input": [
|
||||
464,
|
||||
465,
|
||||
"CreateOneAppTokenInput!"
|
||||
]
|
||||
}
|
||||
@@ -8728,7 +8728,7 @@ export default {
|
||||
"String!"
|
||||
],
|
||||
"fileFolder": [
|
||||
466,
|
||||
467,
|
||||
"FileFolder!"
|
||||
],
|
||||
"filePath": [
|
||||
@@ -10746,6 +10746,17 @@ export default {
|
||||
1
|
||||
]
|
||||
},
|
||||
"FileAttachmentInput": {
|
||||
"id": [
|
||||
3
|
||||
],
|
||||
"filename": [
|
||||
1
|
||||
],
|
||||
"__typename": [
|
||||
1
|
||||
]
|
||||
},
|
||||
"CreateSkillInput": {
|
||||
"id": [
|
||||
3
|
||||
@@ -10828,7 +10839,7 @@ export default {
|
||||
1
|
||||
],
|
||||
"update": [
|
||||
445
|
||||
446
|
||||
],
|
||||
"__typename": [
|
||||
1
|
||||
@@ -10876,7 +10887,7 @@ export default {
|
||||
1
|
||||
],
|
||||
"update": [
|
||||
448
|
||||
449
|
||||
],
|
||||
"__typename": [
|
||||
1
|
||||
@@ -10994,7 +11005,7 @@ export default {
|
||||
},
|
||||
"WorkspaceMigrationInput": {
|
||||
"actions": [
|
||||
453
|
||||
454
|
||||
],
|
||||
"__typename": [
|
||||
1
|
||||
@@ -11002,7 +11013,7 @@ export default {
|
||||
},
|
||||
"WorkspaceMigrationDeleteActionInput": {
|
||||
"type": [
|
||||
454
|
||||
455
|
||||
],
|
||||
"metadataName": [
|
||||
318
|
||||
@@ -11097,7 +11108,7 @@ export default {
|
||||
1
|
||||
],
|
||||
"files": [
|
||||
460
|
||||
461
|
||||
],
|
||||
"__typename": [
|
||||
1
|
||||
@@ -11116,13 +11127,13 @@ export default {
|
||||
},
|
||||
"EmailAccountConnectionParameters": {
|
||||
"IMAP": [
|
||||
462
|
||||
463
|
||||
],
|
||||
"SMTP": [
|
||||
462
|
||||
463
|
||||
],
|
||||
"CALDAV": [
|
||||
462
|
||||
463
|
||||
],
|
||||
"__typename": [
|
||||
1
|
||||
@@ -11161,7 +11172,7 @@ export default {
|
||||
},
|
||||
"CreateOneAppTokenInput": {
|
||||
"appToken": [
|
||||
465
|
||||
466
|
||||
],
|
||||
"__typename": [
|
||||
1
|
||||
@@ -11190,7 +11201,7 @@ export default {
|
||||
230,
|
||||
{
|
||||
"input": [
|
||||
468,
|
||||
469,
|
||||
"LogicFunctionLogsInput!"
|
||||
]
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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` 命令可用于查看您的实例和工作区迁移的当前状态。 这在调试升级问题或提交支持请求时非常有用。
|
||||
|
||||
@@ -1792,6 +1792,11 @@ export type File = {
|
||||
size: Scalars['Float'];
|
||||
};
|
||||
|
||||
export type FileAttachmentInput = {
|
||||
filename: Scalars['String'];
|
||||
id: Scalars['UUID'];
|
||||
};
|
||||
|
||||
export enum FileFolder {
|
||||
AgentChat = 'AgentChat',
|
||||
AppTarball = 'AppTarball',
|
||||
@@ -3197,7 +3202,7 @@ export type MutationSaveImapSmtpCaldavAccountArgs = {
|
||||
|
||||
export type MutationSendChatMessageArgs = {
|
||||
browsingContext?: InputMaybe<Scalars['JSON']>;
|
||||
fileIds?: InputMaybe<Array<Scalars['UUID']>>;
|
||||
fileAttachments?: InputMaybe<Array<FileAttachmentInput>>;
|
||||
messageId: Scalars['UUID'];
|
||||
modelId?: InputMaybe<Scalars['String']>;
|
||||
text: Scalars['String'];
|
||||
@@ -6148,7 +6153,7 @@ export type SendChatMessageMutationVariables = Exact<{
|
||||
messageId: Scalars['UUID'];
|
||||
browsingContext?: InputMaybe<Scalars['JSON']>;
|
||||
modelId?: InputMaybe<Scalars['String']>;
|
||||
fileIds?: InputMaybe<Array<Scalars['UUID']> | Scalars['UUID']>;
|
||||
fileAttachments?: InputMaybe<Array<FileAttachmentInput> | FileAttachmentInput>;
|
||||
}>;
|
||||
|
||||
|
||||
@@ -7983,7 +7988,7 @@ export const EvaluateAgentTurnDocument = {"kind":"Document","definitions":[{"kin
|
||||
export const RemoveRoleFromAgentDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"mutation","name":{"kind":"Name","value":"RemoveRoleFromAgent"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"agentId"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"UUID"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"removeRoleFromAgent"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"agentId"},"value":{"kind":"Variable","name":{"kind":"Name","value":"agentId"}}}]}]}}]} as unknown as DocumentNode<RemoveRoleFromAgentMutation, RemoveRoleFromAgentMutationVariables>;
|
||||
export const RenameChatThreadDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"mutation","name":{"kind":"Name","value":"RenameChatThread"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"id"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"UUID"}}}},{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"title"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"String"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"renameChatThread"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"id"},"value":{"kind":"Variable","name":{"kind":"Name","value":"id"}}},{"kind":"Argument","name":{"kind":"Name","value":"title"},"value":{"kind":"Variable","name":{"kind":"Name","value":"title"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"title"}},{"kind":"Field","name":{"kind":"Name","value":"updatedAt"}}]}}]}}]} as unknown as DocumentNode<RenameChatThreadMutation, RenameChatThreadMutationVariables>;
|
||||
export const RunEvaluationInputDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"mutation","name":{"kind":"Name","value":"RunEvaluationInput"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"agentId"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"UUID"}}}},{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"input"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"String"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"runEvaluationInput"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"agentId"},"value":{"kind":"Variable","name":{"kind":"Name","value":"agentId"}}},{"kind":"Argument","name":{"kind":"Name","value":"input"},"value":{"kind":"Variable","name":{"kind":"Name","value":"input"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"threadId"}},{"kind":"Field","name":{"kind":"Name","value":"agentId"}},{"kind":"Field","name":{"kind":"Name","value":"createdAt"}},{"kind":"Field","name":{"kind":"Name","value":"evaluations"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"score"}},{"kind":"Field","name":{"kind":"Name","value":"comment"}},{"kind":"Field","name":{"kind":"Name","value":"createdAt"}}]}}]}}]}}]} as unknown as DocumentNode<RunEvaluationInputMutation, RunEvaluationInputMutationVariables>;
|
||||
export const SendChatMessageDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"mutation","name":{"kind":"Name","value":"SendChatMessage"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"threadId"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"UUID"}}}},{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"text"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"String"}}}},{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"messageId"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"UUID"}}}},{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"browsingContext"}},"type":{"kind":"NamedType","name":{"kind":"Name","value":"JSON"}}},{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"modelId"}},"type":{"kind":"NamedType","name":{"kind":"Name","value":"String"}}},{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"fileIds"}},"type":{"kind":"ListType","type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"UUID"}}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"sendChatMessage"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"threadId"},"value":{"kind":"Variable","name":{"kind":"Name","value":"threadId"}}},{"kind":"Argument","name":{"kind":"Name","value":"text"},"value":{"kind":"Variable","name":{"kind":"Name","value":"text"}}},{"kind":"Argument","name":{"kind":"Name","value":"messageId"},"value":{"kind":"Variable","name":{"kind":"Name","value":"messageId"}}},{"kind":"Argument","name":{"kind":"Name","value":"browsingContext"},"value":{"kind":"Variable","name":{"kind":"Name","value":"browsingContext"}}},{"kind":"Argument","name":{"kind":"Name","value":"modelId"},"value":{"kind":"Variable","name":{"kind":"Name","value":"modelId"}}},{"kind":"Argument","name":{"kind":"Name","value":"fileIds"},"value":{"kind":"Variable","name":{"kind":"Name","value":"fileIds"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"messageId"}},{"kind":"Field","name":{"kind":"Name","value":"queued"}},{"kind":"Field","name":{"kind":"Name","value":"streamId"}}]}}]}}]} as unknown as DocumentNode<SendChatMessageMutation, SendChatMessageMutationVariables>;
|
||||
export const SendChatMessageDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"mutation","name":{"kind":"Name","value":"SendChatMessage"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"threadId"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"UUID"}}}},{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"text"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"String"}}}},{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"messageId"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"UUID"}}}},{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"browsingContext"}},"type":{"kind":"NamedType","name":{"kind":"Name","value":"JSON"}}},{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"modelId"}},"type":{"kind":"NamedType","name":{"kind":"Name","value":"String"}}},{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"fileAttachments"}},"type":{"kind":"ListType","type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"FileAttachmentInput"}}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"sendChatMessage"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"threadId"},"value":{"kind":"Variable","name":{"kind":"Name","value":"threadId"}}},{"kind":"Argument","name":{"kind":"Name","value":"text"},"value":{"kind":"Variable","name":{"kind":"Name","value":"text"}}},{"kind":"Argument","name":{"kind":"Name","value":"messageId"},"value":{"kind":"Variable","name":{"kind":"Name","value":"messageId"}}},{"kind":"Argument","name":{"kind":"Name","value":"browsingContext"},"value":{"kind":"Variable","name":{"kind":"Name","value":"browsingContext"}}},{"kind":"Argument","name":{"kind":"Name","value":"modelId"},"value":{"kind":"Variable","name":{"kind":"Name","value":"modelId"}}},{"kind":"Argument","name":{"kind":"Name","value":"fileAttachments"},"value":{"kind":"Variable","name":{"kind":"Name","value":"fileAttachments"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"messageId"}},{"kind":"Field","name":{"kind":"Name","value":"queued"}},{"kind":"Field","name":{"kind":"Name","value":"streamId"}}]}}]}}]} as unknown as DocumentNode<SendChatMessageMutation, SendChatMessageMutationVariables>;
|
||||
export const StopAgentChatStreamDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"mutation","name":{"kind":"Name","value":"StopAgentChatStream"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"threadId"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"UUID"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"stopAgentChatStream"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"threadId"},"value":{"kind":"Variable","name":{"kind":"Name","value":"threadId"}}}]}]}}]} as unknown as DocumentNode<StopAgentChatStreamMutation, StopAgentChatStreamMutationVariables>;
|
||||
export const UnarchiveChatThreadDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"mutation","name":{"kind":"Name","value":"UnarchiveChatThread"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"id"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"UUID"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"unarchiveChatThread"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"id"},"value":{"kind":"Variable","name":{"kind":"Name","value":"id"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"deletedAt"}},{"kind":"Field","name":{"kind":"Name","value":"updatedAt"}}]}}]}}]} as unknown as DocumentNode<UnarchiveChatThreadMutation, UnarchiveChatThreadMutationVariables>;
|
||||
export const UpdateOneAgentDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"mutation","name":{"kind":"Name","value":"UpdateOneAgent"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"input"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"UpdateAgentInput"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"updateOneAgent"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"input"},"value":{"kind":"Variable","name":{"kind":"Name","value":"input"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"FragmentSpread","name":{"kind":"Name","value":"AgentFields"}}]}}]}},{"kind":"FragmentDefinition","name":{"kind":"Name","value":"AgentFields"},"typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"Agent"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"name"}},{"kind":"Field","name":{"kind":"Name","value":"label"}},{"kind":"Field","name":{"kind":"Name","value":"description"}},{"kind":"Field","name":{"kind":"Name","value":"icon"}},{"kind":"Field","name":{"kind":"Name","value":"prompt"}},{"kind":"Field","name":{"kind":"Name","value":"modelId"}},{"kind":"Field","name":{"kind":"Name","value":"responseFormat"}},{"kind":"Field","name":{"kind":"Name","value":"roleId"}},{"kind":"Field","name":{"kind":"Name","value":"isCustom"}},{"kind":"Field","name":{"kind":"Name","value":"modelConfiguration"}},{"kind":"Field","name":{"kind":"Name","value":"evaluationInputs"}},{"kind":"Field","name":{"kind":"Name","value":"applicationId"}},{"kind":"Field","name":{"kind":"Name","value":"createdAt"}},{"kind":"Field","name":{"kind":"Name","value":"updatedAt"}}]}}]} as unknown as DocumentNode<UpdateOneAgentMutation, UpdateOneAgentMutationVariables>;
|
||||
|
||||
@@ -2652,11 +2652,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"
|
||||
@@ -3467,6 +3462,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"
|
||||
|
||||
@@ -2652,11 +2652,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"
|
||||
@@ -3467,6 +3462,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"
|
||||
|
||||
@@ -2652,11 +2652,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"
|
||||
@@ -3467,6 +3462,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"
|
||||
|
||||
@@ -2652,11 +2652,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"
|
||||
@@ -3467,6 +3462,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"
|
||||
|
||||
@@ -2652,11 +2652,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"
|
||||
@@ -3467,6 +3462,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"
|
||||
|
||||
@@ -2652,11 +2652,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"
|
||||
@@ -3467,6 +3462,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"
|
||||
|
||||
@@ -2652,11 +2652,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"
|
||||
@@ -3467,6 +3462,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"
|
||||
|
||||
@@ -2647,11 +2647,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"
|
||||
@@ -3462,6 +3457,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"
|
||||
|
||||
@@ -2652,11 +2652,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"
|
||||
@@ -3467,6 +3462,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"
|
||||
|
||||
@@ -2652,11 +2652,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"
|
||||
@@ -3467,6 +3462,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"
|
||||
|
||||
@@ -2652,11 +2652,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"
|
||||
@@ -3467,6 +3462,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"
|
||||
|
||||
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
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
@@ -2652,11 +2652,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"
|
||||
@@ -3467,6 +3462,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"
|
||||
|
||||
@@ -2652,11 +2652,6 @@ msgstr "Vissza ide: {linkText}"
|
||||
msgid "Back to content"
|
||||
msgstr "Vissza a tartalomhoz"
|
||||
|
||||
#. js-lingui-id: OvBnOM
|
||||
#: src/modules/ui/navigation/bread-crumb/components/MobileBreadcrumb.tsx
|
||||
msgid "Back to Settings"
|
||||
msgstr "Vissza a beállításokhoz"
|
||||
|
||||
#. js-lingui-id: GtJbUa
|
||||
#: src/modules/error-handler/components/AppRootErrorFallback.tsx
|
||||
msgid "Background"
|
||||
@@ -3467,6 +3462,11 @@ msgstr "Bezár"
|
||||
msgid "Close banner"
|
||||
msgstr "Banner bezárása"
|
||||
|
||||
#. 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"
|
||||
|
||||
@@ -2652,11 +2652,6 @@ msgstr "Torna a {linkText}"
|
||||
msgid "Back to content"
|
||||
msgstr "Torna al contenuto"
|
||||
|
||||
#. js-lingui-id: OvBnOM
|
||||
#: src/modules/ui/navigation/bread-crumb/components/MobileBreadcrumb.tsx
|
||||
msgid "Back to Settings"
|
||||
msgstr "Torna alle Impostazioni"
|
||||
|
||||
#. js-lingui-id: GtJbUa
|
||||
#: src/modules/error-handler/components/AppRootErrorFallback.tsx
|
||||
msgid "Background"
|
||||
@@ -3467,6 +3462,11 @@ msgstr "Chiudi"
|
||||
msgid "Close banner"
|
||||
msgstr "Chiudi 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"
|
||||
|
||||
@@ -2652,11 +2652,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"
|
||||
@@ -3467,6 +3462,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"
|
||||
|
||||
@@ -2652,11 +2652,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"
|
||||
@@ -3467,6 +3462,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"
|
||||
|
||||
@@ -2652,11 +2652,6 @@ msgstr "Terug naar {linkText}"
|
||||
msgid "Back to content"
|
||||
msgstr "Terug naar inhoud"
|
||||
|
||||
#. js-lingui-id: OvBnOM
|
||||
#: src/modules/ui/navigation/bread-crumb/components/MobileBreadcrumb.tsx
|
||||
msgid "Back to Settings"
|
||||
msgstr "Terug naar Instellingen"
|
||||
|
||||
#. js-lingui-id: GtJbUa
|
||||
#: src/modules/error-handler/components/AppRootErrorFallback.tsx
|
||||
msgid "Background"
|
||||
@@ -3467,6 +3462,11 @@ msgstr "Sluiten"
|
||||
msgid "Close banner"
|
||||
msgstr "Banner sluiten"
|
||||
|
||||
#. 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"
|
||||
|
||||
@@ -2652,11 +2652,6 @@ msgstr "Tilbake til {linkText}"
|
||||
msgid "Back to content"
|
||||
msgstr "Tilbake til innhold"
|
||||
|
||||
#. js-lingui-id: OvBnOM
|
||||
#: src/modules/ui/navigation/bread-crumb/components/MobileBreadcrumb.tsx
|
||||
msgid "Back to Settings"
|
||||
msgstr "Tilbake til Innstillinger"
|
||||
|
||||
#. js-lingui-id: GtJbUa
|
||||
#: src/modules/error-handler/components/AppRootErrorFallback.tsx
|
||||
msgid "Background"
|
||||
@@ -3467,6 +3462,11 @@ msgstr "Lukk"
|
||||
msgid "Close banner"
|
||||
msgstr "Lukk 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"
|
||||
|
||||
@@ -2652,11 +2652,6 @@ msgstr "Powrót do {linkText}"
|
||||
msgid "Back to content"
|
||||
msgstr "Powrót do treści"
|
||||
|
||||
#. js-lingui-id: OvBnOM
|
||||
#: src/modules/ui/navigation/bread-crumb/components/MobileBreadcrumb.tsx
|
||||
msgid "Back to Settings"
|
||||
msgstr "Powrót do ustawień"
|
||||
|
||||
#. js-lingui-id: GtJbUa
|
||||
#: src/modules/error-handler/components/AppRootErrorFallback.tsx
|
||||
msgid "Background"
|
||||
@@ -3467,6 +3462,11 @@ msgstr "Zamknij"
|
||||
msgid "Close banner"
|
||||
msgstr "Zamknij baner"
|
||||
|
||||
#. 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"
|
||||
|
||||
@@ -2647,11 +2647,6 @@ msgstr ""
|
||||
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"
|
||||
@@ -3462,6 +3457,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"
|
||||
|
||||
@@ -2652,11 +2652,6 @@ msgstr "Voltar para {linkText}"
|
||||
msgid "Back to content"
|
||||
msgstr "Voltar ao conteúdo"
|
||||
|
||||
#. js-lingui-id: OvBnOM
|
||||
#: src/modules/ui/navigation/bread-crumb/components/MobileBreadcrumb.tsx
|
||||
msgid "Back to Settings"
|
||||
msgstr "Voltar para Configurações"
|
||||
|
||||
#. js-lingui-id: GtJbUa
|
||||
#: src/modules/error-handler/components/AppRootErrorFallback.tsx
|
||||
msgid "Background"
|
||||
@@ -3467,6 +3462,11 @@ msgstr "Fechar"
|
||||
msgid "Close banner"
|
||||
msgstr "Fechar 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"
|
||||
|
||||
@@ -2652,11 +2652,6 @@ msgstr "Voltar a {linkText}"
|
||||
msgid "Back to content"
|
||||
msgstr "Voltar ao conteúdo"
|
||||
|
||||
#. js-lingui-id: OvBnOM
|
||||
#: src/modules/ui/navigation/bread-crumb/components/MobileBreadcrumb.tsx
|
||||
msgid "Back to Settings"
|
||||
msgstr "Voltar às Definições"
|
||||
|
||||
#. js-lingui-id: GtJbUa
|
||||
#: src/modules/error-handler/components/AppRootErrorFallback.tsx
|
||||
msgid "Background"
|
||||
@@ -3467,6 +3462,11 @@ msgstr "Fechar"
|
||||
msgid "Close banner"
|
||||
msgstr "Fechar 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"
|
||||
|
||||
@@ -2652,11 +2652,6 @@ msgstr "Înapoi la {linkText}"
|
||||
msgid "Back to content"
|
||||
msgstr "Înapoi la conținut"
|
||||
|
||||
#. js-lingui-id: OvBnOM
|
||||
#: src/modules/ui/navigation/bread-crumb/components/MobileBreadcrumb.tsx
|
||||
msgid "Back to Settings"
|
||||
msgstr "Înapoi la Setări"
|
||||
|
||||
#. js-lingui-id: GtJbUa
|
||||
#: src/modules/error-handler/components/AppRootErrorFallback.tsx
|
||||
msgid "Background"
|
||||
@@ -3467,6 +3462,11 @@ msgstr "Închide"
|
||||
msgid "Close banner"
|
||||
msgstr "Închide bannerul"
|
||||
|
||||
#. 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"
|
||||
|
||||
Binary file not shown.
@@ -2652,11 +2652,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"
|
||||
@@ -3467,6 +3462,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"
|
||||
|
||||
@@ -2652,11 +2652,6 @@ msgstr "Tillbaka till {linkText}"
|
||||
msgid "Back to content"
|
||||
msgstr "Tillbaka till innehåll"
|
||||
|
||||
#. js-lingui-id: OvBnOM
|
||||
#: src/modules/ui/navigation/bread-crumb/components/MobileBreadcrumb.tsx
|
||||
msgid "Back to Settings"
|
||||
msgstr "Tillbaka till inställningar"
|
||||
|
||||
#. js-lingui-id: GtJbUa
|
||||
#: src/modules/error-handler/components/AppRootErrorFallback.tsx
|
||||
msgid "Background"
|
||||
@@ -3467,6 +3462,11 @@ msgstr "Stäng"
|
||||
msgid "Close banner"
|
||||
msgstr "Stäng 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"
|
||||
|
||||
@@ -2652,11 +2652,6 @@ msgstr "{linkText}'e geri dön"
|
||||
msgid "Back to content"
|
||||
msgstr "İçeriğe geri dön"
|
||||
|
||||
#. js-lingui-id: OvBnOM
|
||||
#: src/modules/ui/navigation/bread-crumb/components/MobileBreadcrumb.tsx
|
||||
msgid "Back to Settings"
|
||||
msgstr "Ayarlar'a geri dön"
|
||||
|
||||
#. js-lingui-id: GtJbUa
|
||||
#: src/modules/error-handler/components/AppRootErrorFallback.tsx
|
||||
msgid "Background"
|
||||
@@ -3467,6 +3462,11 @@ msgstr "Kapat"
|
||||
msgid "Close banner"
|
||||
msgstr "Afişi kapat"
|
||||
|
||||
#. 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"
|
||||
|
||||
@@ -2652,11 +2652,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"
|
||||
@@ -3467,6 +3462,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"
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user