nexu-io/open-design · error · TeamResourceShareForbiddenError

workspace_resource_share_denied

Error message

workspace_resource_share_denied

What it means

Thrown as TeamResourceShareForbiddenError (message 'workspace_resource_share_denied') by share() when scope.canShare is false. The TeamResourceRequestScope.canShare bit is derived from the authoritative membership directory — true only when the member has canManageSharedResources or canShareProjects permission. The gate is enforced server-side from request-verified authority, never from caller-controlled headers, so a client cannot elevate itself.

Source

Thrown at apps/daemon/src/collab/team-resource-share.ts:209

  const adapter: ResourcePublishAdapter = createVelaCliResourceAdapter({
    resolveProjectDir: options.resolveDir,
    resourceIdFor,
    kind: options.kind,
    // Every operation below already requires a request-verified Team scope.
    // Re-reading the daemon's ambient Workspace here would reintroduce the
    // cross-tab switch race this service boundary exists to prevent.
    hasTeamIdentity: () => true,
    ...(options.describeResource ? { describeProject: options.describeResource } : {}),
    ...(options.run ? { run: options.run } : {}),
  });

  return {
    async share(resourceId, scope) {
      // Permission gate: only a member who can manage shared resources may
      // promote one to the team. The route supplied this bit from the
      // authoritative directory, never from caller-controlled headers.
      if (!scope.canShare) throw new TeamResourceShareForbiddenError();
      const { principal } = scope;
      const result = await adapter.publish({
        projectId: resourceId,
        principal,
        reason: 'share',
      });
      if (result) sharedFor(principal.teamId).add(resourceId);
      return result;
    },
    async unshare(resourceId, scope) {
      const { principal } = scope;
      const sharedResource = (await this.sharedResources(scope)).find((resource) => resource.id === resourceId);
      if (sharedResource && !sharedResource.canUnshare) {
        throw new TeamResourceShareForbiddenError();
      }
      await adapter.unpublish?.({ projectId: resourceId, principal });
      sharedFor(principal.teamId).delete(resourceId);
      return true;

View on GitHub (pinned to 5be4028344)

Solutions

  1. Use an account/role that has canManageSharedResources or canShareProjects on the target team.
  2. Have a team owner/admin grant the share permission, or share on the member's behalf from an authorized account.
  3. Refresh the workspace directory so the canShare bit reflects the current role before retrying.
Defensive patterns

Strategy: try-catch

Validate before calling

// Resolve scope first; if the member cannot share, do not call share().
const scope = teamResourceRequestScopeFromContext(context);
if (!scope?.canShare) {
  throw new Error('current member lacks share permission');
}
await service.share(resourceId, scope);

Type guard

import { TeamResourceShareForbiddenError } from './team-resource-share.js';
// (err instanceof TeamResourceShareForbiddenError) is the guard

Try / catch

try {
  await service.share(resourceId, scope);
} catch (err) {
  if (err instanceof TeamResourceShareForbiddenError) {
    // return 403 'you do not have permission to share to this team'
  }
  throw err;
}

Prevention

When it happens

Trigger: A member with a viewer/read-only role (no canManageSharedResources, no canShareProjects) calling the share endpoint, or a member whose directory entry lacks the share permission bits attempting to promote a resource to the team.

Common situations: A non-owner teammate trying to share a design system/plugin/skill they made, a role downgrade that removed share rights, or stale UI state after a permissions change.

Related errors


AI-assisted analysis of nexu-io/open-design@5be4028344 (2026-08-12). Data as JSON: /api/errors/eda3f61a8d86c694. Report an issue: GitHub.