feat(server): entitlement based model (#14996)

#### PR Dependency Tree


* **PR #14996** 👈

This tree was auto-generated by
[Charcoal](https://github.com/danerwilliams/charcoal)

<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->
## Summary by CodeRabbit

* **New Features**
  * Admin mutations to grant/revoke commercial entitlements.
  * New Doc comment-update permission.
  * Realtime user/workspace quota-state endpoints and live-update rooms.

* **Bug Fixes**
  * More accurate readable-doc filtering and permission evaluation.

* **Refactor**
* Workspace feature management moved to entitlement-based model;
permission and quota pipelines redesigned.
  * Admin workspace UI now edits flags only (feature toggles removed).

* **Tests**
* Extensive new and updated tests for permissions, entitlements, quota,
projection, and backfills.

<!-- review_stack_entry_start -->

[![Review Change
Stack](https://storage.googleapis.com/coderabbit_public_assets/review-stack-in-coderabbit-ui.svg)](https://app.coderabbit.ai/change-stack/toeverything/AFFiNE/pull/14996?utm_source=github_walkthrough&utm_medium=github&utm_campaign=change_stack)

<!-- review_stack_entry_end -->
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
This commit is contained in:
DarkSky
2026-05-19 22:48:05 +08:00
committed by GitHub
parent 103ad2a810
commit c53457691d
150 changed files with 13463 additions and 2458 deletions
@@ -19,6 +19,7 @@ import {
TooManyRequest,
URLHelper,
} from '../../../base';
import { EntitlementService } from '../../../core/entitlement';
import { EarlyAccessType, FeatureService } from '../../../core/features';
import { StripeFactory } from '../stripe';
import {
@@ -64,7 +65,8 @@ export class UserSubscriptionManager extends SubscriptionManager {
private readonly feature: FeatureService,
private readonly event: EventBus,
private readonly url: URLHelper,
private readonly mutex: Mutex
private readonly mutex: Mutex,
private readonly entitlement: EntitlementService
) {
super(stripeProvider, db);
}
@@ -254,7 +256,7 @@ export class UserSubscriptionManager extends SubscriptionManager {
const subscriptionData = this.transformSubscription(subscription);
return this.db.subscription.upsert({
const saved = await this.db.subscription.upsert({
where: {
stripeSubscriptionId: stripeSubscription.id,
},
@@ -270,6 +272,8 @@ export class UserSubscriptionManager extends SubscriptionManager {
...omit(subscriptionData, ['provider', 'iapStore']),
},
});
await this.entitlement.upsertFromCloudSubscription(saved);
return saved;
}
async deleteStripeSubscription({
@@ -285,6 +289,11 @@ export class UserSubscriptionManager extends SubscriptionManager {
});
if (result.count > 0) {
await this.entitlement.revokeCloudSubscription({
targetId: userId,
plan: lookupKey.plan,
stripeSubscriptionId: stripeSubscription.id,
});
this.event.emit('user.subscription.canceled', {
userId,
plan: lookupKey.plan,
@@ -416,7 +425,7 @@ export class UserSubscriptionManager extends SubscriptionManager {
if (prevSubscription) {
if (prevSubscription.stripeSubscriptionId) {
await this.db.subscription.update({
const subscription = await this.db.subscription.update({
where: {
id: prevSubscription.id,
},
@@ -431,6 +440,7 @@ export class UserSubscriptionManager extends SubscriptionManager {
nextBillAt: null,
},
});
await this.entitlement.upsertFromCloudSubscription(subscription);
await this.stripe.subscriptions.cancel(
prevSubscription.stripeSubscriptionId,
@@ -440,7 +450,7 @@ export class UserSubscriptionManager extends SubscriptionManager {
);
}
} else {
await this.db.subscription.create({
const subscription = await this.db.subscription.create({
data: {
targetId: knownInvoice.userId,
stripeSubscriptionId: null,
@@ -452,6 +462,7 @@ export class UserSubscriptionManager extends SubscriptionManager {
nextBillAt: null,
},
});
await this.entitlement.upsertFromCloudSubscription(subscription);
}
this.event.emit('user.subscription.activated', {
@@ -552,6 +563,10 @@ export class UserSubscriptionManager extends SubscriptionManager {
plan: lookupKey.plan,
recurring: lookupKey.recurring,
});
await this.entitlement.upsertFromCloudSubscription({
...subscription,
targetId: userId,
});
return subscription;
}
@@ -582,6 +597,12 @@ export class UserSubscriptionManager extends SubscriptionManager {
canceledAt: new Date(),
},
});
await this.entitlement.revokeCloudSubscription({
targetId: userId,
plan: lookupKey.plan,
subscriptionId: subscription.id,
stripeSubscriptionId: subscription.stripeSubscriptionId,
});
this.event.emit('user.subscription.canceled', {
userId,
@@ -621,7 +642,7 @@ export class UserSubscriptionManager extends SubscriptionManager {
}
if (subscription) {
await this.db.subscription.update({
const saved = await this.db.subscription.update({
where: { id: subscription.id },
data: {
status: SubscriptionStatus.Active,
@@ -631,8 +652,9 @@ export class UserSubscriptionManager extends SubscriptionManager {
end,
},
});
await this.entitlement.upsertFromCloudSubscription(saved);
} else {
await this.db.subscription.create({
const saved = await this.db.subscription.create({
data: {
targetId: userId,
stripeSubscriptionId: null,
@@ -643,6 +665,7 @@ export class UserSubscriptionManager extends SubscriptionManager {
nextBillAt: null,
},
});
await this.entitlement.upsertFromCloudSubscription(saved);
}
this.event.emit('user.subscription.activated', {