feat(twenty-server): introduce ENCRYPTION_KEY env var with versioned envelope (#20528)
## Summary
- Adds `ENCRYPTION_KEY` (primary) and `FALLBACK_ENCRYPTION_KEY`
(decrypt-only fallback for rotation) env vars to twenty-server, with
backward-compatible fallback to `APP_SECRET` when `ENCRYPTION_KEY` is
unset.
- Introduces a versioned ciphertext envelope `enc:v2:<keyId>:<base64>`
using AES-256-GCM with HKDF-SHA256 derived per-context keys. The 8-hex
`keyId` fingerprint lets every row identify which physical key encrypted
it, so rotation routes directly to primary or fallback without trial
decryption; GCM's auth tag gives true integrity (legacy CTR has none).
- Migrates `ConnectedAccountTokenEncryptionService` to the new envelope
and plumbs `workspaceId` through every caller, so per-workspace HKDF
context binds each row to its tenant.
The remaining encryption sites (`jwt-key-manager`, `config-storage`,
`postgres-credentials`, `application-variable`, TOTP) stay on the legacy
unprefixed CTR path and will be migrated in follow-up PRs. The
operator-facing rotation runbook is out of scope here.
### Format details
`enc:v{N}:{keyId}:{base64}` — `N=2` is the only version produced by new
writes (`v1` exists for backward-compatible decryption of existing
connected-account rows). `keyId =
sha256(rawKey).slice(0,4).toString('hex')`. The CHECK constraint on
`core.connectedAccount.{accessToken,refreshToken}` is relaxed from `LIKE
'enc:v1:%'` to `LIKE 'enc:v_:%'` so both versions pass.
### Key resolution
| `ENCRYPTION_KEY` | `FALLBACK_ENCRYPTION_KEY` | `APP_SECRET` | Encrypt
with | Decrypt try order |
|---|---|---|---|---|
| set | set | (any) | `ENCRYPTION_KEY` | match `keyId` → primary →
fallback |
| set | unset | (any) | `ENCRYPTION_KEY` | match `keyId` → primary |
| unset | set | set | `APP_SECRET` | match `keyId` → `APP_SECRET` →
fallback |
| unset | unset | set | `APP_SECRET` | match `keyId` → `APP_SECRET` |
| unset | unset | unset | startup error | n/a |
## Test plan
- [x] `npx nx typecheck twenty-server` — clean
- [x] `npx jest
'secret-encryption|connected-account-token-encryption|connected-account-refresh-tokens|encrypt-connected-account-tokens|connection-provider-oauth-flow'`
— 87 tests pass
- [x] New `secret-encryption.service.versioned.spec.ts` covers: key
resolution table (no-key error, APP_SECRET fallback, ENCRYPTION_KEY
precedence), v2 round-trip with/without workspaceId, GCM tamper
rejection, workspaceId-mismatch rejection, keyId-based primary→fallback
routing, missing-key error names the fingerprint, v1 legacy decryption,
no-prefix legacy decryption, malformed envelope rejection.
- [x] Updated `connected-account-token-encryption.service.spec.ts`
covers workspaceId binding and HKDF context isolation.
- [x] Updated slow instance command spec verifies workspaceId is
threaded through encryption and the relaxed `enc:v_:%` LIKE pattern
matches both v1 and v2.
- [ ] Manual E2E: connect a Gmail account on a freshly deployed instance
with `APP_SECRET` only → confirm `core.connectedAccount.accessToken` is
`enc:v2:<keyId>:<base64>`.
- [ ] Manual E2E: rotate — set `ENCRYPTION_KEY=<new>` and
`FALLBACK_ENCRYPTION_KEY=<old APP_SECRET>`, restart, confirm
pre-rotation rows still decrypt and new rows carry the new `keyId`.
- [ ] Manual E2E: missing key — set `ENCRYPTION_KEY=<new>` without the
fallback, confirm decrypt error names the old `keyId` so the operator
can identify the missing key.
This commit is contained in:
+1
-1
@@ -22,7 +22,7 @@ export class GoogleCalendarGetEventsService {
|
||||
public async getCalendarEvents(
|
||||
connectedAccount: Pick<
|
||||
ConnectedAccountEntity,
|
||||
'provider' | 'refreshToken' | 'id'
|
||||
'provider' | 'refreshToken' | 'id' | 'workspaceId'
|
||||
>,
|
||||
syncCursor?: string,
|
||||
): Promise<GetCalendarEventsResponse> {
|
||||
|
||||
+1
-1
@@ -20,7 +20,7 @@ export class MicrosoftCalendarGetEventsService {
|
||||
public async getCalendarEvents(
|
||||
connectedAccount: Pick<
|
||||
ConnectedAccountEntity,
|
||||
'provider' | 'accessToken' | 'refreshToken' | 'id'
|
||||
'provider' | 'accessToken' | 'refreshToken' | 'id' | 'workspaceId'
|
||||
>,
|
||||
syncCursor?: string,
|
||||
): Promise<GetCalendarEventsResponse> {
|
||||
|
||||
+1
-1
@@ -17,7 +17,7 @@ export class MicrosoftCalendarImportEventsService {
|
||||
public async getCalendarEvents(
|
||||
connectedAccount: Pick<
|
||||
ConnectedAccountEntity,
|
||||
'provider' | 'accessToken' | 'refreshToken' | 'id'
|
||||
'provider' | 'accessToken' | 'refreshToken' | 'id' | 'workspaceId'
|
||||
>,
|
||||
changedEventIds: string[],
|
||||
): Promise<FetchedCalendarEvent[]> {
|
||||
|
||||
+1
@@ -36,6 +36,7 @@ export class CalendarGetCalendarEventsService {
|
||||
| 'id'
|
||||
| 'connectionParameters'
|
||||
| 'handle'
|
||||
| 'workspaceId'
|
||||
>,
|
||||
syncCursor?: string,
|
||||
): Promise<GetCalendarEventsResponse> {
|
||||
|
||||
+16
-8
@@ -19,7 +19,10 @@ export class OAuth2ClientManagerService {
|
||||
) {}
|
||||
|
||||
public async getGoogleOAuth2Client(
|
||||
connectedAccount: Pick<ConnectedAccountEntity, 'provider' | 'refreshToken'>,
|
||||
connectedAccount: Pick<
|
||||
ConnectedAccountEntity,
|
||||
'provider' | 'refreshToken' | 'workspaceId'
|
||||
>,
|
||||
): Promise<Auth.OAuth2Client> {
|
||||
if (!isDefined(connectedAccount.refreshToken)) {
|
||||
throw new CustomError(
|
||||
@@ -29,14 +32,18 @@ export class OAuth2ClientManagerService {
|
||||
}
|
||||
|
||||
return this.googleOAuth2ClientManagerService.getOAuth2Client(
|
||||
this.connectedAccountTokenEncryptionService.decrypt(
|
||||
connectedAccount.refreshToken,
|
||||
),
|
||||
this.connectedAccountTokenEncryptionService.decrypt({
|
||||
ciphertext: connectedAccount.refreshToken,
|
||||
workspaceId: connectedAccount.workspaceId,
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
public async getMicrosoftOAuth2Client(
|
||||
connectedAccount: Pick<ConnectedAccountEntity, 'provider' | 'accessToken'>,
|
||||
connectedAccount: Pick<
|
||||
ConnectedAccountEntity,
|
||||
'provider' | 'accessToken' | 'workspaceId'
|
||||
>,
|
||||
): Promise<Client> {
|
||||
if (!isDefined(connectedAccount.accessToken)) {
|
||||
throw new CustomError(
|
||||
@@ -46,9 +53,10 @@ export class OAuth2ClientManagerService {
|
||||
}
|
||||
|
||||
return this.microsoftOAuth2ClientManagerService.getOAuth2Client(
|
||||
this.connectedAccountTokenEncryptionService.decrypt(
|
||||
connectedAccount.accessToken,
|
||||
),
|
||||
this.connectedAccountTokenEncryptionService.decrypt({
|
||||
ciphertext: connectedAccount.accessToken,
|
||||
workspaceId: connectedAccount.workspaceId,
|
||||
}),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
+42
-34
@@ -2,23 +2,24 @@ import { Test, type TestingModule } from '@nestjs/testing';
|
||||
import { getRepositoryToken } from '@nestjs/typeorm';
|
||||
|
||||
import { ConnectedAccountProvider } from 'twenty-shared/types';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
|
||||
import { AppOAuthRefreshAccessTokenService } from 'src/engine/core-modules/application/connection-provider/refresh/services/app-oauth-refresh-tokens.service';
|
||||
import { SECRET_ENCRYPTION_ENVELOPE_V2_PREFIX } from 'src/engine/core-modules/secret-encryption/constants/secret-encryption.constant';
|
||||
import { ConnectedAccountEntity } from 'src/engine/metadata-modules/connected-account/entities/connected-account.entity';
|
||||
import {
|
||||
CONNECTED_ACCOUNT_TOKEN_ENCRYPTION_PREFIX,
|
||||
ConnectedAccountTokenEncryptionService,
|
||||
} from 'src/engine/metadata-modules/connected-account/services/connected-account-token-encryption.service';
|
||||
import { GlobalWorkspaceOrmManager } from 'src/engine/twenty-orm/global-workspace-datasource/global-workspace-orm.manager';
|
||||
import { GoogleAPIRefreshAccessTokenService } from 'src/modules/connected-account/refresh-tokens-manager/drivers/google/services/google-api-refresh-tokens.service';
|
||||
import { MicrosoftAPIRefreshAccessTokenService } from 'src/modules/connected-account/refresh-tokens-manager/drivers/microsoft/services/microsoft-api-refresh-tokens.service';
|
||||
import {
|
||||
ConnectedAccountRefreshAccessTokenException,
|
||||
ConnectedAccountRefreshAccessTokenExceptionCode,
|
||||
} from 'src/engine/metadata-modules/connected-account/exceptions/connected-account-refresh-tokens.exception';
|
||||
import { ConnectedAccountTokenEncryptionService } from 'src/engine/metadata-modules/connected-account/services/connected-account-token-encryption.service';
|
||||
import { GlobalWorkspaceOrmManager } from 'src/engine/twenty-orm/global-workspace-datasource/global-workspace-orm.manager';
|
||||
import { GoogleAPIRefreshAccessTokenService } from 'src/modules/connected-account/refresh-tokens-manager/drivers/google/services/google-api-refresh-tokens.service';
|
||||
import { MicrosoftAPIRefreshAccessTokenService } from 'src/modules/connected-account/refresh-tokens-manager/drivers/microsoft/services/microsoft-api-refresh-tokens.service';
|
||||
|
||||
import { ConnectedAccountRefreshTokensService } from './connected-account-refresh-tokens.service';
|
||||
|
||||
const FAKE_CIPHER_PREFIX = `${SECRET_ENCRYPTION_ENVELOPE_V2_PREFIX}keyid:`;
|
||||
|
||||
describe('ConnectedAccountRefreshTokensService', () => {
|
||||
let service: ConnectedAccountRefreshTokensService;
|
||||
let googleAPIRefreshAccessTokenService: GoogleAPIRefreshAccessTokenService;
|
||||
@@ -36,8 +37,8 @@ describe('ConnectedAccountRefreshTokensService', () => {
|
||||
const mockRefreshTokenPlaintext = 'valid-refresh-token';
|
||||
const mockNewAccessTokenPlaintext = 'new-access-token';
|
||||
|
||||
const mockEncryptedAccessToken = `${CONNECTED_ACCOUNT_TOKEN_ENCRYPTION_PREFIX}CIPHER(${mockAccessTokenPlaintext})`;
|
||||
const mockEncryptedRefreshToken = `${CONNECTED_ACCOUNT_TOKEN_ENCRYPTION_PREFIX}CIPHER(${mockRefreshTokenPlaintext})`;
|
||||
const mockEncryptedAccessToken = `${FAKE_CIPHER_PREFIX}CIPHER(${mockAccessTokenPlaintext})`;
|
||||
const mockEncryptedRefreshToken = `${FAKE_CIPHER_PREFIX}CIPHER(${mockRefreshTokenPlaintext})`;
|
||||
|
||||
// Real prefix/round-trip invariants are asserted in
|
||||
// connected-account-token-encryption.service.spec.ts.
|
||||
@@ -45,25 +46,24 @@ describe('ConnectedAccountRefreshTokensService', () => {
|
||||
decrypt: jest.Mock;
|
||||
encryptTokenPair: jest.Mock;
|
||||
} => {
|
||||
const wrap = (value: string) =>
|
||||
`${CONNECTED_ACCOUNT_TOKEN_ENCRYPTION_PREFIX}CIPHER(${value})`;
|
||||
const wrap = (value: string) => `${FAKE_CIPHER_PREFIX}CIPHER(${value})`;
|
||||
|
||||
return {
|
||||
decrypt: jest.fn((value: string) => {
|
||||
const match = value.match(
|
||||
new RegExp(
|
||||
`^${CONNECTED_ACCOUNT_TOKEN_ENCRYPTION_PREFIX}CIPHER\\((.*)\\)$`,
|
||||
),
|
||||
);
|
||||
|
||||
if (match === null) {
|
||||
throw new Error(
|
||||
`fake encryption stub: decrypt called with a non-CIPHER value: ${value}`,
|
||||
decrypt: jest.fn(
|
||||
({ ciphertext }: { ciphertext: string; workspaceId: string }) => {
|
||||
const match = ciphertext.match(
|
||||
new RegExp(`^${FAKE_CIPHER_PREFIX}CIPHER\\((.*)\\)$`),
|
||||
);
|
||||
}
|
||||
|
||||
return match[1];
|
||||
}),
|
||||
if (!isDefined(match)) {
|
||||
throw new Error(
|
||||
`fake encryption stub: decrypt called with a non-CIPHER value: ${ciphertext}`,
|
||||
);
|
||||
}
|
||||
|
||||
return match[1];
|
||||
},
|
||||
),
|
||||
encryptTokenPair: jest.fn(
|
||||
({
|
||||
accessToken,
|
||||
@@ -71,10 +71,12 @@ describe('ConnectedAccountRefreshTokensService', () => {
|
||||
}: {
|
||||
accessToken: string;
|
||||
refreshToken: string | null;
|
||||
workspaceId: string;
|
||||
}) => ({
|
||||
encryptedAccessToken: wrap(accessToken),
|
||||
encryptedRefreshToken:
|
||||
refreshToken === null ? null : wrap(refreshToken),
|
||||
encryptedRefreshToken: isDefined(refreshToken)
|
||||
? wrap(refreshToken)
|
||||
: null,
|
||||
}),
|
||||
),
|
||||
};
|
||||
@@ -167,10 +169,16 @@ describe('ConnectedAccountRefreshTokensService', () => {
|
||||
});
|
||||
expect(
|
||||
connectedAccountTokenEncryptionService.decrypt,
|
||||
).toHaveBeenCalledWith(mockEncryptedAccessToken);
|
||||
).toHaveBeenCalledWith({
|
||||
ciphertext: mockEncryptedAccessToken,
|
||||
workspaceId: mockWorkspaceId,
|
||||
});
|
||||
expect(
|
||||
connectedAccountTokenEncryptionService.decrypt,
|
||||
).toHaveBeenCalledWith(mockEncryptedRefreshToken);
|
||||
).toHaveBeenCalledWith({
|
||||
ciphertext: mockEncryptedRefreshToken,
|
||||
workspaceId: mockWorkspaceId,
|
||||
});
|
||||
expect(
|
||||
microsoftAPIRefreshAccessTokenService.refreshTokens,
|
||||
).not.toHaveBeenCalled();
|
||||
@@ -207,8 +215,8 @@ describe('ConnectedAccountRefreshTokensService', () => {
|
||||
expect(connectedAccountRepository.update).toHaveBeenCalledWith(
|
||||
{ id: mockConnectedAccountId, workspaceId: mockWorkspaceId },
|
||||
expect.objectContaining({
|
||||
accessToken: `${CONNECTED_ACCOUNT_TOKEN_ENCRYPTION_PREFIX}CIPHER(${mockNewAccessTokenPlaintext})`,
|
||||
refreshToken: `${CONNECTED_ACCOUNT_TOKEN_ENCRYPTION_PREFIX}CIPHER(${mockRefreshTokenPlaintext})`,
|
||||
accessToken: `${FAKE_CIPHER_PREFIX}CIPHER(${mockNewAccessTokenPlaintext})`,
|
||||
refreshToken: `${FAKE_CIPHER_PREFIX}CIPHER(${mockRefreshTokenPlaintext})`,
|
||||
lastCredentialsRefreshedAt: expect.any(Date),
|
||||
}),
|
||||
);
|
||||
@@ -244,8 +252,8 @@ describe('ConnectedAccountRefreshTokensService', () => {
|
||||
expect(connectedAccountRepository.update).toHaveBeenCalledWith(
|
||||
{ id: mockConnectedAccountId, workspaceId: mockWorkspaceId },
|
||||
expect.objectContaining({
|
||||
accessToken: `${CONNECTED_ACCOUNT_TOKEN_ENCRYPTION_PREFIX}CIPHER(${mockNewAccessTokenPlaintext})`,
|
||||
refreshToken: `${CONNECTED_ACCOUNT_TOKEN_ENCRYPTION_PREFIX}CIPHER(${mockRefreshTokenPlaintext})`,
|
||||
accessToken: `${FAKE_CIPHER_PREFIX}CIPHER(${mockNewAccessTokenPlaintext})`,
|
||||
refreshToken: `${FAKE_CIPHER_PREFIX}CIPHER(${mockRefreshTokenPlaintext})`,
|
||||
lastCredentialsRefreshedAt: expect.any(Date),
|
||||
}),
|
||||
);
|
||||
@@ -281,8 +289,8 @@ describe('ConnectedAccountRefreshTokensService', () => {
|
||||
expect(connectedAccountRepository.update).toHaveBeenCalledWith(
|
||||
{ id: mockConnectedAccountId, workspaceId: mockWorkspaceId },
|
||||
expect.objectContaining({
|
||||
accessToken: `${CONNECTED_ACCOUNT_TOKEN_ENCRYPTION_PREFIX}CIPHER(${mockNewAccessTokenPlaintext})`,
|
||||
refreshToken: `${CONNECTED_ACCOUNT_TOKEN_ENCRYPTION_PREFIX}CIPHER(${mockRefreshTokenPlaintext})`,
|
||||
accessToken: `${FAKE_CIPHER_PREFIX}CIPHER(${mockNewAccessTokenPlaintext})`,
|
||||
refreshToken: `${FAKE_CIPHER_PREFIX}CIPHER(${mockRefreshTokenPlaintext})`,
|
||||
lastCredentialsRefreshedAt: expect.any(Date),
|
||||
}),
|
||||
);
|
||||
|
||||
+13
-10
@@ -71,13 +71,14 @@ export class ConnectedAccountRefreshTokensService {
|
||||
}
|
||||
|
||||
return {
|
||||
accessToken:
|
||||
this.connectedAccountTokenEncryptionService.decrypt(
|
||||
encryptedAccessToken,
|
||||
),
|
||||
refreshToken: this.connectedAccountTokenEncryptionService.decrypt(
|
||||
encryptedRefreshToken,
|
||||
),
|
||||
accessToken: this.connectedAccountTokenEncryptionService.decrypt({
|
||||
ciphertext: encryptedAccessToken,
|
||||
workspaceId,
|
||||
}),
|
||||
refreshToken: this.connectedAccountTokenEncryptionService.decrypt({
|
||||
ciphertext: encryptedRefreshToken,
|
||||
workspaceId,
|
||||
}),
|
||||
};
|
||||
}
|
||||
|
||||
@@ -86,9 +87,10 @@ export class ConnectedAccountRefreshTokensService {
|
||||
);
|
||||
|
||||
const decryptedRefreshTokenForRefreshCall =
|
||||
this.connectedAccountTokenEncryptionService.decrypt(
|
||||
encryptedRefreshToken,
|
||||
);
|
||||
this.connectedAccountTokenEncryptionService.decrypt({
|
||||
ciphertext: encryptedRefreshToken,
|
||||
workspaceId,
|
||||
});
|
||||
|
||||
const connectedAccountTokens = await this.refreshTokens(
|
||||
connectedAccount,
|
||||
@@ -102,6 +104,7 @@ export class ConnectedAccountRefreshTokensService {
|
||||
} = this.connectedAccountTokenEncryptionService.encryptTokenPair({
|
||||
accessToken: connectedAccountTokens.accessToken,
|
||||
refreshToken: connectedAccountTokens.refreshToken,
|
||||
workspaceId,
|
||||
});
|
||||
|
||||
const authContext = buildSystemAuthContext(workspaceId);
|
||||
|
||||
+6
-1
@@ -28,7 +28,12 @@ export class GmailGetAllFoldersService implements MessageFolderDriver {
|
||||
async getAllMessageFolders(
|
||||
connectedAccount: Pick<
|
||||
ConnectedAccountEntity,
|
||||
'provider' | 'refreshToken' | 'accessToken' | 'id' | 'handle'
|
||||
| 'provider'
|
||||
| 'refreshToken'
|
||||
| 'accessToken'
|
||||
| 'id'
|
||||
| 'handle'
|
||||
| 'workspaceId'
|
||||
>,
|
||||
messageChannel: Pick<MessageChannelEntity, 'messageFolderImportPolicy'>,
|
||||
): Promise<DiscoveredMessageFolder[]> {
|
||||
|
||||
+6
-1
@@ -38,7 +38,12 @@ export class MicrosoftGetAllFoldersService implements MessageFolderDriver {
|
||||
async getAllMessageFolders(
|
||||
connectedAccount: Pick<
|
||||
ConnectedAccountEntity,
|
||||
'accessToken' | 'refreshToken' | 'id' | 'handle' | 'provider'
|
||||
| 'accessToken'
|
||||
| 'refreshToken'
|
||||
| 'id'
|
||||
| 'handle'
|
||||
| 'provider'
|
||||
| 'workspaceId'
|
||||
>,
|
||||
messageChannel: Pick<MessageChannelEntity, 'messageFolderImportPolicy'>,
|
||||
): Promise<DiscoveredMessageFolder[]> {
|
||||
|
||||
+1
@@ -29,6 +29,7 @@ export type MessageFolderDriver = {
|
||||
| 'id'
|
||||
| 'handle'
|
||||
| 'connectionParameters'
|
||||
| 'workspaceId'
|
||||
>,
|
||||
messageChannel: Pick<MessageChannelEntity, 'messageFolderImportPolicy'>,
|
||||
): Promise<DiscoveredMessageFolder[]>;
|
||||
|
||||
+1
@@ -49,6 +49,7 @@ const createMockMessageChannel = (
|
||||
accessToken: 'mock-access-token',
|
||||
refreshToken: 'mock-refresh-token',
|
||||
connectionParameters: {},
|
||||
workspaceId: 'workspace-123',
|
||||
},
|
||||
messageFolders: overrides.messageFolders ?? [],
|
||||
visibility: MessageChannelVisibility.SHARE_EVERYTHING,
|
||||
|
||||
+2
@@ -53,6 +53,7 @@ export class SyncMessageFoldersService {
|
||||
| 'id'
|
||||
| 'handle'
|
||||
| 'connectionParameters'
|
||||
| 'workspaceId'
|
||||
>;
|
||||
messageFolders: MessageFolder[];
|
||||
};
|
||||
@@ -83,6 +84,7 @@ export class SyncMessageFoldersService {
|
||||
| 'handle'
|
||||
| 'provider'
|
||||
| 'connectionParameters'
|
||||
| 'workspaceId'
|
||||
>,
|
||||
messageChannel: Pick<MessageChannelEntity, 'messageFolderImportPolicy'>,
|
||||
): Promise<DiscoveredMessageFolder[]> {
|
||||
|
||||
+2
@@ -40,6 +40,7 @@ describe('GmailGetMessageListService', () => {
|
||||
| 'id'
|
||||
| 'handle'
|
||||
| 'connectionParameters'
|
||||
| 'workspaceId'
|
||||
> = {
|
||||
id: 'connected-account-id',
|
||||
provider: ConnectedAccountProvider.GOOGLE,
|
||||
@@ -47,6 +48,7 @@ describe('GmailGetMessageListService', () => {
|
||||
refreshToken: 'refresh-token',
|
||||
handle: 'test@gmail.com',
|
||||
connectionParameters: {},
|
||||
workspaceId: 'workspace-id',
|
||||
};
|
||||
|
||||
beforeEach(async () => {
|
||||
|
||||
+6
-1
@@ -31,7 +31,12 @@ export class GmailGetMessageListService {
|
||||
private async getMessageListWithoutCursor(
|
||||
connectedAccount: Pick<
|
||||
ConnectedAccountEntity,
|
||||
'provider' | 'accessToken' | 'refreshToken' | 'id' | 'handle'
|
||||
| 'provider'
|
||||
| 'accessToken'
|
||||
| 'refreshToken'
|
||||
| 'id'
|
||||
| 'handle'
|
||||
| 'workspaceId'
|
||||
>,
|
||||
messageFolders: Pick<
|
||||
MessageFolderEntity,
|
||||
|
||||
+1
@@ -33,6 +33,7 @@ export class GmailGetMessagesService {
|
||||
| 'id'
|
||||
| 'handle'
|
||||
| 'handleAliases'
|
||||
| 'workspaceId'
|
||||
>,
|
||||
messageChannel: Pick<
|
||||
MessageChannelEntity,
|
||||
|
||||
+2
@@ -38,6 +38,7 @@ describe('ImapGetMessageListService', () => {
|
||||
| 'id'
|
||||
| 'handle'
|
||||
| 'connectionParameters'
|
||||
| 'workspaceId'
|
||||
> = {
|
||||
id: 'connected-account-id',
|
||||
provider: ConnectedAccountProvider.IMAP_SMTP_CALDAV,
|
||||
@@ -45,6 +46,7 @@ describe('ImapGetMessageListService', () => {
|
||||
refreshToken: 'refresh-token',
|
||||
handle: 'test@example.com',
|
||||
connectionParameters: {},
|
||||
workspaceId: 'workspace-id',
|
||||
};
|
||||
|
||||
const mockImapClient = {
|
||||
|
||||
+1
-1
@@ -14,7 +14,7 @@ export class MicrosoftFetchByBatchService {
|
||||
messageIds: string[],
|
||||
connectedAccount: Pick<
|
||||
ConnectedAccountEntity,
|
||||
'accessToken' | 'refreshToken' | 'id' | 'provider'
|
||||
'accessToken' | 'refreshToken' | 'id' | 'provider' | 'workspaceId'
|
||||
>,
|
||||
): Promise<{
|
||||
messageIdsByBatch: string[][];
|
||||
|
||||
+3
@@ -32,6 +32,7 @@ const mockConnectedAccount: Pick<
|
||||
| 'id'
|
||||
| 'handle'
|
||||
| 'connectionParameters'
|
||||
| 'workspaceId'
|
||||
> = {
|
||||
id: 'connected-account-id',
|
||||
provider: ConnectedAccountProvider.MICROSOFT,
|
||||
@@ -39,6 +40,7 @@ const mockConnectedAccount: Pick<
|
||||
refreshToken: refreshToken,
|
||||
handle: 'test@gmail.com',
|
||||
connectionParameters: {},
|
||||
workspaceId: 'workspace-id',
|
||||
};
|
||||
|
||||
const mockMessageChannel: Pick<
|
||||
@@ -103,6 +105,7 @@ xdescribe('Microsoft dev tests : get message list service', () => {
|
||||
refreshToken: 'invalid-token',
|
||||
handle: 'test@microsoft.com',
|
||||
connectionParameters: {},
|
||||
workspaceId: 'workspace-id',
|
||||
};
|
||||
|
||||
await expect(
|
||||
|
||||
+2
@@ -37,6 +37,7 @@ describe('MicrosoftGetMessageListService', () => {
|
||||
| 'id'
|
||||
| 'handle'
|
||||
| 'connectionParameters'
|
||||
| 'workspaceId'
|
||||
> = {
|
||||
id: 'connected-account-id',
|
||||
provider: ConnectedAccountProvider.MICROSOFT,
|
||||
@@ -44,6 +45,7 @@ describe('MicrosoftGetMessageListService', () => {
|
||||
refreshToken: 'refresh-token',
|
||||
handle: 'test@outlook.com',
|
||||
connectionParameters: {},
|
||||
workspaceId: 'workspace-id',
|
||||
};
|
||||
|
||||
const createMockMicrosoftClient = () => ({
|
||||
|
||||
+1
-1
@@ -73,7 +73,7 @@ export class MicrosoftGetMessageListService {
|
||||
public async getMessageList(
|
||||
connectedAccount: Pick<
|
||||
ConnectedAccountEntity,
|
||||
'provider' | 'accessToken' | 'id'
|
||||
'provider' | 'accessToken' | 'id' | 'workspaceId'
|
||||
>,
|
||||
messageFolder: Pick<
|
||||
MessageFolderEntity,
|
||||
|
||||
+1
@@ -52,6 +52,7 @@ xdescribe('Microsoft dev tests : get messages service', () => {
|
||||
handleAliases: [] as string[],
|
||||
accessToken: accessToken,
|
||||
refreshToken: refreshToken,
|
||||
workspaceId: 'workspace-id',
|
||||
};
|
||||
|
||||
it('should fetch and format messages successfully', async () => {
|
||||
|
||||
+2
@@ -72,6 +72,7 @@ describe('Microsoft get messages service', () => {
|
||||
refreshToken: 'refresh-token',
|
||||
handle: 'John.l@outlook.fr',
|
||||
handleAliases: [] as string[],
|
||||
workspaceId: 'workspace-id',
|
||||
};
|
||||
const messages = service.formatBatchResponsesAsMessages(
|
||||
batchResponses,
|
||||
@@ -173,6 +174,7 @@ describe('Microsoft get messages service', () => {
|
||||
refreshToken: 'refresh-token',
|
||||
handle: 'John.l@outlook.fr',
|
||||
handleAliases: [] as string[],
|
||||
workspaceId: 'workspace-id',
|
||||
};
|
||||
const messages = service.formatBatchResponsesAsMessages(
|
||||
batchResponses,
|
||||
|
||||
+1
@@ -24,6 +24,7 @@ type ConnectedAccountType = Pick<
|
||||
| 'provider'
|
||||
| 'handle'
|
||||
| 'handleAliases'
|
||||
| 'workspaceId'
|
||||
>;
|
||||
|
||||
@Injectable()
|
||||
|
||||
+1
@@ -35,6 +35,7 @@ export class MessagingGetMessagesService {
|
||||
| 'handleAliases'
|
||||
| 'userWorkspaceId'
|
||||
| 'connectionParameters'
|
||||
| 'workspaceId'
|
||||
>,
|
||||
messageChannel: Pick<
|
||||
MessageChannelEntity,
|
||||
|
||||
+1
@@ -16,6 +16,7 @@ export type GetMessageListsArgs = {
|
||||
| 'id'
|
||||
| 'handle'
|
||||
| 'connectionParameters'
|
||||
| 'workspaceId'
|
||||
>;
|
||||
messageFolders: MessageFolder[];
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user