{"record":{"id":"e95baad57277a627","repo":"toeverything/AFFiNE","slug":"action-forbidden-e95baa","errorCode":"action_forbidden","errorMessage":"This feature is temporarily unavailable for you.","messagePattern":"This feature is temporarily unavailable for you\\.","errorType":"http","errorClass":"ActionForbidden","httpStatus":403,"severity":"warning","filePath":"packages/backend/server/src/core/workspaces/resolvers/member.ts","lineNumber":124,"sourceCode":"    private readonly quota: QuotaService,\n    private readonly config: Config,\n    private readonly inviteQuota: InviteQuotaAssertService,\n    private readonly runtime: BackendRuntimeProvider\n  ) {}\n\n  private async assertCanInviteOrShare(\n    userId: string,\n    context: {\n      workspaceId: string;\n      action: 'createInviteLink';\n    }\n  ) {\n    if (await this.runtime.isInviteAbuseUserQuarantinedOrBanned(userId)) {\n      this.logger.warn('Share action blocked for quarantined actor', {\n        userId,\n        ...context,\n      });\n      throw new ActionForbidden(\n        'This feature is temporarily unavailable for you.'\n      );\n    }\n    if (\n      await this.runtime.isInviteAbuseWorkspaceQuarantined(context.workspaceId)\n    ) {\n      this.logger.warn('Share action blocked for quarantined workspace', {\n        userId,\n        ...context,\n      });\n      throw new ActionForbidden(\n        'This feature is temporarily unavailable for you.'\n      );\n    }\n    // Member invites are owned by native quota; this guard stays for invite links until share/link actions migrate.\n    const user = await this.models.user.get(userId);\n    const newAccountAgeMs = this.config.auth.newAccountShareActionDelay * 1000;\n    if (!user || !canUserExecuteLimitedActions(user, newAccountAgeMs)) {","sourceCodeStart":106,"sourceCodeEnd":142,"githubUrl":"https://github.com/toeverything/AFFiNE/blob/26c515e050211269e911f7d9cfe162a26c83ed98/packages/backend/server/src/core/workspaces/resolvers/member.ts#L106-L142","documentation":"Thrown by assertCanInviteOrShare when the acting user is flagged by the invite-abuse system as quarantined or banned (runtime.isInviteAbuseUserQuarantinedOrBanned returns true). The native runtime tracks abuse signals (burst invites, suspicious domains); once a user is in quarantine, all share/invite-link actions are blocked until the disposition expires or is cleared. Reported as action_forbidden (HTTP 403).","triggerScenarios":"Any action routed through assertCanInviteOrShare (e.g. createInviteLink) by a user whose id is in the invite-abuse quarantine or ban set.","commonSituations":"A user who sent a large burst of invites tripped the abuse heuristic and was auto-quarantined; test/seeding accounts sharing an IP or fingerprint got flagged; a shared VPN/proxy IP caused false positives.","solutions":["Wait for the quarantine window to expire, then retry.","Review and clear the abuse disposition through the admin/abuse tooling if it was a false positive.","Reduce invite cadence to stay under the abuse thresholds.","Confirm the flagged userId with the abuse dashboard to rule out account compromise."],"exampleFix":null,"handlingStrategy":"try-catch","validationCode":null,"typeGuard":null,"tryCatchPattern":"try {\n  await createInviteLink(workspaceId);\n} catch (e) {\n  if (isGraphQLError(e, 'action_forbidden') && /temporarily unavailable/i.test(e.message)) {\n    // user quarantined/banned: surface a 'try again later' message; do not auto-retry\n    showUserBlockedNotice(e);\n  } else throw e;\n}","preventionTips":["Do not auto-retry abuse blocks; they are intentional and time-based.","Surface the message to the user and link to support/appeal if available.","Throttle invite/share actions client-side to avoid tripping abuse heuristics."],"tags":["graphql","abuse","permissions","rate-limit","workspace"],"backgroundTag":null,"analyzedSha":"26c515e050211269e911f7d9cfe162a26c83ed98","analyzedAt":"2026-08-12T13:15:16.447Z","schemaVersion":2},"datasetVersion":"2026-08-12T23:17:12.415Z"}