Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
33 changes: 21 additions & 12 deletions apps/integration-calendar-service/src/calendar-connection-create.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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 ||
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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();
}
}
}
Loading