diff --git a/apps/integration-calendar-service/src/calendar-connection-create.test.ts b/apps/integration-calendar-service/src/calendar-connection-create.test.ts index 2d9ad50c..67900e3f 100644 --- a/apps/integration-calendar-service/src/calendar-connection-create.test.ts +++ b/apps/integration-calendar-service/src/calendar-connection-create.test.ts @@ -139,6 +139,54 @@ describe('CalendarConnectionCreateApplication', () => { expect(credentials.deleteSecret).toHaveBeenCalledWith(ACCESS_HANDLE); }); + it('deletes newly stored credentials when durable metadata evidence mismatches authority', async () => { + const credentials = store(); + const connections: CalendarConnectionCreateRepository = { + async createConnection() { + return Object.freeze({ + ...record(), + connectionId: 'aaaaaaaa-aaaa-4aaa-8aaa-aaaaaaaaaaaa', + }); + }, + }; + const application = new CalendarConnectionCreateApplication( + connections, + credentials, + () => CREATED_AT, + ); + + await expect(application.create(authority, providerResult)).rejects.toBeInstanceOf( + CalendarConnectionCreateDependencyError, + ); + expect(credentials.deleteSecret).toHaveBeenCalledTimes(2); + expect(credentials.deleteSecret).toHaveBeenCalledWith(REFRESH_HANDLE); + expect(credentials.deleteSecret).toHaveBeenCalledWith(ACCESS_HANDLE); + }); + + it('rejects mismatched durable provider identity and compensates credentials', async () => { + const credentials = store(); + const connections: CalendarConnectionCreateRepository = { + async createConnection() { + return Object.freeze({ + ...record(), + providerAccountSubject: 'different-provider-subject', + }); + }, + }; + const application = new CalendarConnectionCreateApplication( + connections, + credentials, + () => CREATED_AT, + ); + + await expect(application.create(authority, providerResult)).rejects.toBeInstanceOf( + CalendarConnectionCreateDependencyError, + ); + expect(credentials.deleteSecret).toHaveBeenCalledTimes(2); + expect(credentials.deleteSecret).toHaveBeenCalledWith(REFRESH_HANDLE); + expect(credentials.deleteSecret).toHaveBeenCalledWith(ACCESS_HANDLE); + }); + it('deletes the access credential when refresh credential storage fails', async () => { const credentials: CalendarConnectionCredentialStore = { writeSecret: vi diff --git a/apps/integration-calendar-service/src/calendar-connection-create.ts b/apps/integration-calendar-service/src/calendar-connection-create.ts index e172827c..e34ac826 100644 --- a/apps/integration-calendar-service/src/calendar-connection-create.ts +++ b/apps/integration-calendar-service/src/calendar-connection-create.ts @@ -224,6 +224,7 @@ function projectCreated( readonly workspaceId: string; readonly userId: string; readonly providerCode: CalendarConnectionProvider; + readonly providerAccountSubject: string; readonly scopeValues: readonly string[]; readonly tokenExpiresAt: string; readonly selectedCalendarIdentifier: string; @@ -236,6 +237,7 @@ function projectCreated( record.workspaceId !== expected.workspaceId || record.userId !== expected.userId || record.providerCode !== expected.providerCode || + record.providerAccountSubject !== expected.providerAccountSubject || record.status !== 'active' || record.revokedAt !== null || record.accessSecretHandle !== expected.accessSecretHandle || @@ -271,7 +273,8 @@ export class CalendarConnectionCreateApplication { /** * Stores provider credentials first, persists only opaque handles, and - * compensates newly written credentials if durable metadata creation fails. + * compensates newly written credentials if durable metadata creation or + * returned persistence evidence fails validation. */ async create( authority: TrustedCalendarUserContext, @@ -334,16 +337,22 @@ export class CalendarConnectionCreateApplication { return unavailable(); } - return projectCreated(record, { - connectionId: safe.connectionId, - workspaceId: safe.workspaceId, - userId: safe.userId, - providerCode: safe.providerCode, - scopeValues: safe.scopeValues, - tokenExpiresAt: safe.tokenExpiresAt, - selectedCalendarIdentifier: safe.selectedCalendarIdentifier, - accessSecretHandle, - refreshSecretHandle, - }); + try { + return projectCreated(record, { + connectionId: safe.connectionId, + workspaceId: safe.workspaceId, + userId: safe.userId, + providerCode: safe.providerCode, + providerAccountSubject: safe.providerAccountSubject, + scopeValues: safe.scopeValues, + tokenExpiresAt: safe.tokenExpiresAt, + selectedCalendarIdentifier: safe.selectedCalendarIdentifier, + accessSecretHandle, + refreshSecretHandle, + }); + } catch { + await bestEffortCleanup(this.credentials, writtenHandles); + return unavailable(); + } } }