Skip to content
Open
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
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
### Fixes

- API: fix `GET /api/v1/skills` pagination so `cursor` advances to the next page instead of repeating the first page for supported non-trending sorts (#2275) (thanks @vyctorbrzezowski, @enerj).
- Web: block collaborative membership on personal publishers while allowing the linked owner to clean up stale extra membership rows (thanks @vyctorbrzezowski).

## 0.17.0 - 2026-05-19

Expand Down
38 changes: 36 additions & 2 deletions convex/lib/publishers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -127,7 +127,10 @@ export async function assertCanManageOwnedResource(
}

const publisher = await ctx.db.get(params.ownerPublisherId);
if (publisher?.kind === "user" && publisher.linkedUserId === params.actor._id) return;
if (publisher?.kind === "user") {
if (publisher.linkedUserId === params.actor._id) return;
throw new ConvexError("Forbidden");
}

const membership = await getPublisherMembership(ctx, params.ownerPublisherId, params.actor._id);
if (
Expand Down Expand Up @@ -475,6 +478,26 @@ export async function getPublisherMembership(
}
}

export async function canAccessPublisherOwnerScope(
ctx: DbCtx,
params: {
publisher: Doc<"publishers"> | null | undefined;
userId: Id<"users">;
allowedPublisherRoles?: PublisherRole[];
},
) {
const publisher = params.publisher;
if (!publisher || !isPublisherActive(publisher)) return false;
if (publisher.kind === "user") {
return publisher.linkedUserId === params.userId;
}
const membership = await getPublisherMembership(ctx, publisher._id, params.userId);
return Boolean(
membership &&
isPublisherRoleAllowed(membership.role, params.allowedPublisherRoles ?? ["publisher"]),
);
}

export async function requirePublisherRole(
ctx: DbCtx,
params: {
Expand All @@ -484,7 +507,14 @@ export async function requirePublisherRole(
},
) {
const publisher = await ctx.db.get(params.publisherId);
if (!isPublisherActive(publisher)) throw new ConvexError("Publisher not found");
if (!publisher || !isPublisherActive(publisher)) throw new ConvexError("Publisher not found");
if (publisher.kind === "user") {
if (publisher.linkedUserId !== params.userId) {
throw new ConvexError("Forbidden");
}
const membership = await getPublisherMembership(ctx, params.publisherId, params.userId);
return { publisher, membership };
}
const membership = await getPublisherMembership(ctx, params.publisherId, params.userId);
if (!membership || !isPublisherRoleAllowed(membership.role, params.allowed)) {
throw new ConvexError("Forbidden");
Expand Down Expand Up @@ -514,6 +544,10 @@ export async function resolvePublisherForActor(
if (!publisher || !isPublisherActive(publisher)) {
throw new ConvexError(`Publisher "@${requestedHandle}" not found`);
}
if (publisher.kind === "user") {
if (publisher.linkedUserId === params.actor._id) return publisher;
throw new ConvexError(`You do not have publish access for "@${requestedHandle}"`);
}
const membership = await getPublisherMembership(ctx, publisher._id, params.actor._id);
if (!membership || !isPublisherRoleAllowed(membership.role, params.allowed)) {
throw new ConvexError(`You do not have publish access for "@${requestedHandle}"`);
Expand Down
Loading
Loading