diff --git a/prisma/schema.prisma b/prisma/schema.prisma index 7dbbcdd21..d37aedff9 100644 --- a/prisma/schema.prisma +++ b/prisma/schema.prisma @@ -6036,7 +6036,10 @@ model AccountGrant { revokedBy AccountGrantRevoker? @map("revoked_by") /// Optional lapse date. Checked live on every resolution, so an expired - /// grant stops conferring access without anything having to sweep it. + /// grant stops conferring access without anything having to sweep it. A + /// managed-profile invitation may carry an expiry while awaiting consent; + /// accepting its MANAGE grant clears this field because an active Guardian + /// ends only through an explicit lifecycle transition. expiresAt DateTime? @map("expires_at") /// Refreshed fire-and-forget when the grant is exercised, the same posture /// as `ApiToken.lastUsedAt`. The owner's panel reads it to answer "is this diff --git a/src/lib/sharing/grants.ts b/src/lib/sharing/grants.ts index 908294b4e..41b3e067f 100644 --- a/src/lib/sharing/grants.ts +++ b/src/lib/sharing/grants.ts @@ -432,6 +432,28 @@ export async function acceptGrant( throw await refusalFor(input.grantId, { granteeId: input.granteeId }, db); } + const accepted = await db.accountGrant.findUniqueOrThrow({ + where: { id: input.grantId }, + include: { grantor: { select: { managedProfileAt: true } } }, + }); + + // The expiry belongs to the invitation, not to a Guardian relationship. A + // managed profile still lets its creator set an expiry while the invitation + // waits for the other adult's consent, but once accepted the MANAGE grant is + // standing until a Guardian explicitly ends it. Ordinary adult-record grants + // retain their requested expiry unchanged. + if (accepted.access === "MANAGE" && accepted.grantor.managedProfileAt) { + await db.accountGrant.updateMany({ + where: { + id: accepted.id, + granteeId: input.granteeId, + acceptedAt: { not: null }, + revokedAt: null, + }, + data: { expiresAt: null }, + }); + } + return db.accountGrant.findUniqueOrThrow({ where: { id: input.grantId } }); }