fix: apply forced retention before creating a share

The share route enforced retention after createSharedLink and the permission
grant. If the cascade threw at that point, the client got a 500 with a live
share already created, and a retry never reached the cascade again because
createSharedLink rejects when an active share exists — leaving a valid share
whose source conversation stays non-temporary and non-expiring under ephemeral
mode.

Convert the source conversation first: a failed share attempt (or an
existing-share retry) still converts the touched chat, and the share expiration
resolution now reads the converted conversation's deadline.
This commit is contained in:
Marco Beretta 2026-07-05 03:11:42 +02:00
parent b3785d7c02
commit cc4533d554
No known key found for this signature in database
GPG key ID: D918033D8E74CC11
2 changed files with 26 additions and 7 deletions

View file

@ -374,7 +374,7 @@ describe('share routes', () => {
);
});
it('does not convert the source conversation when the share is not created', async () => {
it('converts the source conversation before creating the share so retries stay covered', async () => {
mockGetSharedLinkExpiration.mockResolvedValue(activeExpiration);
createSharedLink.mockResolvedValue(null);
@ -383,7 +383,19 @@ describe('share routes', () => {
.send({ targetMessageId: 'msg-123' });
expect(response.status).toBe(404);
expect(applyForcedRetention).not.toHaveBeenCalled();
/**
* Retention runs before createSharedLink: a share attempt that fails (or hits an existing
* active share on retry) must still convert the touched conversation, otherwise a live
* share could outlast a source chat that never converts.
*/
expect(applyForcedRetention).toHaveBeenCalledWith(
{ userId: 'user-123', interfaceConfig: { retentionMode: RetentionMode.EPHEMERAL } },
{ conversationId: 'convo-123' },
expect.objectContaining({ context: expect.any(String) }),
);
expect(applyForcedRetention.mock.invocationCallOrder[0]).toBeLessThan(
createSharedLink.mock.invocationCallOrder[0],
);
});
it('converts the source conversation under forced retention when updating a share', async () => {

View file

@ -467,6 +467,18 @@ router.post(
async (req, res) => {
try {
const { targetMessageId } = req.body;
/**
* Convert the source conversation before creating the link. createSharedLink rejects
* when an active share already exists, so a retention failure after creation would
* leave a live share whose source chat never converts no retry could reach the
* cascade again. Converting first also lets the share expiration below read the
* converted conversation's deadline.
*/
await enforceForcedRetention(
req,
req.params.conversationId,
'POST /api/share/:conversationId',
);
const expiredAt = await resolveSharedLinkExpiration(req, req.params.conversationId);
if (expiredAt != null && !isActiveExpirationDate(expiredAt)) {
return res.status(404).end();
@ -488,11 +500,6 @@ router.post(
);
if (created) {
await grantCreationPermissions(created._id, req.user.id, grantPublic, expiredAt);
await enforceForcedRetention(
req,
req.params.conversationId,
'POST /api/share/:conversationId',
);
res.status(200).json(created);
} else {
res.status(404).end();