feat(server): entitlement primitive (#14964)

#### PR Dependency Tree


* **PR #14964** 👈

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**
* Added entitlement resolution to validate licenses and derive plan,
quotas, expiry and flags.
* Introduced persistent quota/entitlement state for users and workspaces
with legacy sync behavior.
* Real-time quota-state operations and change events for monitoring
usage.

* **Chores**
  * Updated workspace dependencies to add cryptography/hash crates.

* **Tests**
* Added native entitlement tests covering validation, quantity handling,
and signature/expiry cases.

<!-- 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/14964)

<!-- review_stack_entry_end -->
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
This commit is contained in:
DarkSky
2026-05-14 18:25:03 +08:00
committed by GitHub
parent 542da0b347
commit c8cdc488db
12 changed files with 1371 additions and 50 deletions
@@ -141,7 +141,7 @@ CREATE INDEX "workspace_invitations_workspace_id_status_idx" ON "workspace_invit
CREATE INDEX "workspace_invitations_invitee_user_id_status_idx" ON "workspace_invitations"("invitee_user_id", "status");
-- CreateIndex
CREATE INDEX "workspace_invitations_email_status_idx" ON "workspace_invitations"("workspace_id", "normalized_email", "status");
CREATE INDEX "workspace_invitations_workspace_id_normalized_email_status_idx" ON "workspace_invitations"("workspace_id", "normalized_email", "status");
-- CreateIndex
CREATE UNIQUE INDEX "workspace_invitations_workspace_id_invitee_user_id_key" ON "workspace_invitations"("workspace_id", "invitee_user_id");
@@ -150,13 +150,13 @@ CREATE UNIQUE INDEX "workspace_invitations_workspace_id_invitee_user_id_key" ON
CREATE INDEX "workspace_access_policies_visibility_idx" ON "workspace_access_policies"("visibility");
-- CreateIndex
CREATE INDEX "workspace_access_policies_preview_idx" ON "workspace_access_policies"("url_preview_enabled", "sharing_enabled");
CREATE INDEX "workspace_access_policies_url_preview_enabled_sharing_enabl_idx" ON "workspace_access_policies"("url_preview_enabled", "sharing_enabled");
-- CreateIndex
CREATE INDEX "doc_access_policies_public_idx" ON "doc_access_policies"("workspace_id", "visibility", "published_at") WHERE "visibility" = 'public';
-- CreateIndex
CREATE INDEX "doc_access_policies_doc_idx" ON "doc_access_policies"("workspace_id", "doc_id");
CREATE INDEX "doc_access_policies_workspace_id_doc_id_idx" ON "doc_access_policies"("workspace_id", "doc_id");
-- CreateIndex
CREATE UNIQUE INDEX "doc_grants_owner_key" ON "doc_grants"("workspace_id", "doc_id") WHERE "principal_type" = 'user' AND "role" = 'owner';
@@ -165,10 +165,10 @@ CREATE UNIQUE INDEX "doc_grants_owner_key" ON "doc_grants"("workspace_id", "doc_
CREATE UNIQUE INDEX "doc_grants_legacy_key" ON "doc_grants"("legacy_workspace_id", "legacy_doc_id", "legacy_user_id") WHERE "legacy_workspace_id" IS NOT NULL AND "legacy_doc_id" IS NOT NULL AND "legacy_user_id" IS NOT NULL;
-- CreateIndex
CREATE INDEX "doc_grants_principal_idx" ON "doc_grants"("principal_type", "principal_id", "role");
CREATE INDEX "doc_grants_principal_type_principal_id_role_idx" ON "doc_grants"("principal_type", "principal_id", "role");
-- CreateIndex
CREATE INDEX "doc_grants_doc_role_idx" ON "doc_grants"("workspace_id", "doc_id", "role");
CREATE INDEX "doc_grants_workspace_id_doc_id_role_idx" ON "doc_grants"("workspace_id", "doc_id", "role");
-- AddForeignKey
ALTER TABLE "workspace_runtime_states" ADD CONSTRAINT "workspace_runtime_states_workspace_id_fkey" FOREIGN KEY ("workspace_id") REFERENCES "workspaces"("id") ON DELETE CASCADE ON UPDATE CASCADE;
@@ -0,0 +1,184 @@
-- CreateTable
CREATE TABLE "entitlements" (
"id" VARCHAR NOT NULL,
"target_type" TEXT NOT NULL,
"target_id" VARCHAR,
"source" TEXT NOT NULL,
"plan" TEXT NOT NULL,
"status" TEXT NOT NULL,
"subject_id" VARCHAR,
"issuer" TEXT,
"quantity" INTEGER,
"signed_payload" BYTEA,
"token_hash" TEXT,
"metadata" JSONB NOT NULL DEFAULT '{}',
"issued_at" TIMESTAMPTZ(3),
"starts_at" TIMESTAMPTZ(3),
"expires_at" TIMESTAMPTZ(3),
"validated_at" TIMESTAMPTZ(3),
"grace_until" TIMESTAMPTZ(3),
"created_at" TIMESTAMPTZ(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updated_at" TIMESTAMPTZ(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
CONSTRAINT "entitlements_pkey" PRIMARY KEY ("id"),
CONSTRAINT "entitlements_target_type_check" CHECK ("target_type" IN ('user', 'workspace', 'instance')),
CONSTRAINT "entitlements_source_check" CHECK ("source" IN ('builtin', 'cloud_subscription', 'selfhost_license', 'admin_grant')),
CONSTRAINT "entitlements_status_check" CHECK ("status" IN ('active', 'grace', 'expired', 'revoked', 'needs_reupload')),
CONSTRAINT "entitlements_quantity_check" CHECK ("quantity" IS NULL OR ("quantity" > 0 AND "quantity" <= 100000))
);
-- CreateTable
CREATE TABLE "effective_user_quota_states" (
"user_id" VARCHAR NOT NULL,
"plan" TEXT NOT NULL,
"source_entitlement_id" VARCHAR,
"blob_limit" BIGINT NOT NULL,
"storage_quota" BIGINT NOT NULL,
"used_storage_quota" BIGINT NOT NULL DEFAULT 0,
"history_period_seconds" INTEGER NOT NULL,
"copilot_action_limit" INTEGER,
"flags" JSONB NOT NULL DEFAULT '{}',
"known" BOOLEAN NOT NULL DEFAULT false,
"stale" BOOLEAN NOT NULL DEFAULT false,
"last_reconciled_at" TIMESTAMPTZ(3),
"stale_after" TIMESTAMPTZ(3),
"created_at" TIMESTAMPTZ(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updated_at" TIMESTAMPTZ(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
CONSTRAINT "effective_user_quota_states_pkey" PRIMARY KEY ("user_id"),
CONSTRAINT "effective_user_quota_states_blob_limit_check" CHECK ("blob_limit" >= 0),
CONSTRAINT "effective_user_quota_states_storage_quota_check" CHECK ("storage_quota" >= 0),
CONSTRAINT "effective_user_quota_states_used_storage_quota_check" CHECK ("used_storage_quota" >= 0),
CONSTRAINT "effective_user_quota_states_history_period_check" CHECK ("history_period_seconds" >= 0),
CONSTRAINT "effective_user_quota_states_copilot_limit_check" CHECK ("copilot_action_limit" IS NULL OR "copilot_action_limit" >= 0)
);
-- CreateTable
CREATE TABLE "effective_workspace_quota_states" (
"workspace_id" VARCHAR NOT NULL,
"plan" TEXT NOT NULL,
"source_entitlement_id" VARCHAR,
"owner_user_id" VARCHAR,
"uses_owner_quota" BOOLEAN NOT NULL DEFAULT false,
"seat_limit" INTEGER NOT NULL,
"member_count" INTEGER NOT NULL DEFAULT 0,
"overcapacity_member_count" INTEGER NOT NULL DEFAULT 0,
"blob_limit" BIGINT NOT NULL,
"storage_quota" BIGINT NOT NULL,
"used_storage_quota" BIGINT NOT NULL DEFAULT 0,
"history_period_seconds" INTEGER NOT NULL,
"readonly" BOOLEAN NOT NULL DEFAULT false,
"readonly_reasons" TEXT[] NOT NULL DEFAULT ARRAY[]::TEXT[],
"flags" JSONB NOT NULL DEFAULT '{}',
"known" BOOLEAN NOT NULL DEFAULT false,
"stale" BOOLEAN NOT NULL DEFAULT false,
"last_reconciled_at" TIMESTAMPTZ(3),
"stale_after" TIMESTAMPTZ(3),
"created_at" TIMESTAMPTZ(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updated_at" TIMESTAMPTZ(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
CONSTRAINT "effective_workspace_quota_states_pkey" PRIMARY KEY ("workspace_id"),
CONSTRAINT "effective_workspace_quota_states_seat_limit_check" CHECK ("seat_limit" >= 0),
CONSTRAINT "effective_workspace_quota_states_member_count_check" CHECK ("member_count" >= 0),
CONSTRAINT "effective_workspace_quota_states_overcapacity_check" CHECK ("overcapacity_member_count" >= 0),
CONSTRAINT "effective_workspace_quota_states_blob_limit_check" CHECK ("blob_limit" >= 0),
CONSTRAINT "effective_workspace_quota_states_storage_quota_check" CHECK ("storage_quota" >= 0),
CONSTRAINT "effective_workspace_quota_states_used_storage_quota_check" CHECK ("used_storage_quota" >= 0),
CONSTRAINT "effective_workspace_quota_states_history_period_check" CHECK ("history_period_seconds" >= 0),
CONSTRAINT "effective_workspace_quota_states_readonly_reasons_check" CHECK ("readonly_reasons" <@ ARRAY['member_overflow', 'storage_overflow']::TEXT[])
);
-- CreateIndex
CREATE INDEX "entitlements_target_type_target_id_status_idx" ON "entitlements"("target_type", "target_id", "status");
-- CreateIndex
CREATE INDEX "entitlements_status_expires_at_idx" ON "entitlements"("status", "expires_at");
-- CreateIndex
CREATE UNIQUE INDEX "entitlements_active_subject_key" ON "entitlements"("source", "subject_id")
WHERE "subject_id" IS NOT NULL AND "status" IN ('active', 'grace');
-- CreateIndex
CREATE INDEX "effective_user_quota_states_known_stale_idx" ON "effective_user_quota_states"("known", "stale");
-- CreateIndex
CREATE INDEX "effective_user_quota_states_stale_after_idx" ON "effective_user_quota_states"("stale_after");
-- CreateIndex
CREATE INDEX "effective_workspace_quota_states_owner_user_id_idx" ON "effective_workspace_quota_states"("owner_user_id");
-- CreateIndex
CREATE INDEX "effective_workspace_quota_states_known_stale_idx" ON "effective_workspace_quota_states"("known", "stale");
-- CreateIndex
CREATE INDEX "effective_workspace_quota_states_readonly_stale_idx" ON "effective_workspace_quota_states"("readonly", "stale");
-- CreateIndex
CREATE INDEX "effective_workspace_quota_states_stale_after_idx" ON "effective_workspace_quota_states"("stale_after");
-- AddForeignKey
ALTER TABLE "effective_user_quota_states" ADD CONSTRAINT "effective_user_quota_states_user_id_fkey" FOREIGN KEY ("user_id") REFERENCES "users"("id") ON DELETE CASCADE ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "effective_user_quota_states" ADD CONSTRAINT "effective_user_quota_states_source_entitlement_id_fkey" FOREIGN KEY ("source_entitlement_id") REFERENCES "entitlements"("id") ON DELETE SET NULL ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "effective_workspace_quota_states" ADD CONSTRAINT "effective_workspace_quota_states_workspace_id_fkey" FOREIGN KEY ("workspace_id") REFERENCES "workspaces"("id") ON DELETE CASCADE ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "effective_workspace_quota_states" ADD CONSTRAINT "effective_workspace_quota_states_owner_user_id_fkey" FOREIGN KEY ("owner_user_id") REFERENCES "users"("id") ON DELETE SET NULL ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "effective_workspace_quota_states" ADD CONSTRAINT "effective_workspace_quota_states_source_entitlement_id_fkey" FOREIGN KEY ("source_entitlement_id") REFERENCES "entitlements"("id") ON DELETE SET NULL ON UPDATE CASCADE;
CREATE OR REPLACE FUNCTION "project_legacy_workspace_readonly_feature"()
RETURNS trigger
LANGUAGE plpgsql
AS $$
BEGIN
IF TG_OP = 'DELETE' THEN
DELETE FROM "workspace_features"
WHERE "workspace_id" = OLD."workspace_id"
AND "name" = 'quota_exceeded_readonly_workspace_v1';
RETURN OLD;
END IF;
IF NEW."readonly" THEN
UPDATE "workspace_features"
SET "reason" = 'legacy quota state projection trigger',
"activated" = true
WHERE "workspace_id" = NEW."workspace_id"
AND "name" = 'quota_exceeded_readonly_workspace_v1';
IF NOT FOUND THEN
INSERT INTO "workspace_features"(
"workspace_id",
"name",
"type",
"configs",
"reason",
"activated"
)
VALUES (
NEW."workspace_id",
'quota_exceeded_readonly_workspace_v1',
0,
'{}',
'legacy quota state projection trigger',
true
);
END IF;
ELSE
DELETE FROM "workspace_features"
WHERE "workspace_id" = NEW."workspace_id"
AND "name" = 'quota_exceeded_readonly_workspace_v1';
END IF;
RETURN NEW;
END;
$$;
CREATE TRIGGER "project_legacy_workspace_readonly_feature_trigger"
AFTER INSERT OR UPDATE OF "readonly" OR DELETE ON "effective_workspace_quota_states"
FOR EACH ROW
EXECUTE FUNCTION "project_legacy_workspace_readonly_feature"();