EncryptedString PlaintextString branded string types (#21001)

## Summary

closes https://github.com/twentyhq/core-team-issues/issues/2464

Introduces compile-time branded types to distinguish encrypted
ciphertext from plaintext strings, preventing mix-ups like the one fixed
in #20819 — but at the type level rather in addition to the one existing
at runtime.

### Branded string primitives

- Created `EncryptedString` and `PlaintextString` as hard nominal brands
using `z.string().brand(...)`, making them non-assignable to each other
or to raw `string`
- Created `isEncryptedString` type predicate to narrow `string` to
`EncryptedString` based on the `enc:v2:` envelope prefix
- Retyped `SecretEncryptionService`: `encryptVersioned` accepts
`PlaintextString`, `decryptVersioned` returns `PlaintextString`

### Entity typing

- Typed encrypted columns across entities:
`SigningKeyEntity.privateKey`,
`TwoFactorAuthenticationMethodEntity.secret`,
`ApplicationRegistrationVariableEntity.encryptedValue`,
`ApplicationVariableEntity.value`
- Parameterized JSONB types for connected account connection parameters
(`ImapSmtpCaldavParams<Pwd>`) with reusable aliases
`EncryptedImapSmtpCaldavParams` / `DecryptedImapSmtpCaldavParams`
- Typed DTOs (`CreateApplicationRegistrationVariableInput`,
`UpdateApplicationRegistrationVariablePayload`,
`UpdateApplicationVariableEntityInput`) with `PlaintextString`

### ApplicationVariable always-encrypt uniformization

- Retyped `ApplicationVariableEntity.value` to `EncryptedString | ''` —
all values are now encrypted regardless of `isSecret`
- Updated `ApplicationVariableEntityService` to always encrypt on write
and always decrypt on read
- Simplified `UpdateApplicationVariableActionHandlerService` by removing
conditional encrypt/decrypt-on-isSecret-toggle logic
- Added slow instance command (`2.9.0`) to backfill-encrypt existing
`isSecret=false` plaintext rows and tighten the `CHECK` constraint

### ConfigStorageService refactor

- Split `convertAndSecureValue` (which used `any`) into two well-typed
methods: `convertAndDecrypt` and `convertAndEncrypt`
- Introduced `isSensitiveStringValue` type predicate to narrow values
before encryption/decryption

### What's next
- Typeorm entity derivation to strictly type sitemap configuration as
code + handler logic for encryption rotation
- https://github.com/twentyhq/core-team-issues/issues/2465
This commit is contained in:
Paul Rastoin
2026-05-28 17:41:16 +02:00
committed by GitHub
parent 9b54200d8c
commit ebfaca5b3d
85 changed files with 1528 additions and 937 deletions
@@ -4,8 +4,8 @@ import { type Manifest } from 'twenty-shared/application';
import { ALL_METADATA_NAME } from 'twenty-shared/metadata';
import { isDefined } from 'twenty-shared/utils';
import { ComputeApplicationManifestAllUniversalFlatEntityMapsService } from 'src/engine/core-modules/application/application-manifest/services/compute-application-manifest-all-universal-flat-entity-maps.service';
import { buildFromToAllUniversalFlatEntityMaps } from 'src/engine/core-modules/application/application-manifest/utils/build-from-to-all-universal-flat-entity-maps.util';
import { computeApplicationManifestAllUniversalFlatEntityMaps } from 'src/engine/core-modules/application/application-manifest/utils/compute-application-manifest-all-universal-flat-entity-maps.util';
import { getApplicationSubAllFlatEntityMaps } from 'src/engine/core-modules/application/application-manifest/utils/get-application-sub-all-flat-entity-maps.util';
import {
ApplicationException,
@@ -31,6 +31,7 @@ export class ApplicationManifestMigrationService {
private readonly workspaceCacheService: WorkspaceCacheService,
private readonly workspaceMigrationValidateBuildAndRunService: WorkspaceMigrationValidateBuildAndRunService,
private readonly applicationService: ApplicationService,
private readonly computeManifestFlatEntityMapsService: ComputeApplicationManifestAllUniversalFlatEntityMapsService,
) {}
async syncPreInstallLogicFunctionFromManifest({
@@ -106,10 +107,11 @@ export class ApplicationManifestMigrationService {
});
const toAllUniversalFlatEntityMaps =
computeApplicationManifestAllUniversalFlatEntityMaps({
this.computeManifestFlatEntityMapsService.compute({
manifest: preInstallOnlyManifest,
ownerFlatApplication,
now,
workspaceId,
});
const dependencyAllFlatEntityMaps = getApplicationSubAllFlatEntityMaps({
@@ -190,10 +192,11 @@ export class ApplicationManifestMigrationService {
});
const toAllUniversalFlatEntityMaps =
computeApplicationManifestAllUniversalFlatEntityMaps({
this.computeManifestFlatEntityMapsService.compute({
manifest,
ownerFlatApplication,
now,
workspaceId,
});
const dependencyAllFlatEntityMaps = getApplicationSubAllFlatEntityMaps({
@@ -3,10 +3,12 @@ import { Module } from '@nestjs/common';
import { ApplicationModule } from 'src/engine/core-modules/application/application.module';
import { ApplicationManifestMigrationService } from 'src/engine/core-modules/application/application-manifest/application-manifest-migration.service';
import { ApplicationManifestResolver } from 'src/engine/core-modules/application/application-manifest/application-manifest.resolver';
import { ComputeApplicationManifestAllUniversalFlatEntityMapsService } from 'src/engine/core-modules/application/application-manifest/services/compute-application-manifest-all-universal-flat-entity-maps.service';
import { ApplicationSyncService } from 'src/engine/core-modules/application/application-manifest/application-sync.service';
import { ApplicationVariableEntityModule } from 'src/engine/core-modules/application/application-variable/application-variable.module';
import { FeatureFlagModule } from 'src/engine/core-modules/feature-flag/feature-flag.module';
import { FileStorageModule } from 'src/engine/core-modules/file-storage/file-storage.module';
import { SecretEncryptionModule } from 'src/engine/core-modules/secret-encryption/secret-encryption.module';
import { PermissionsModule } from 'src/engine/metadata-modules/permissions/permissions.module';
import { WorkspaceCacheModule } from 'src/engine/workspace-cache/workspace-cache.module';
import { WorkspaceMigrationGraphqlApiExceptionInterceptor } from 'src/engine/workspace-manager/workspace-migration/interceptors/workspace-migration-graphql-api-exception.interceptor';
@@ -20,6 +22,7 @@ import { WorkspaceMigrationModule } from 'src/engine/workspace-manager/workspace
FeatureFlagModule,
FileStorageModule,
PermissionsModule,
SecretEncryptionModule,
WorkspaceCacheModule,
WorkspaceMigrationModule,
WorkspaceMigrationRunnerModule,
@@ -28,6 +31,7 @@ import { WorkspaceMigrationModule } from 'src/engine/workspace-manager/workspace
ApplicationManifestMigrationService,
ApplicationManifestResolver,
ApplicationSyncService,
ComputeApplicationManifestAllUniversalFlatEntityMapsService,
WorkspaceMigrationGraphqlApiExceptionInterceptor,
],
exports: [ApplicationManifestMigrationService, ApplicationSyncService],
@@ -1,3 +1,4 @@
import { type EncryptedString } from 'src/engine/core-modules/secret-encryption/branded-strings/encrypted-string.type';
import { type UniversalFlatApplicationVariable } from 'src/engine/workspace-manager/workspace-migration/universal-flat-entity/types/universal-flat-application-variable.type';
export const fromApplicationVariableManifestToUniversalFlatApplicationVariable =
@@ -5,7 +6,7 @@ export const fromApplicationVariableManifestToUniversalFlatApplicationVariable =
key,
universalIdentifier,
description,
value,
encryptedValue,
isSecret,
applicationUniversalIdentifier,
now,
@@ -13,7 +14,7 @@ export const fromApplicationVariableManifestToUniversalFlatApplicationVariable =
key: string;
universalIdentifier: string;
description?: string;
value?: string;
encryptedValue: EncryptedString | '';
isSecret?: boolean;
applicationUniversalIdentifier: string;
now: string;
@@ -22,7 +23,7 @@ export const fromApplicationVariableManifestToUniversalFlatApplicationVariable =
universalIdentifier,
applicationUniversalIdentifier,
key,
value: isSecret ? '' : (value ?? ''), // We protect secret variable by not syncing its value at all
value: encryptedValue,
description: description ?? '',
isSecret: isSecret ?? false,
createdAt: now,
@@ -0,0 +1,621 @@
import { Injectable } from '@nestjs/common';
import { type Manifest } from 'twenty-shared/application';
import { MAX_CUSTOM_INDEXES_PER_OBJECT } from 'twenty-shared/constants';
import { FieldMetadataType } from 'twenty-shared/types';
import { isDefined } from 'twenty-shared/utils';
import { fromApplicationVariableManifestToUniversalFlatApplicationVariable } from 'src/engine/core-modules/application/application-manifest/converters/from-application-variable-manifest-to-universal-flat-application-variable.util';
import { fromCommandMenuItemManifestToUniversalFlatCommandMenuItem } from 'src/engine/core-modules/application/application-manifest/converters/from-command-menu-item-manifest-to-universal-flat-command-menu-item.util';
import { fromConnectionProviderManifestToUniversalFlatConnectionProvider } from 'src/engine/core-modules/application/application-manifest/converters/from-connection-provider-manifest-to-universal-flat-connection-provider.util';
import { fromFieldManifestToUniversalFlatFieldMetadata } from 'src/engine/core-modules/application/application-manifest/converters/from-field-manifest-to-universal-flat-field-metadata.util';
import { fromFieldPermissionManifestToUniversalFlatFieldPermission } from 'src/engine/core-modules/application/application-manifest/converters/from-field-permission-manifest-to-universal-flat-field-permission.util';
import { fromFrontComponentManifestToUniversalFlatFrontComponent } from 'src/engine/core-modules/application/application-manifest/converters/from-front-component-manifest-to-universal-flat-front-component.util';
import { fromIndexManifestToUniversalFlatIndex } from 'src/engine/core-modules/application/application-manifest/converters/from-index-manifest-to-universal-flat-index.util';
import { fromLogicFunctionManifestToUniversalFlatLogicFunction } from 'src/engine/core-modules/application/application-manifest/converters/from-logic-function-manifest-to-universal-flat-logic-function.util';
import { fromNavigationMenuItemManifestToUniversalFlatNavigationMenuItem } from 'src/engine/core-modules/application/application-manifest/converters/from-navigation-menu-item-manifest-to-universal-flat-navigation-menu-item.util';
import { fromObjectManifestToUniversalFlatObjectMetadata } from 'src/engine/core-modules/application/application-manifest/converters/from-object-manifest-to-universal-flat-object-metadata.util';
import { fromObjectPermissionManifestToUniversalFlatObjectPermission } from 'src/engine/core-modules/application/application-manifest/converters/from-object-permission-manifest-to-universal-flat-object-permission.util';
import { fromPageLayoutManifestToUniversalFlatPageLayout } from 'src/engine/core-modules/application/application-manifest/converters/from-page-layout-manifest-to-universal-flat-page-layout.util';
import { fromPageLayoutTabManifestToUniversalFlatPageLayoutTab } from 'src/engine/core-modules/application/application-manifest/converters/from-page-layout-tab-manifest-to-universal-flat-page-layout-tab.util';
import { fromPageLayoutWidgetManifestToUniversalFlatPageLayoutWidget } from 'src/engine/core-modules/application/application-manifest/converters/from-page-layout-widget-manifest-to-universal-flat-page-layout-widget.util';
import { fromPermissionFlagManifestToUniversalFlatPermissionFlag } from 'src/engine/core-modules/application/application-manifest/converters/from-permission-flag-manifest-to-universal-flat-permission-flag.util';
import { fromPermissionFlagToUniversalFlatRolePermissionFlag } from 'src/engine/core-modules/application/application-manifest/converters/from-permission-flag-to-universal-flat-role-permission-flag.util';
import { fromRoleManifestToUniversalFlatRole } from 'src/engine/core-modules/application/application-manifest/converters/from-role-manifest-to-universal-flat-role.util';
import { fromSkillManifestToUniversalFlatSkill } from 'src/engine/core-modules/application/application-manifest/converters/from-skill-manifest-to-universal-flat-skill.util';
import { computeSearchVectorUniversalSettingsFromObjectManifest } from 'src/engine/core-modules/application/application-manifest/utils/compute-search-vector-universal-settings-from-object-manifest.util';
import { fromViewFieldGroupManifestToUniversalFlatViewFieldGroup } from 'src/engine/core-modules/application/application-manifest/converters/from-view-field-group-manifest-to-universal-flat-view-field-group.util';
import { fromViewFieldManifestToUniversalFlatViewField } from 'src/engine/core-modules/application/application-manifest/converters/from-view-field-manifest-to-universal-flat-view-field.util';
import { fromViewFilterGroupManifestToUniversalFlatViewFilterGroup } from 'src/engine/core-modules/application/application-manifest/converters/from-view-filter-group-manifest-to-universal-flat-view-filter-group.util';
import { fromViewFilterManifestToUniversalFlatViewFilter } from 'src/engine/core-modules/application/application-manifest/converters/from-view-filter-manifest-to-universal-flat-view-filter.util';
import { fromViewGroupManifestToUniversalFlatViewGroup } from 'src/engine/core-modules/application/application-manifest/converters/from-view-group-manifest-to-universal-flat-view-group.util';
import { fromViewManifestToUniversalFlatView } from 'src/engine/core-modules/application/application-manifest/converters/from-view-manifest-to-universal-flat-view.util';
import { fromViewSortManifestToUniversalFlatViewSort } from 'src/engine/core-modules/application/application-manifest/converters/from-view-sort-manifest-to-universal-flat-view-sort.util';
import { type FlatApplication } from 'src/engine/core-modules/application/types/flat-application.type';
import { fromAgentManifestToUniversalFlatAgent } from 'src/engine/core-modules/application/utils/from-agent-manifest-to-universal-flat-agent.util';
import { type EncryptedString } from 'src/engine/core-modules/secret-encryption/branded-strings/encrypted-string.type';
import { type PlaintextString } from 'src/engine/core-modules/secret-encryption/branded-strings/plaintext-string.type';
import { SecretEncryptionService } from 'src/engine/core-modules/secret-encryption/secret-encryption.service';
import { generateIndexForFlatFieldMetadata } from 'src/engine/metadata-modules/flat-field-metadata/utils/generate-index-for-flat-field-metadata.util';
import { createEmptyAllFlatEntityMaps } from 'src/engine/metadata-modules/flat-entity/constant/create-empty-all-flat-entity-maps.constant';
import { type AllFlatEntityMaps } from 'src/engine/metadata-modules/flat-entity/types/all-flat-entity-maps.type';
import { type UniversalFlatFieldMetadata } from 'src/engine/workspace-manager/workspace-migration/universal-flat-entity/types/universal-flat-field-metadata.type';
import { addUniversalFlatEntityToUniversalFlatEntityMapsThroughMutationOrThrow } from 'src/engine/workspace-manager/workspace-migration/universal-flat-entity/utils/add-universal-flat-entity-to-universal-flat-entity-maps-through-mutation-or-throw.util';
@Injectable()
export class ComputeApplicationManifestAllUniversalFlatEntityMapsService {
constructor(
private readonly secretEncryptionService: SecretEncryptionService,
) {}
private encryptApplicationVariableValue(
plaintext: string,
workspaceId: string,
): EncryptedString | '' {
if (plaintext === '') {
return '';
}
return this.secretEncryptionService.encryptVersioned(
plaintext as PlaintextString,
{ workspaceId },
);
}
compute({
manifest,
ownerFlatApplication,
now,
workspaceId,
}: {
manifest: Manifest;
ownerFlatApplication: FlatApplication;
now: string;
workspaceId: string;
}): AllFlatEntityMaps {
const allUniversalFlatEntityMaps = createEmptyAllFlatEntityMaps();
const { universalIdentifier: applicationUniversalIdentifier } =
ownerFlatApplication;
for (const objectManifest of manifest.objects) {
const flatObjectMetadata =
fromObjectManifestToUniversalFlatObjectMetadata({
objectManifest,
applicationUniversalIdentifier,
now,
});
addUniversalFlatEntityToUniversalFlatEntityMapsThroughMutationOrThrow({
universalFlatEntity: flatObjectMetadata,
universalFlatEntityMapsToMutate:
allUniversalFlatEntityMaps.flatObjectMetadataMaps,
});
for (const fieldManifest of objectManifest.fields) {
const enrichedFieldManifest =
fieldManifest.type === FieldMetadataType.TS_VECTOR &&
!isDefined(fieldManifest.universalSettings)
? {
...fieldManifest,
objectUniversalIdentifier: objectManifest.universalIdentifier,
universalSettings:
computeSearchVectorUniversalSettingsFromObjectManifest({
objectManifest,
}),
}
: {
...fieldManifest,
objectUniversalIdentifier: objectManifest.universalIdentifier,
};
const flatFieldMetadata = fromFieldManifestToUniversalFlatFieldMetadata(
{
fieldManifest: enrichedFieldManifest,
applicationUniversalIdentifier,
now,
},
);
addUniversalFlatEntityToUniversalFlatEntityMapsThroughMutationOrThrow({
universalFlatEntity: flatFieldMetadata,
universalFlatEntityMapsToMutate:
allUniversalFlatEntityMaps.flatFieldMetadataMaps,
});
if (flatFieldMetadata.isUnique) {
addUniversalFlatEntityToUniversalFlatEntityMapsThroughMutationOrThrow(
{
universalFlatEntity: generateIndexForFlatFieldMetadata({
flatFieldMetadata,
flatObjectMetadata,
}),
universalFlatEntityMapsToMutate:
allUniversalFlatEntityMaps.flatIndexMaps,
},
);
}
}
}
for (const fieldManifest of manifest.fields) {
const flatFieldMetadata = fromFieldManifestToUniversalFlatFieldMetadata({
fieldManifest: fieldManifest,
applicationUniversalIdentifier,
now,
});
addUniversalFlatEntityToUniversalFlatEntityMapsThroughMutationOrThrow({
universalFlatEntity: flatFieldMetadata,
universalFlatEntityMapsToMutate:
allUniversalFlatEntityMaps.flatFieldMetadataMaps,
});
if (flatFieldMetadata.isUnique) {
const flatObjectMetadata =
allUniversalFlatEntityMaps.flatObjectMetadataMaps
.byUniversalIdentifier[
flatFieldMetadata.objectMetadataUniversalIdentifier
];
if (isDefined(flatObjectMetadata)) {
addUniversalFlatEntityToUniversalFlatEntityMapsThroughMutationOrThrow(
{
universalFlatEntity: generateIndexForFlatFieldMetadata({
flatFieldMetadata,
flatObjectMetadata,
}),
universalFlatEntityMapsToMutate:
allUniversalFlatEntityMaps.flatIndexMaps,
},
);
}
}
}
const indexCountByObjectUniversalIdentifier = new Map<string, number>();
const fieldsByObjectUniversalIdentifier = new Map<
string,
UniversalFlatFieldMetadata[]
>();
for (const flatField of Object.values(
allUniversalFlatEntityMaps.flatFieldMetadataMaps.byUniversalIdentifier,
)) {
if (!isDefined(flatField)) continue;
const bucket =
fieldsByObjectUniversalIdentifier.get(
flatField.objectMetadataUniversalIdentifier,
) ?? [];
if (bucket.length === 0) {
fieldsByObjectUniversalIdentifier.set(
flatField.objectMetadataUniversalIdentifier,
bucket,
);
}
bucket.push(flatField);
}
for (const indexManifest of manifest.indexes ?? []) {
const flatObjectMetadata =
allUniversalFlatEntityMaps.flatObjectMetadataMaps.byUniversalIdentifier[
indexManifest.objectUniversalIdentifier
];
if (!isDefined(flatObjectMetadata)) {
throw new Error(
`Index "${indexManifest.universalIdentifier}" references unknown object ${indexManifest.objectUniversalIdentifier}`,
);
}
const nextCount =
(indexCountByObjectUniversalIdentifier.get(
indexManifest.objectUniversalIdentifier,
) ?? 0) + 1;
if (nextCount > MAX_CUSTOM_INDEXES_PER_OBJECT) {
throw new Error(
`Application declares more than ${MAX_CUSTOM_INDEXES_PER_OBJECT} indexes on object ${indexManifest.objectUniversalIdentifier}`,
);
}
indexCountByObjectUniversalIdentifier.set(
indexManifest.objectUniversalIdentifier,
nextCount,
);
addUniversalFlatEntityToUniversalFlatEntityMapsThroughMutationOrThrow({
universalFlatEntity: fromIndexManifestToUniversalFlatIndex({
indexManifest,
flatObjectMetadata,
objectFlatFieldMetadatas:
fieldsByObjectUniversalIdentifier.get(
flatObjectMetadata.universalIdentifier,
) ?? [],
applicationUniversalIdentifier,
now,
}),
universalFlatEntityMapsToMutate:
allUniversalFlatEntityMaps.flatIndexMaps,
});
}
for (const logicFunctionManifest of manifest.logicFunctions) {
addUniversalFlatEntityToUniversalFlatEntityMapsThroughMutationOrThrow({
universalFlatEntity:
fromLogicFunctionManifestToUniversalFlatLogicFunction({
logicFunctionManifest,
applicationUniversalIdentifier,
now,
}),
universalFlatEntityMapsToMutate:
allUniversalFlatEntityMaps.flatLogicFunctionMaps,
});
}
for (const frontComponentManifest of manifest.frontComponents) {
addUniversalFlatEntityToUniversalFlatEntityMapsThroughMutationOrThrow({
universalFlatEntity:
fromFrontComponentManifestToUniversalFlatFrontComponent({
frontComponentManifest,
applicationUniversalIdentifier,
now,
}),
universalFlatEntityMapsToMutate:
allUniversalFlatEntityMaps.flatFrontComponentMaps,
});
}
for (const connectionProviderManifest of manifest.connectionProviders ??
[]) {
addUniversalFlatEntityToUniversalFlatEntityMapsThroughMutationOrThrow({
universalFlatEntity:
fromConnectionProviderManifestToUniversalFlatConnectionProvider({
connectionProviderManifest,
applicationUniversalIdentifier,
now,
}),
universalFlatEntityMapsToMutate:
allUniversalFlatEntityMaps.flatConnectionProviderMaps,
});
}
for (const permissionFlagManifest of manifest.permissionFlags ?? []) {
addUniversalFlatEntityToUniversalFlatEntityMapsThroughMutationOrThrow({
universalFlatEntity:
fromPermissionFlagManifestToUniversalFlatPermissionFlag({
permissionFlagManifest,
applicationUniversalIdentifier,
now,
}),
universalFlatEntityMapsToMutate:
allUniversalFlatEntityMaps.flatPermissionFlagMaps,
});
}
for (const roleManifest of manifest.roles) {
addUniversalFlatEntityToUniversalFlatEntityMapsThroughMutationOrThrow({
universalFlatEntity: fromRoleManifestToUniversalFlatRole({
roleManifest,
applicationUniversalIdentifier,
now,
}),
universalFlatEntityMapsToMutate:
allUniversalFlatEntityMaps.flatRoleMaps,
});
for (const objectPermissionManifest of roleManifest.objectPermissions ??
[]) {
addUniversalFlatEntityToUniversalFlatEntityMapsThroughMutationOrThrow({
universalFlatEntity:
fromObjectPermissionManifestToUniversalFlatObjectPermission({
objectPermissionManifest,
roleUniversalIdentifier: roleManifest.universalIdentifier,
applicationUniversalIdentifier,
now,
}),
universalFlatEntityMapsToMutate:
allUniversalFlatEntityMaps.flatObjectPermissionMaps,
});
}
for (const fieldPermissionManifest of roleManifest.fieldPermissions ??
[]) {
addUniversalFlatEntityToUniversalFlatEntityMapsThroughMutationOrThrow({
universalFlatEntity:
fromFieldPermissionManifestToUniversalFlatFieldPermission({
fieldPermissionManifest,
roleUniversalIdentifier: roleManifest.universalIdentifier,
applicationUniversalIdentifier,
now,
}),
universalFlatEntityMapsToMutate:
allUniversalFlatEntityMaps.flatFieldPermissionMaps,
});
}
for (const permissionFlagUniversalIdentifier of roleManifest.permissionFlagUniversalIdentifiers ??
[]) {
addUniversalFlatEntityToUniversalFlatEntityMapsThroughMutationOrThrow({
universalFlatEntity:
fromPermissionFlagToUniversalFlatRolePermissionFlag({
permissionFlagUniversalIdentifier,
roleUniversalIdentifier: roleManifest.universalIdentifier,
applicationUniversalIdentifier,
now,
}),
universalFlatEntityMapsToMutate:
allUniversalFlatEntityMaps.flatRolePermissionFlagMaps,
});
}
}
for (const skillManifest of manifest.skills ?? []) {
addUniversalFlatEntityToUniversalFlatEntityMapsThroughMutationOrThrow({
universalFlatEntity: fromSkillManifestToUniversalFlatSkill({
skillManifest,
applicationUniversalIdentifier,
now,
}),
universalFlatEntityMapsToMutate:
allUniversalFlatEntityMaps.flatSkillMaps,
});
}
for (const agentManifest of manifest.agents ?? []) {
addUniversalFlatEntityToUniversalFlatEntityMapsThroughMutationOrThrow({
universalFlatEntity: fromAgentManifestToUniversalFlatAgent({
agentManifest,
applicationUniversalIdentifier,
now,
}),
universalFlatEntityMapsToMutate:
allUniversalFlatEntityMaps.flatAgentMaps,
});
}
for (const viewManifest of manifest.views ?? []) {
addUniversalFlatEntityToUniversalFlatEntityMapsThroughMutationOrThrow({
universalFlatEntity: fromViewManifestToUniversalFlatView({
viewManifest,
applicationUniversalIdentifier,
now,
}),
universalFlatEntityMapsToMutate:
allUniversalFlatEntityMaps.flatViewMaps,
});
for (const viewFieldGroupManifest of viewManifest.fieldGroups ?? []) {
addUniversalFlatEntityToUniversalFlatEntityMapsThroughMutationOrThrow({
universalFlatEntity:
fromViewFieldGroupManifestToUniversalFlatViewFieldGroup({
viewFieldGroupManifest,
viewUniversalIdentifier: viewManifest.universalIdentifier,
applicationUniversalIdentifier,
now,
}),
universalFlatEntityMapsToMutate:
allUniversalFlatEntityMaps.flatViewFieldGroupMaps,
});
}
for (const viewFieldManifest of viewManifest.fields ?? []) {
addUniversalFlatEntityToUniversalFlatEntityMapsThroughMutationOrThrow({
universalFlatEntity: fromViewFieldManifestToUniversalFlatViewField({
viewFieldManifest,
viewUniversalIdentifier: viewManifest.universalIdentifier,
applicationUniversalIdentifier,
now,
}),
universalFlatEntityMapsToMutate:
allUniversalFlatEntityMaps.flatViewFieldMaps,
});
}
for (const viewFilterGroupManifest of viewManifest.filterGroups ?? []) {
addUniversalFlatEntityToUniversalFlatEntityMapsThroughMutationOrThrow({
universalFlatEntity:
fromViewFilterGroupManifestToUniversalFlatViewFilterGroup({
viewFilterGroupManifest,
viewUniversalIdentifier: viewManifest.universalIdentifier,
applicationUniversalIdentifier,
now,
}),
universalFlatEntityMapsToMutate:
allUniversalFlatEntityMaps.flatViewFilterGroupMaps,
});
}
for (const viewFilterManifest of viewManifest.filters ?? []) {
addUniversalFlatEntityToUniversalFlatEntityMapsThroughMutationOrThrow({
universalFlatEntity: fromViewFilterManifestToUniversalFlatViewFilter({
viewFilterManifest,
viewUniversalIdentifier: viewManifest.universalIdentifier,
applicationUniversalIdentifier,
now,
}),
universalFlatEntityMapsToMutate:
allUniversalFlatEntityMaps.flatViewFilterMaps,
});
}
for (const viewGroupManifest of viewManifest.groups ?? []) {
addUniversalFlatEntityToUniversalFlatEntityMapsThroughMutationOrThrow({
universalFlatEntity: fromViewGroupManifestToUniversalFlatViewGroup({
viewGroupManifest,
viewUniversalIdentifier: viewManifest.universalIdentifier,
applicationUniversalIdentifier,
now,
}),
universalFlatEntityMapsToMutate:
allUniversalFlatEntityMaps.flatViewGroupMaps,
});
}
for (const viewSortManifest of viewManifest.sorts ?? []) {
addUniversalFlatEntityToUniversalFlatEntityMapsThroughMutationOrThrow({
universalFlatEntity: fromViewSortManifestToUniversalFlatViewSort({
viewSortManifest,
viewUniversalIdentifier: viewManifest.universalIdentifier,
applicationUniversalIdentifier,
now,
}),
universalFlatEntityMapsToMutate:
allUniversalFlatEntityMaps.flatViewSortMaps,
});
}
}
for (const navigationMenuItemManifest of manifest.navigationMenuItems ??
[]) {
addUniversalFlatEntityToUniversalFlatEntityMapsThroughMutationOrThrow({
universalFlatEntity:
fromNavigationMenuItemManifestToUniversalFlatNavigationMenuItem({
navigationMenuItemManifest,
applicationUniversalIdentifier,
now,
}),
universalFlatEntityMapsToMutate:
allUniversalFlatEntityMaps.flatNavigationMenuItemMaps,
});
}
for (const pageLayoutManifest of manifest.pageLayouts ?? []) {
addUniversalFlatEntityToUniversalFlatEntityMapsThroughMutationOrThrow({
universalFlatEntity: fromPageLayoutManifestToUniversalFlatPageLayout({
pageLayoutManifest,
applicationUniversalIdentifier,
now,
}),
universalFlatEntityMapsToMutate:
allUniversalFlatEntityMaps.flatPageLayoutMaps,
});
for (const pageLayoutTabManifest of pageLayoutManifest.tabs ?? []) {
addUniversalFlatEntityToUniversalFlatEntityMapsThroughMutationOrThrow({
universalFlatEntity:
fromPageLayoutTabManifestToUniversalFlatPageLayoutTab({
pageLayoutTabManifest,
pageLayoutUniversalIdentifier:
pageLayoutManifest.universalIdentifier,
applicationUniversalIdentifier,
now,
}),
universalFlatEntityMapsToMutate:
allUniversalFlatEntityMaps.flatPageLayoutTabMaps,
});
for (const pageLayoutWidgetManifest of pageLayoutTabManifest.widgets ??
[]) {
addUniversalFlatEntityToUniversalFlatEntityMapsThroughMutationOrThrow(
{
universalFlatEntity:
fromPageLayoutWidgetManifestToUniversalFlatPageLayoutWidget({
pageLayoutWidgetManifest,
pageLayoutTabUniversalIdentifier:
pageLayoutTabManifest.universalIdentifier,
applicationUniversalIdentifier,
now,
}),
universalFlatEntityMapsToMutate:
allUniversalFlatEntityMaps.flatPageLayoutWidgetMaps,
},
);
}
}
}
for (const pageLayoutTabManifest of manifest.pageLayoutTabs ?? []) {
if (!isDefined(pageLayoutTabManifest.pageLayoutUniversalIdentifier)) {
throw new Error(
`Top-level pageLayoutTab "${pageLayoutTabManifest.universalIdentifier}" is missing required pageLayoutUniversalIdentifier`,
);
}
addUniversalFlatEntityToUniversalFlatEntityMapsThroughMutationOrThrow({
universalFlatEntity:
fromPageLayoutTabManifestToUniversalFlatPageLayoutTab({
pageLayoutTabManifest,
pageLayoutUniversalIdentifier:
pageLayoutTabManifest.pageLayoutUniversalIdentifier,
applicationUniversalIdentifier,
now,
}),
universalFlatEntityMapsToMutate:
allUniversalFlatEntityMaps.flatPageLayoutTabMaps,
});
for (const pageLayoutWidgetManifest of pageLayoutTabManifest.widgets ??
[]) {
addUniversalFlatEntityToUniversalFlatEntityMapsThroughMutationOrThrow({
universalFlatEntity:
fromPageLayoutWidgetManifestToUniversalFlatPageLayoutWidget({
pageLayoutWidgetManifest,
pageLayoutTabUniversalIdentifier:
pageLayoutTabManifest.universalIdentifier,
applicationUniversalIdentifier,
now,
}),
universalFlatEntityMapsToMutate:
allUniversalFlatEntityMaps.flatPageLayoutWidgetMaps,
});
}
}
for (const [key, applicationVariableManifest] of Object.entries(
manifest.application.applicationVariables ?? {},
)) {
const plaintextValue =
'value' in applicationVariableManifest
? applicationVariableManifest.value
: undefined;
const isSecret = applicationVariableManifest.isSecret;
const rawValue = isSecret ? '' : (plaintextValue ?? '');
addUniversalFlatEntityToUniversalFlatEntityMapsThroughMutationOrThrow({
universalFlatEntity:
fromApplicationVariableManifestToUniversalFlatApplicationVariable({
key,
universalIdentifier:
applicationVariableManifest.universalIdentifier,
encryptedValue: this.encryptApplicationVariableValue(
rawValue,
workspaceId,
),
description: applicationVariableManifest.description,
isSecret,
applicationUniversalIdentifier,
now,
}),
universalFlatEntityMapsToMutate:
allUniversalFlatEntityMaps.flatApplicationVariableMaps,
});
}
for (const commandMenuItemManifest of manifest.commandMenuItems ?? []) {
if (
!isDefined(commandMenuItemManifest.frontComponentUniversalIdentifier)
) {
throw new Error(
`Top-level commandMenuItem "${commandMenuItemManifest.universalIdentifier}" is missing required frontComponentUniversalIdentifier`,
);
}
addUniversalFlatEntityToUniversalFlatEntityMapsThroughMutationOrThrow({
universalFlatEntity:
fromCommandMenuItemManifestToUniversalFlatCommandMenuItem({
commandMenuItemManifest,
applicationUniversalIdentifier,
now,
}),
universalFlatEntityMapsToMutate:
allUniversalFlatEntityMaps.flatCommandMenuItemMaps,
});
}
return allUniversalFlatEntityMaps;
}
}
@@ -1,565 +0,0 @@
import { type Manifest } from 'twenty-shared/application';
import { MAX_CUSTOM_INDEXES_PER_OBJECT } from 'twenty-shared/constants';
import { FieldMetadataType } from 'twenty-shared/types';
import { isDefined } from 'twenty-shared/utils';
import { generateIndexForFlatFieldMetadata } from 'src/engine/metadata-modules/flat-field-metadata/utils/generate-index-for-flat-field-metadata.util';
import { fromApplicationVariableManifestToUniversalFlatApplicationVariable } from 'src/engine/core-modules/application/application-manifest/converters/from-application-variable-manifest-to-universal-flat-application-variable.util';
import { fromCommandMenuItemManifestToUniversalFlatCommandMenuItem } from 'src/engine/core-modules/application/application-manifest/converters/from-command-menu-item-manifest-to-universal-flat-command-menu-item.util';
import { fromConnectionProviderManifestToUniversalFlatConnectionProvider } from 'src/engine/core-modules/application/application-manifest/converters/from-connection-provider-manifest-to-universal-flat-connection-provider.util';
import { fromFieldManifestToUniversalFlatFieldMetadata } from 'src/engine/core-modules/application/application-manifest/converters/from-field-manifest-to-universal-flat-field-metadata.util';
import { fromFieldPermissionManifestToUniversalFlatFieldPermission } from 'src/engine/core-modules/application/application-manifest/converters/from-field-permission-manifest-to-universal-flat-field-permission.util';
import { fromFrontComponentManifestToUniversalFlatFrontComponent } from 'src/engine/core-modules/application/application-manifest/converters/from-front-component-manifest-to-universal-flat-front-component.util';
import { fromIndexManifestToUniversalFlatIndex } from 'src/engine/core-modules/application/application-manifest/converters/from-index-manifest-to-universal-flat-index.util';
import { fromLogicFunctionManifestToUniversalFlatLogicFunction } from 'src/engine/core-modules/application/application-manifest/converters/from-logic-function-manifest-to-universal-flat-logic-function.util';
import { fromNavigationMenuItemManifestToUniversalFlatNavigationMenuItem } from 'src/engine/core-modules/application/application-manifest/converters/from-navigation-menu-item-manifest-to-universal-flat-navigation-menu-item.util';
import { fromObjectManifestToUniversalFlatObjectMetadata } from 'src/engine/core-modules/application/application-manifest/converters/from-object-manifest-to-universal-flat-object-metadata.util';
import { fromObjectPermissionManifestToUniversalFlatObjectPermission } from 'src/engine/core-modules/application/application-manifest/converters/from-object-permission-manifest-to-universal-flat-object-permission.util';
import { fromPageLayoutManifestToUniversalFlatPageLayout } from 'src/engine/core-modules/application/application-manifest/converters/from-page-layout-manifest-to-universal-flat-page-layout.util';
import { fromPageLayoutTabManifestToUniversalFlatPageLayoutTab } from 'src/engine/core-modules/application/application-manifest/converters/from-page-layout-tab-manifest-to-universal-flat-page-layout-tab.util';
import { fromPageLayoutWidgetManifestToUniversalFlatPageLayoutWidget } from 'src/engine/core-modules/application/application-manifest/converters/from-page-layout-widget-manifest-to-universal-flat-page-layout-widget.util';
import { fromPermissionFlagManifestToUniversalFlatPermissionFlag } from 'src/engine/core-modules/application/application-manifest/converters/from-permission-flag-manifest-to-universal-flat-permission-flag.util';
import { fromPermissionFlagToUniversalFlatRolePermissionFlag } from 'src/engine/core-modules/application/application-manifest/converters/from-permission-flag-to-universal-flat-role-permission-flag.util';
import { fromRoleManifestToUniversalFlatRole } from 'src/engine/core-modules/application/application-manifest/converters/from-role-manifest-to-universal-flat-role.util';
import { fromSkillManifestToUniversalFlatSkill } from 'src/engine/core-modules/application/application-manifest/converters/from-skill-manifest-to-universal-flat-skill.util';
import { computeSearchVectorUniversalSettingsFromObjectManifest } from 'src/engine/core-modules/application/application-manifest/utils/compute-search-vector-universal-settings-from-object-manifest.util';
import { fromViewFieldGroupManifestToUniversalFlatViewFieldGroup } from 'src/engine/core-modules/application/application-manifest/converters/from-view-field-group-manifest-to-universal-flat-view-field-group.util';
import { fromViewFieldManifestToUniversalFlatViewField } from 'src/engine/core-modules/application/application-manifest/converters/from-view-field-manifest-to-universal-flat-view-field.util';
import { fromViewFilterGroupManifestToUniversalFlatViewFilterGroup } from 'src/engine/core-modules/application/application-manifest/converters/from-view-filter-group-manifest-to-universal-flat-view-filter-group.util';
import { fromViewFilterManifestToUniversalFlatViewFilter } from 'src/engine/core-modules/application/application-manifest/converters/from-view-filter-manifest-to-universal-flat-view-filter.util';
import { fromViewGroupManifestToUniversalFlatViewGroup } from 'src/engine/core-modules/application/application-manifest/converters/from-view-group-manifest-to-universal-flat-view-group.util';
import { fromViewManifestToUniversalFlatView } from 'src/engine/core-modules/application/application-manifest/converters/from-view-manifest-to-universal-flat-view.util';
import { fromViewSortManifestToUniversalFlatViewSort } from 'src/engine/core-modules/application/application-manifest/converters/from-view-sort-manifest-to-universal-flat-view-sort.util';
import { type FlatApplication } from 'src/engine/core-modules/application/types/flat-application.type';
import { fromAgentManifestToUniversalFlatAgent } from 'src/engine/core-modules/application/utils/from-agent-manifest-to-universal-flat-agent.util';
import { createEmptyAllFlatEntityMaps } from 'src/engine/metadata-modules/flat-entity/constant/create-empty-all-flat-entity-maps.constant';
import { type AllFlatEntityMaps } from 'src/engine/metadata-modules/flat-entity/types/all-flat-entity-maps.type';
import { type UniversalFlatFieldMetadata } from 'src/engine/workspace-manager/workspace-migration/universal-flat-entity/types/universal-flat-field-metadata.type';
import { addUniversalFlatEntityToUniversalFlatEntityMapsThroughMutationOrThrow } from 'src/engine/workspace-manager/workspace-migration/universal-flat-entity/utils/add-universal-flat-entity-to-universal-flat-entity-maps-through-mutation-or-throw.util';
export const computeApplicationManifestAllUniversalFlatEntityMaps = ({
manifest,
ownerFlatApplication,
now,
}: {
manifest: Manifest;
ownerFlatApplication: FlatApplication;
now: string;
}): AllFlatEntityMaps => {
const allUniversalFlatEntityMaps = createEmptyAllFlatEntityMaps();
const { universalIdentifier: applicationUniversalIdentifier } =
ownerFlatApplication;
for (const objectManifest of manifest.objects) {
const flatObjectMetadata = fromObjectManifestToUniversalFlatObjectMetadata({
objectManifest,
applicationUniversalIdentifier,
now,
});
addUniversalFlatEntityToUniversalFlatEntityMapsThroughMutationOrThrow({
universalFlatEntity: flatObjectMetadata,
universalFlatEntityMapsToMutate:
allUniversalFlatEntityMaps.flatObjectMetadataMaps,
});
for (const fieldManifest of objectManifest.fields) {
const enrichedFieldManifest =
fieldManifest.type === FieldMetadataType.TS_VECTOR &&
!isDefined(fieldManifest.universalSettings)
? {
...fieldManifest,
objectUniversalIdentifier: objectManifest.universalIdentifier,
universalSettings:
computeSearchVectorUniversalSettingsFromObjectManifest({
objectManifest,
}),
}
: {
...fieldManifest,
objectUniversalIdentifier: objectManifest.universalIdentifier,
};
const flatFieldMetadata = fromFieldManifestToUniversalFlatFieldMetadata({
fieldManifest: enrichedFieldManifest,
applicationUniversalIdentifier,
now,
});
addUniversalFlatEntityToUniversalFlatEntityMapsThroughMutationOrThrow({
universalFlatEntity: flatFieldMetadata,
universalFlatEntityMapsToMutate:
allUniversalFlatEntityMaps.flatFieldMetadataMaps,
});
if (flatFieldMetadata.isUnique) {
addUniversalFlatEntityToUniversalFlatEntityMapsThroughMutationOrThrow({
universalFlatEntity: generateIndexForFlatFieldMetadata({
flatFieldMetadata,
flatObjectMetadata,
}),
universalFlatEntityMapsToMutate:
allUniversalFlatEntityMaps.flatIndexMaps,
});
}
}
}
for (const fieldManifest of manifest.fields) {
const flatFieldMetadata = fromFieldManifestToUniversalFlatFieldMetadata({
fieldManifest: fieldManifest,
applicationUniversalIdentifier,
now,
});
addUniversalFlatEntityToUniversalFlatEntityMapsThroughMutationOrThrow({
universalFlatEntity: flatFieldMetadata,
universalFlatEntityMapsToMutate:
allUniversalFlatEntityMaps.flatFieldMetadataMaps,
});
if (flatFieldMetadata.isUnique) {
const flatObjectMetadata =
allUniversalFlatEntityMaps.flatObjectMetadataMaps.byUniversalIdentifier[
flatFieldMetadata.objectMetadataUniversalIdentifier
];
if (isDefined(flatObjectMetadata)) {
addUniversalFlatEntityToUniversalFlatEntityMapsThroughMutationOrThrow({
universalFlatEntity: generateIndexForFlatFieldMetadata({
flatFieldMetadata,
flatObjectMetadata,
}),
universalFlatEntityMapsToMutate:
allUniversalFlatEntityMaps.flatIndexMaps,
});
}
}
}
const indexCountByObjectUniversalIdentifier = new Map<string, number>();
const fieldsByObjectUniversalIdentifier = new Map<
string,
UniversalFlatFieldMetadata[]
>();
for (const flatField of Object.values(
allUniversalFlatEntityMaps.flatFieldMetadataMaps.byUniversalIdentifier,
)) {
if (!isDefined(flatField)) continue;
const bucket =
fieldsByObjectUniversalIdentifier.get(
flatField.objectMetadataUniversalIdentifier,
) ?? [];
if (bucket.length === 0) {
fieldsByObjectUniversalIdentifier.set(
flatField.objectMetadataUniversalIdentifier,
bucket,
);
}
bucket.push(flatField);
}
for (const indexManifest of manifest.indexes ?? []) {
const flatObjectMetadata =
allUniversalFlatEntityMaps.flatObjectMetadataMaps.byUniversalIdentifier[
indexManifest.objectUniversalIdentifier
];
if (!isDefined(flatObjectMetadata)) {
throw new Error(
`Index "${indexManifest.universalIdentifier}" references unknown object ${indexManifest.objectUniversalIdentifier}`,
);
}
const nextCount =
(indexCountByObjectUniversalIdentifier.get(
indexManifest.objectUniversalIdentifier,
) ?? 0) + 1;
if (nextCount > MAX_CUSTOM_INDEXES_PER_OBJECT) {
throw new Error(
`Application declares more than ${MAX_CUSTOM_INDEXES_PER_OBJECT} indexes on object ${indexManifest.objectUniversalIdentifier}`,
);
}
indexCountByObjectUniversalIdentifier.set(
indexManifest.objectUniversalIdentifier,
nextCount,
);
addUniversalFlatEntityToUniversalFlatEntityMapsThroughMutationOrThrow({
universalFlatEntity: fromIndexManifestToUniversalFlatIndex({
indexManifest,
flatObjectMetadata,
objectFlatFieldMetadatas:
fieldsByObjectUniversalIdentifier.get(
flatObjectMetadata.universalIdentifier,
) ?? [],
applicationUniversalIdentifier,
now,
}),
universalFlatEntityMapsToMutate: allUniversalFlatEntityMaps.flatIndexMaps,
});
}
for (const logicFunctionManifest of manifest.logicFunctions) {
addUniversalFlatEntityToUniversalFlatEntityMapsThroughMutationOrThrow({
universalFlatEntity:
fromLogicFunctionManifestToUniversalFlatLogicFunction({
logicFunctionManifest,
applicationUniversalIdentifier,
now,
}),
universalFlatEntityMapsToMutate:
allUniversalFlatEntityMaps.flatLogicFunctionMaps,
});
}
for (const frontComponentManifest of manifest.frontComponents) {
addUniversalFlatEntityToUniversalFlatEntityMapsThroughMutationOrThrow({
universalFlatEntity:
fromFrontComponentManifestToUniversalFlatFrontComponent({
frontComponentManifest,
applicationUniversalIdentifier,
now,
}),
universalFlatEntityMapsToMutate:
allUniversalFlatEntityMaps.flatFrontComponentMaps,
});
}
for (const connectionProviderManifest of manifest.connectionProviders ?? []) {
addUniversalFlatEntityToUniversalFlatEntityMapsThroughMutationOrThrow({
universalFlatEntity:
fromConnectionProviderManifestToUniversalFlatConnectionProvider({
connectionProviderManifest,
applicationUniversalIdentifier,
now,
}),
universalFlatEntityMapsToMutate:
allUniversalFlatEntityMaps.flatConnectionProviderMaps,
});
}
for (const permissionFlagManifest of manifest.permissionFlags ?? []) {
addUniversalFlatEntityToUniversalFlatEntityMapsThroughMutationOrThrow({
universalFlatEntity:
fromPermissionFlagManifestToUniversalFlatPermissionFlag({
permissionFlagManifest,
applicationUniversalIdentifier,
now,
}),
universalFlatEntityMapsToMutate:
allUniversalFlatEntityMaps.flatPermissionFlagMaps,
});
}
for (const roleManifest of manifest.roles) {
addUniversalFlatEntityToUniversalFlatEntityMapsThroughMutationOrThrow({
universalFlatEntity: fromRoleManifestToUniversalFlatRole({
roleManifest,
applicationUniversalIdentifier,
now,
}),
universalFlatEntityMapsToMutate: allUniversalFlatEntityMaps.flatRoleMaps,
});
for (const objectPermissionManifest of roleManifest.objectPermissions ??
[]) {
addUniversalFlatEntityToUniversalFlatEntityMapsThroughMutationOrThrow({
universalFlatEntity:
fromObjectPermissionManifestToUniversalFlatObjectPermission({
objectPermissionManifest,
roleUniversalIdentifier: roleManifest.universalIdentifier,
applicationUniversalIdentifier,
now,
}),
universalFlatEntityMapsToMutate:
allUniversalFlatEntityMaps.flatObjectPermissionMaps,
});
}
for (const fieldPermissionManifest of roleManifest.fieldPermissions ?? []) {
addUniversalFlatEntityToUniversalFlatEntityMapsThroughMutationOrThrow({
universalFlatEntity:
fromFieldPermissionManifestToUniversalFlatFieldPermission({
fieldPermissionManifest,
roleUniversalIdentifier: roleManifest.universalIdentifier,
applicationUniversalIdentifier,
now,
}),
universalFlatEntityMapsToMutate:
allUniversalFlatEntityMaps.flatFieldPermissionMaps,
});
}
for (const permissionFlagUniversalIdentifier of roleManifest.permissionFlagUniversalIdentifiers ??
[]) {
addUniversalFlatEntityToUniversalFlatEntityMapsThroughMutationOrThrow({
universalFlatEntity:
fromPermissionFlagToUniversalFlatRolePermissionFlag({
permissionFlagUniversalIdentifier,
roleUniversalIdentifier: roleManifest.universalIdentifier,
applicationUniversalIdentifier,
now,
}),
universalFlatEntityMapsToMutate:
allUniversalFlatEntityMaps.flatRolePermissionFlagMaps,
});
}
}
for (const skillManifest of manifest.skills ?? []) {
addUniversalFlatEntityToUniversalFlatEntityMapsThroughMutationOrThrow({
universalFlatEntity: fromSkillManifestToUniversalFlatSkill({
skillManifest,
applicationUniversalIdentifier,
now,
}),
universalFlatEntityMapsToMutate: allUniversalFlatEntityMaps.flatSkillMaps,
});
}
for (const agentManifest of manifest.agents ?? []) {
addUniversalFlatEntityToUniversalFlatEntityMapsThroughMutationOrThrow({
universalFlatEntity: fromAgentManifestToUniversalFlatAgent({
agentManifest,
applicationUniversalIdentifier,
now,
}),
universalFlatEntityMapsToMutate: allUniversalFlatEntityMaps.flatAgentMaps,
});
}
for (const viewManifest of manifest.views ?? []) {
addUniversalFlatEntityToUniversalFlatEntityMapsThroughMutationOrThrow({
universalFlatEntity: fromViewManifestToUniversalFlatView({
viewManifest,
applicationUniversalIdentifier,
now,
}),
universalFlatEntityMapsToMutate: allUniversalFlatEntityMaps.flatViewMaps,
});
for (const viewFieldGroupManifest of viewManifest.fieldGroups ?? []) {
addUniversalFlatEntityToUniversalFlatEntityMapsThroughMutationOrThrow({
universalFlatEntity:
fromViewFieldGroupManifestToUniversalFlatViewFieldGroup({
viewFieldGroupManifest,
viewUniversalIdentifier: viewManifest.universalIdentifier,
applicationUniversalIdentifier,
now,
}),
universalFlatEntityMapsToMutate:
allUniversalFlatEntityMaps.flatViewFieldGroupMaps,
});
}
for (const viewFieldManifest of viewManifest.fields ?? []) {
addUniversalFlatEntityToUniversalFlatEntityMapsThroughMutationOrThrow({
universalFlatEntity: fromViewFieldManifestToUniversalFlatViewField({
viewFieldManifest,
viewUniversalIdentifier: viewManifest.universalIdentifier,
applicationUniversalIdentifier,
now,
}),
universalFlatEntityMapsToMutate:
allUniversalFlatEntityMaps.flatViewFieldMaps,
});
}
for (const viewFilterGroupManifest of viewManifest.filterGroups ?? []) {
addUniversalFlatEntityToUniversalFlatEntityMapsThroughMutationOrThrow({
universalFlatEntity:
fromViewFilterGroupManifestToUniversalFlatViewFilterGroup({
viewFilterGroupManifest,
viewUniversalIdentifier: viewManifest.universalIdentifier,
applicationUniversalIdentifier,
now,
}),
universalFlatEntityMapsToMutate:
allUniversalFlatEntityMaps.flatViewFilterGroupMaps,
});
}
for (const viewFilterManifest of viewManifest.filters ?? []) {
addUniversalFlatEntityToUniversalFlatEntityMapsThroughMutationOrThrow({
universalFlatEntity: fromViewFilterManifestToUniversalFlatViewFilter({
viewFilterManifest,
viewUniversalIdentifier: viewManifest.universalIdentifier,
applicationUniversalIdentifier,
now,
}),
universalFlatEntityMapsToMutate:
allUniversalFlatEntityMaps.flatViewFilterMaps,
});
}
for (const viewGroupManifest of viewManifest.groups ?? []) {
addUniversalFlatEntityToUniversalFlatEntityMapsThroughMutationOrThrow({
universalFlatEntity: fromViewGroupManifestToUniversalFlatViewGroup({
viewGroupManifest,
viewUniversalIdentifier: viewManifest.universalIdentifier,
applicationUniversalIdentifier,
now,
}),
universalFlatEntityMapsToMutate:
allUniversalFlatEntityMaps.flatViewGroupMaps,
});
}
for (const viewSortManifest of viewManifest.sorts ?? []) {
addUniversalFlatEntityToUniversalFlatEntityMapsThroughMutationOrThrow({
universalFlatEntity: fromViewSortManifestToUniversalFlatViewSort({
viewSortManifest,
viewUniversalIdentifier: viewManifest.universalIdentifier,
applicationUniversalIdentifier,
now,
}),
universalFlatEntityMapsToMutate:
allUniversalFlatEntityMaps.flatViewSortMaps,
});
}
}
for (const navigationMenuItemManifest of manifest.navigationMenuItems ?? []) {
addUniversalFlatEntityToUniversalFlatEntityMapsThroughMutationOrThrow({
universalFlatEntity:
fromNavigationMenuItemManifestToUniversalFlatNavigationMenuItem({
navigationMenuItemManifest,
applicationUniversalIdentifier,
now,
}),
universalFlatEntityMapsToMutate:
allUniversalFlatEntityMaps.flatNavigationMenuItemMaps,
});
}
for (const pageLayoutManifest of manifest.pageLayouts ?? []) {
addUniversalFlatEntityToUniversalFlatEntityMapsThroughMutationOrThrow({
universalFlatEntity: fromPageLayoutManifestToUniversalFlatPageLayout({
pageLayoutManifest,
applicationUniversalIdentifier,
now,
}),
universalFlatEntityMapsToMutate:
allUniversalFlatEntityMaps.flatPageLayoutMaps,
});
for (const pageLayoutTabManifest of pageLayoutManifest.tabs ?? []) {
addUniversalFlatEntityToUniversalFlatEntityMapsThroughMutationOrThrow({
universalFlatEntity:
fromPageLayoutTabManifestToUniversalFlatPageLayoutTab({
pageLayoutTabManifest,
pageLayoutUniversalIdentifier:
pageLayoutManifest.universalIdentifier,
applicationUniversalIdentifier,
now,
}),
universalFlatEntityMapsToMutate:
allUniversalFlatEntityMaps.flatPageLayoutTabMaps,
});
for (const pageLayoutWidgetManifest of pageLayoutTabManifest.widgets ??
[]) {
addUniversalFlatEntityToUniversalFlatEntityMapsThroughMutationOrThrow({
universalFlatEntity:
fromPageLayoutWidgetManifestToUniversalFlatPageLayoutWidget({
pageLayoutWidgetManifest,
pageLayoutTabUniversalIdentifier:
pageLayoutTabManifest.universalIdentifier,
applicationUniversalIdentifier,
now,
}),
universalFlatEntityMapsToMutate:
allUniversalFlatEntityMaps.flatPageLayoutWidgetMaps,
});
}
}
}
for (const pageLayoutTabManifest of manifest.pageLayoutTabs ?? []) {
if (!isDefined(pageLayoutTabManifest.pageLayoutUniversalIdentifier)) {
throw new Error(
`Top-level pageLayoutTab "${pageLayoutTabManifest.universalIdentifier}" is missing required pageLayoutUniversalIdentifier`,
);
}
addUniversalFlatEntityToUniversalFlatEntityMapsThroughMutationOrThrow({
universalFlatEntity:
fromPageLayoutTabManifestToUniversalFlatPageLayoutTab({
pageLayoutTabManifest,
pageLayoutUniversalIdentifier:
pageLayoutTabManifest.pageLayoutUniversalIdentifier,
applicationUniversalIdentifier,
now,
}),
universalFlatEntityMapsToMutate:
allUniversalFlatEntityMaps.flatPageLayoutTabMaps,
});
for (const pageLayoutWidgetManifest of pageLayoutTabManifest.widgets ??
[]) {
addUniversalFlatEntityToUniversalFlatEntityMapsThroughMutationOrThrow({
universalFlatEntity:
fromPageLayoutWidgetManifestToUniversalFlatPageLayoutWidget({
pageLayoutWidgetManifest,
pageLayoutTabUniversalIdentifier:
pageLayoutTabManifest.universalIdentifier,
applicationUniversalIdentifier,
now,
}),
universalFlatEntityMapsToMutate:
allUniversalFlatEntityMaps.flatPageLayoutWidgetMaps,
});
}
}
for (const [key, applicationVariableManifest] of Object.entries(
manifest.application.applicationVariables ?? {},
)) {
addUniversalFlatEntityToUniversalFlatEntityMapsThroughMutationOrThrow({
universalFlatEntity:
fromApplicationVariableManifestToUniversalFlatApplicationVariable({
key,
universalIdentifier: applicationVariableManifest.universalIdentifier,
value:
'value' in applicationVariableManifest
? applicationVariableManifest.value
: undefined,
description: applicationVariableManifest.description,
isSecret: applicationVariableManifest.isSecret,
applicationUniversalIdentifier,
now,
}),
universalFlatEntityMapsToMutate:
allUniversalFlatEntityMaps.flatApplicationVariableMaps,
});
}
for (const commandMenuItemManifest of manifest.commandMenuItems ?? []) {
if (!isDefined(commandMenuItemManifest.frontComponentUniversalIdentifier)) {
throw new Error(
`Top-level commandMenuItem "${commandMenuItemManifest.universalIdentifier}" is missing required frontComponentUniversalIdentifier`,
);
}
addUniversalFlatEntityToUniversalFlatEntityMapsThroughMutationOrThrow({
universalFlatEntity:
fromCommandMenuItemManifestToUniversalFlatCommandMenuItem({
commandMenuItemManifest,
applicationUniversalIdentifier,
now,
}),
universalFlatEntityMapsToMutate:
allUniversalFlatEntityMaps.flatCommandMenuItemMaps,
});
}
return allUniversalFlatEntityMaps;
};
@@ -17,6 +17,7 @@ import {
import { UUIDScalarType } from 'src/engine/api/graphql/workspace-schema-builder/graphql-types/scalars';
import { ApplicationRegistrationEntity } from 'src/engine/core-modules/application/application-registration/application-registration.entity';
import { type EncryptedString } from 'src/engine/core-modules/secret-encryption/branded-strings/encrypted-string.type';
@Entity({ name: 'applicationRegistrationVariable', schema: 'core' })
@ObjectType('ApplicationRegistrationVariable')
@@ -42,7 +43,7 @@ export class ApplicationRegistrationVariableEntity {
key: string;
@Column({ nullable: false, type: 'text', default: '' })
encryptedValue: string;
encryptedValue: EncryptedString | '';
@Field()
@Column({ nullable: false, type: 'text', default: '' })
@@ -48,15 +48,20 @@ export class ApplicationRegistrationVariableService {
order: { key: 'ASC' },
});
return variables.map((variable) => ({
...variable,
isFilled: variable.isFilled,
value: variable.isFilled
? variable.isSecret
? '•••••••••••••'
: this.encryptionService.decryptVersioned(variable.encryptedValue)
: null,
}));
return variables.map((variable) => {
const { encryptedValue } = variable;
return {
...variable,
isFilled: variable.isFilled,
value:
encryptedValue !== ''
? variable.isSecret
? '•••••••••••••'
: this.encryptionService.decryptVersioned(encryptedValue)
: null,
};
});
}
async createVariable(
@@ -8,6 +8,8 @@ import {
MaxLength,
} from 'class-validator';
import { type PlaintextString } from 'src/engine/core-modules/secret-encryption/branded-strings/plaintext-string.type';
@InputType()
export class CreateApplicationRegistrationVariableInput {
@Field()
@@ -19,10 +21,10 @@ export class CreateApplicationRegistrationVariableInput {
@MaxLength(256)
key: string;
@Field()
@Field(() => String)
@IsString()
@MaxLength(10000)
value: string;
value: PlaintextString;
@Field({ nullable: true })
@IsString()
@@ -10,13 +10,15 @@ import {
ValidateNested,
} from 'class-validator';
import { type PlaintextString } from 'src/engine/core-modules/secret-encryption/branded-strings/plaintext-string.type';
@InputType()
export class UpdateApplicationRegistrationVariablePayload {
@Field({ nullable: true })
@Field(() => String, { nullable: true })
@IsString()
@MaxLength(10000)
@IsOptional()
value?: string;
value?: PlaintextString;
@Field({ nullable: true })
@IsOptional()
@@ -10,6 +10,7 @@ import {
} from 'src/engine/core-modules/application/application-variable/application-variable.exception';
import { ApplicationVariableEntityService } from 'src/engine/core-modules/application/application-variable/application-variable.service';
import { SECRET_APPLICATION_VARIABLE_MASK } from 'src/engine/core-modules/application/application-variable/constants/secret-application-variable-mask.constant';
import { type PlaintextString } from 'src/engine/core-modules/secret-encryption/branded-strings/plaintext-string.type';
import { SecretEncryptionService } from 'src/engine/core-modules/secret-encryption/secret-encryption.service';
import { WorkspaceCacheService } from 'src/engine/workspace-cache/services/workspace-cache.service';
@@ -96,7 +97,7 @@ describe('ApplicationVariableEntityService', () => {
await service.update({
key: 'API_KEY',
plainTextValue: 'new-secret-value',
plainTextValue: 'new-secret-value' as PlaintextString,
applicationId: mockApplicationId,
workspaceId: mockWorkspaceId,
});
@@ -115,7 +116,7 @@ describe('ApplicationVariableEntityService', () => {
);
});
it('should not encrypt value when variable is not secret', async () => {
it('should encrypt value even when variable is not secret', async () => {
const existingVariable = {
id: '1',
key: 'PUBLIC_URL',
@@ -129,15 +130,18 @@ describe('ApplicationVariableEntityService', () => {
await service.update({
key: 'PUBLIC_URL',
plainTextValue: 'https://new-url.com',
plainTextValue: 'https://new-url.com' as PlaintextString,
applicationId: mockApplicationId,
workspaceId: mockWorkspaceId,
});
expect(secretEncryptionService.encryptVersioned).not.toHaveBeenCalled();
expect(secretEncryptionService.encryptVersioned).toHaveBeenCalledWith(
'https://new-url.com',
{ workspaceId: mockWorkspaceId },
);
expect(repository.update).toHaveBeenCalledWith(
{ key: 'PUBLIC_URL', applicationId: mockApplicationId },
{ value: 'https://new-url.com' },
{ value: `enc:v2:deadbeef:https://new-url.com|${mockWorkspaceId}` },
);
});
@@ -147,7 +151,7 @@ describe('ApplicationVariableEntityService', () => {
await expect(
service.update({
key: 'NON_EXISTENT',
plainTextValue: 'some-value',
plainTextValue: 'some-value' as PlaintextString,
applicationId: mockApplicationId,
workspaceId: mockWorkspaceId,
}),
@@ -156,7 +160,7 @@ describe('ApplicationVariableEntityService', () => {
await expect(
service.update({
key: 'NON_EXISTENT',
plainTextValue: 'some-value',
plainTextValue: 'some-value' as PlaintextString,
applicationId: mockApplicationId,
workspaceId: mockWorkspaceId,
}),
@@ -11,6 +11,7 @@ import {
} from 'typeorm';
import { UUIDScalarType } from 'src/engine/api/graphql/workspace-schema-builder/graphql-types/scalars';
import { type EncryptedString } from 'src/engine/core-modules/secret-encryption/branded-strings/encrypted-string.type';
import { SyncableEntity } from 'src/engine/workspace-manager/types/syncable-entity.interface';
@Entity({
@@ -18,13 +19,11 @@ import { SyncableEntity } from 'src/engine/workspace-manager/types/syncable-enti
schema: 'core',
})
@ObjectType('ApplicationVariable')
// Constrains `value` for secret rows to the versioned envelope, while
// leaving plaintext non-secret values untouched. The keyId portion is
// not constrained so future ENCRYPTION_KEY rotations do not need a DDL
// migration.
// All values are always encrypted regardless of `isSecret`. The
// `isSecret` flag only controls display behavior (masked vs plaintext).
@Check(
'CHK_applicationVariable_value_encrypted',
`"isSecret" = false OR "value" = '' OR "value" LIKE 'enc:v2:%'`,
`"value" = '' OR "value" LIKE 'enc:v2:%'`,
)
export class ApplicationVariableEntity extends SyncableEntity {
@IDField(() => UUIDScalarType)
@@ -35,7 +34,7 @@ export class ApplicationVariableEntity extends SyncableEntity {
key: string;
@Column({ nullable: false, type: 'text', default: '' })
value: string;
value: EncryptedString | '';
@Column({ nullable: false, type: 'text', default: '' })
description: string;
@@ -10,9 +10,9 @@ import {
ApplicationVariableEntityExceptionCode,
} from 'src/engine/core-modules/application/application-variable/application-variable.exception';
import { SECRET_APPLICATION_VARIABLE_MASK } from 'src/engine/core-modules/application/application-variable/constants/secret-application-variable-mask.constant';
import { type PlaintextString } from 'src/engine/core-modules/secret-encryption/branded-strings/plaintext-string.type';
import { SecretEncryptionService } from 'src/engine/core-modules/secret-encryption/secret-encryption.service';
import { WorkspaceCacheService } from 'src/engine/workspace-cache/services/workspace-cache.service';
import { isNonEmptyString } from '@sniptt/guards';
@Injectable()
export class ApplicationVariableEntityService {
@@ -24,19 +24,22 @@ export class ApplicationVariableEntityService {
) {}
getDisplayValue(applicationVariable: ApplicationVariableEntity): string {
if (!applicationVariable.isSecret) {
return applicationVariable.value;
}
if (!isNonEmptyString(applicationVariable.value)) {
if (applicationVariable.value === '') {
return '';
}
return this.secretEncryptionService.decryptAndMaskVersioned({
value: applicationVariable.value,
mask: SECRET_APPLICATION_VARIABLE_MASK,
workspaceId: applicationVariable.workspaceId,
});
if (applicationVariable.isSecret) {
return this.secretEncryptionService.decryptAndMaskVersioned({
value: applicationVariable.value,
mask: SECRET_APPLICATION_VARIABLE_MASK,
workspaceId: applicationVariable.workspaceId,
});
}
return this.secretEncryptionService.decryptVersioned(
applicationVariable.value,
{ workspaceId: applicationVariable.workspaceId },
);
}
async update({
@@ -47,7 +50,7 @@ export class ApplicationVariableEntityService {
}: Pick<ApplicationVariableEntity, 'key'> & {
applicationId: string;
workspaceId: string;
plainTextValue: string;
plainTextValue: PlaintextString;
}) {
const existingVariable = await this.applicationVariableRepository.findOne({
where: { key, applicationId },
@@ -60,16 +63,12 @@ export class ApplicationVariableEntityService {
);
}
const encryptedValue = existingVariable.isSecret
? this.secretEncryptionService.encryptVersioned(plainTextValue, {
workspaceId,
})
: plainTextValue;
await this.applicationVariableRepository.update(
{ key, applicationId },
{
value: encryptedValue,
value: this.secretEncryptionService.encryptVersioned(plainTextValue, {
workspaceId,
}),
},
);
@@ -1,5 +1,6 @@
import { ArgsType, Field } from '@nestjs/graphql';
import { type PlaintextString } from 'src/engine/core-modules/secret-encryption/branded-strings/plaintext-string.type';
import { UUIDScalarType } from 'src/engine/api/graphql/workspace-schema-builder/graphql-types/scalars';
@ArgsType()
@@ -8,7 +9,7 @@ export class UpdateApplicationVariableEntityInput {
key: string;
@Field(() => String, { nullable: false })
value: string;
value: PlaintextString;
@Field(() => UUIDScalarType, { nullable: false })
applicationId: string;
@@ -247,7 +247,9 @@ export class ConnectionProviderOAuthFlowService {
const { encryptedAccessToken, encryptedRefreshToken } =
this.connectedAccountTokenEncryptionService.encryptTokenPair({
accessToken: tokenResponse.accessToken,
refreshToken: tokenResponse.refreshToken,
refreshToken: isDefined(tokenResponse.refreshToken)
? tokenResponse.refreshToken
: null,
workspaceId,
});
@@ -4,12 +4,12 @@ import { InjectRepository } from '@nestjs/typeorm';
import { isDefined } from 'twenty-shared/utils';
import { In, Repository } from 'typeorm';
import { ConnectionProviderEntity } from 'src/engine/core-modules/application/connection-provider/connection-provider.entity';
import { ConnectionProviderExceptionCode } from 'src/engine/core-modules/application/connection-provider/connection-provider-exception-code.enum';
import { ConnectionProviderException } from 'src/engine/core-modules/application/connection-provider/connection-provider.exception';
import { assertOAuthProvider } from 'src/engine/core-modules/application/connection-provider/utils/assert-oauth-provider.util';
import { ApplicationRegistrationVariableEntity } from 'src/engine/core-modules/application/application-registration-variable/application-registration-variable.entity';
import { ApplicationEntity } from 'src/engine/core-modules/application/application.entity';
import { ConnectionProviderExceptionCode } from 'src/engine/core-modules/application/connection-provider/connection-provider-exception-code.enum';
import { ConnectionProviderEntity } from 'src/engine/core-modules/application/connection-provider/connection-provider.entity';
import { ConnectionProviderException } from 'src/engine/core-modules/application/connection-provider/connection-provider.exception';
import { assertOAuthProvider } from 'src/engine/core-modules/application/connection-provider/utils/assert-oauth-provider.util';
import { SecretEncryptionService } from 'src/engine/core-modules/secret-encryption/secret-encryption.service';
@Injectable()
@@ -52,7 +52,7 @@ export class ConnectionProviderService {
const valuesByKey = new Map(
variables.map((v) => [
v.key,
v.encryptedValue
v.encryptedValue !== ''
? this.secretEncryptionService.decryptVersioned(v.encryptedValue)
: '',
]),
@@ -5,15 +5,16 @@ import { isDefined } from 'twenty-shared/utils';
import { ConnectionProviderException } from 'src/engine/core-modules/application/connection-provider/connection-provider.exception';
import { ConnectionProviderService } from 'src/engine/core-modules/application/connection-provider/connection-provider.service';
import { assertOAuthProvider } from 'src/engine/core-modules/application/connection-provider/utils/assert-oauth-provider.util';
import { type ConnectedAccountTokens } from 'src/modules/connected-account/refresh-tokens-manager/services/connected-account-refresh-tokens.service';
import { exchangeRefreshTokenForToken } from 'src/engine/core-modules/application/connection-provider/utils/exchange-refresh-token-for-token.util';
import { OAuthTokenEndpointError } from 'src/engine/core-modules/application/connection-provider/utils/post-oauth-token-request.util';
import { type PlaintextString } from 'src/engine/core-modules/secret-encryption/branded-strings/plaintext-string.type';
import { SecureHttpClientService } from 'src/engine/core-modules/secure-http-client/secure-http-client.service';
import { type ConnectedAccountEntity } from 'src/engine/metadata-modules/connected-account/entities/connected-account.entity';
import {
ConnectedAccountRefreshAccessTokenException,
ConnectedAccountRefreshAccessTokenExceptionCode,
} from 'src/engine/metadata-modules/connected-account/exceptions/connected-account-refresh-tokens.exception';
import { type ConnectedAccountPlaintextTokens } from 'src/modules/connected-account/refresh-tokens-manager/services/connected-account-refresh-tokens.service';
@Injectable()
export class AppOAuthRefreshAccessTokenService {
@@ -26,8 +27,8 @@ export class AppOAuthRefreshAccessTokenService {
async refreshTokens(
connectedAccount: ConnectedAccountEntity,
refreshToken: string,
): Promise<ConnectedAccountTokens> {
refreshToken: PlaintextString,
): Promise<ConnectedAccountPlaintextTokens> {
if (!isDefined(connectedAccount.connectionProviderId)) {
throw new ConnectedAccountRefreshAccessTokenException(
`Connected account ${connectedAccount.id} has no connectionProviderId`,
@@ -53,7 +54,9 @@ export class AppOAuthRefreshAccessTokenService {
accessToken: tokenResponse.accessToken,
// Fall back to the original when the response omits one — some
// providers don't rotate refresh tokens.
refreshToken: tokenResponse.refreshToken ?? refreshToken,
refreshToken: isDefined(tokenResponse.refreshToken)
? tokenResponse.refreshToken
: refreshToken,
};
} catch (error) {
this.logger.warn(
@@ -1,5 +1,7 @@
import { PlaintextString } from 'src/engine/core-modules/secret-encryption/branded-strings';
export type TokenExchangeResponse = {
accessToken: string;
refreshToken: string | null;
accessToken: PlaintextString;
refreshToken: PlaintextString | null;
scopes: string[] | null;
};
@@ -1,10 +1,13 @@
import { type TokenExchangeResponse } from 'src/engine/core-modules/application/connection-provider/types/token-exchange-response.type';
import { type PlaintextString } from 'src/engine/core-modules/secret-encryption/branded-strings/plaintext-string.type';
export const parseTokenResponse = (
json: Record<string, unknown>,
): TokenExchangeResponse => {
const accessToken =
typeof json.access_token === 'string' ? json.access_token : null;
typeof json.access_token === 'string'
? (json.access_token as PlaintextString)
: null;
if (!accessToken) {
throw new Error(
@@ -15,7 +18,9 @@ export const parseTokenResponse = (
return {
accessToken,
refreshToken:
typeof json.refresh_token === 'string' ? json.refresh_token : null,
typeof json.refresh_token === 'string'
? (json.refresh_token as PlaintextString)
: null,
scopes:
typeof json.scope === 'string'
? json.scope.split(/[\s,]+/).filter(Boolean)