Repository: thirdweb-dev/js
Pull request #6641: Add team capabilities to service-utils
Author: login: jnsdls
Labels: name: Dashboard
description: Involves changes to the Dashboard.
name: packages
Created: 2025-04-03T21:24:43Z | Merged: 2025-04-04T04:37:23Z

blocked by: https://github.com/thirdweb-dev/api-server/pull/1500

# Add Team Capabilities

This PR adds a `TeamCapabilities` type to the service-utils package, which defines the structure of team capabilities that need to be kept in sync with the backend. The type includes capabilities for:

- RPC Edge
- Insight
- Storage (download and upload)
- Nebula
- Bundler
- Embedded Wallets

The `TeamResponse` type is also updated to include a `capabilities` field of the new `TeamCapabilities` type.

A changeset is added to track this patch update.

<!-- start pr-codex -->

---

## PR-Codex overview
This PR introduces a comprehensive set of `capabilities` for team services, enhancing functionality related to storage, RPC, and other services. It ensures proper authorization checks based on these capabilities, enabling better service management and control.

### Detailed summary
- Added `capabilities` object to team configuration in `stubs.ts`.
- Defined `TeamCapabilities` type in `api.ts` to match service capabilities schema.
- Updated `TeamResponse` type to include `capabilities`.
- Implemented service enablement checks in `authorize` function.
- Created `isServiceEnabledForTeam` function to validate service access based on capabilities.

> ✨ Ask PR-Codex anything about this PR by commenting with `/codex {your question}`

<!-- end pr-codex -->


Diff:
diff --git a/.changeset/loose-comics-serve.md b/.changeset/loose-comics-serve.md
new file mode 100644
index 00000000000..0c2788601ad
--- /dev/null
+++ b/.changeset/loose-comics-serve.md
@@ -0,0 +1,5 @@
+---
+"@thirdweb-dev/service-utils": patch
+---
+
+add team capabilities
diff --git a/apps/dashboard/src/stories/stubs.ts b/apps/dashboard/src/stories/stubs.ts
index 0eaa2d227c0..c525a76e71a 100644
--- a/apps/dashboard/src/stories/stubs.ts
+++ b/apps/dashboard/src/stories/stubs.ts
@@ -56,6 +56,40 @@ export function teamStub(id: string, billingPlan: Team["billingPlan"]): Team {
       "chainsaw",
     ],
     stripeCustomerId: "cus_1234567890",
+    capabilities: {
+      rpc: {
+        enabled: true,
+        rateLimit: 1000,
+      },
+      storage: {
+        enabled: true,
+        download: {
+          rateLimit: 1000,
+        },
+        upload: {
+          totalFileSizeBytesLimit: 1_000_000_000,
+          rateLimit: 1000,
+        },
+      },
+      bundler: {
+        enabled: true,
+        rateLimit: 1000,
+        mainnetEnabled: true,
+      },
+      insight: {
+        enabled: true,
+        rateLimit: 1000,
+      },
+      embeddedWallets: {
+        enabled: true,
+        customAuth: true,
+        customBranding: true,
+      },
+      nebula: {
+        enabled: true,
+        rateLimit: 1000,
+      },
+    },
   };
 
   return team;
diff --git a/packages/service-utils/src/core/api.ts b/packages/service-utils/src/core/api.ts
index 56daf87f9e3..a8d7c493775 100644
--- a/packages/service-utils/src/core/api.ts
+++ b/packages/service-utils/src/core/api.ts
@@ -43,6 +43,45 @@ export type ApiResponse = {
   };
 };
 
+/**
+ * Stores service-specific capabilities.
+ * This type should match the schema from API server.
+ */
+type TeamCapabilities = {
+  rpc: {
+    enabled: boolean;
+    rateLimit: number;
+  };
+  insight: {
+    enabled: boolean;
+    rateLimit: number;
+  };
+  storage: {
+    enabled: boolean;
+    download: {
+      rateLimit: number;
+    };
+    upload: {
+      totalFileSizeBytesLimit: number;
+      rateLimit: number;
+    };
+  };
+  nebula: {
+    enabled: boolean;
+    rateLimit: number;
+  };
+  bundler: {
+    enabled: boolean;
+    mainnetEnabled: boolean;
+    rateLimit: number;
+  };
+  embeddedWallets: {
+    enabled: boolean;
+    customAuth: boolean;
+    customBranding: boolean;
+  };
+};
+
 export type TeamResponse = {
   id: string;
   name: string;
@@ -68,6 +107,7 @@ export type TeamResponse = {
   canCreatePublicChains: boolean | null;
   enabledScopes: ServiceName[];
   isOnboarded: boolean;
+  capabilities: TeamCapabilities;
 };
 
 export type ProjectSecretKey = {
diff --git a/packages/service-utils/src/core/authorize/index.ts b/packages/service-utils/src/core/authorize/index.ts
index 29fef274aa5..98f509b8491 100644
--- a/packages/service-utils/src/core/authorize/index.ts
+++ b/packages/service-utils/src/core/authorize/index.ts
@@ -1,6 +1,7 @@
 import {
   type CoreServiceConfig,
   type TeamAndProjectResponse,
+  type TeamResponse,
   fetchTeamAndProject,
 } from "../api.js";
 import { authorizeClient } from "./client.js";
@@ -129,6 +130,21 @@ export async function authorize(
       errorCode: "INVALID_KEY",
     };
   }
+  // check if the service is maybe disabled for the team (usually due to a billing issue / exceeding the free plan limit)
+  if (
+    !isServiceEnabledForTeam(
+      serviceConfig.serviceScope,
+      teamAndProjectResponse.team.capabilities,
+    )
+  ) {
+    return {
+      authorized: false,
+      status: 403,
+      errorMessage:
+        "You currently do not have access to this service. Please check if your subscription includes this service and is active.",
+      errorCode: "SERVICE_TEMPORARILY_DISABLED",
+    };
+  }
   // now we can validate the key itself
   const clientAuth = authorizeClient(authData, teamAndProjectResponse);
 
@@ -161,3 +177,26 @@ export async function authorize(
     authMethod: clientAuth.authMethod,
   };
 }
+
+function isServiceEnabledForTeam(
+  scope: CoreServiceConfig["serviceScope"],
+  teamCapabilities: TeamResponse["capabilities"],
+): boolean {
+  switch (scope) {
+    case "rpc":
+      return teamCapabilities.rpc.enabled;
+    case "bundler":
+      return teamCapabilities.bundler.enabled;
+    case "storage":
+      return teamCapabilities.storage.enabled;
+    case "insight":
+      return teamCapabilities.insight.enabled;
+    case "nebula":
+      return teamCapabilities.nebula.enabled;
+    case "embeddedWallets":
+      return teamCapabilities.embeddedWallets.enabled;
+    default:
+      // always return true for any legacy / un-named services
+      return true;
+  }
+}
diff --git a/packages/service-utils/src/mocks.ts b/packages/service-utils/src/mocks.ts
index af27d445927..10236afbf8e 100644
--- a/packages/service-utils/src/mocks.ts
+++ b/packages/service-utils/src/mocks.ts
@@ -59,6 +59,40 @@ export const validTeamResponse: TeamResponse = {
   canCreatePublicChains: false,
   enabledScopes: ["storage", "rpc", "bundler"],
   isOnboarded: true,
+  capabilities: {
+    rpc: {
+      enabled: true,
+      rateLimit: 1000,
+    },
+    insight: {
+      enabled: true,
+      rateLimit: 1000,
+    },
+    storage: {
+      enabled: true,
+      download: {
+        rateLimit: 1000,
+      },
+      upload: {
+        totalFileSizeBytesLimit: 1_000_000_000,
+        rateLimit: 1000,
+      },
+    },
+    nebula: {
+      enabled: true,
+      rateLimit: 1000,
+    },
+    bundler: {
+      enabled: true,
+      mainnetEnabled: true,
+      rateLimit: 1000,
+    },
+    embeddedWallets: {
+      enabled: true,
+      customAuth: true,
+      customBranding: true,
+    },
+  },
 };
 
 export const validTeamAndProjectResponse: TeamAndProjectResponse = {


Reviews:
user: login: graphite-app[bot]
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-03T21:44:40Z
user: login: joaquim-verges
state: APPROVED
author_association: MEMBER
submitted_at: 2025-04-03T22:12:33Z
user: login: arcoraven
state: APPROVED
author_association: CONTRIBUTOR
submitted_at: 2025-04-04T04:25:38Z

Review comments:
diff_hunk: @@ -129,6 +130,21 @@ export async function authorize(
       errorCode: "INVALID_KEY",
     };
   }
+  // check if the service is maybe disabled for the team (usually due to a billing issue / exceeding the free plan limit)
+  if (
+    !isServiceEnabledForTeam(
+      serviceConfig.serviceScope,
+      teamAndProjectResponse.team.capabilities,
+    )
+  ) {
+    return {
+      authorized: false,
+      status: 403,
+      errorMessage:
+        "You currently do not have access to this service. Please check if your subscription includes this service and is active.",
+      errorCode: "SERVICE_TEMPOARILY_DISABLED",
path: packages/service-utils/src/core/authorize/index.ts
user: login: graphite-app[bot]
body: There's a typo in the error code `SERVICE_TEMPOARILY_DISABLED`. It should be corrected to `SERVICE_TEMPORARILY_DISABLED` (the word "temporarily" is misspelled).
```suggestion
      errorCode: "SERVICE_TEMPORARILY_DISABLED",
```
  

*Spotted by [Diamond](https://app.graphite.dev/diamond/?org=thirdweb-dev&ref=ai-review-comment)*<i class='graphite__hidden'><br /><br />Is this helpful? React 👍 or 👎 to let us know.</i>
created_at: 2025-04-03T21:44:38Z
author_association: CONTRIBUTOR

PR comments:
user: login: vercel[bot]
created_at: 2025-04-03T21:24:46Z
body: [vc]: #IRLl5hmNXtJYOGeQZeFt6GJXrvIL65W5QI66suQRr/k=:eyJpc01vbm9yZXBvIjp0cnVlLCJ0eXBlIjoiZ2l0aHViIiwicHJvamVjdHMiOlt7Im5hbWUiOiJ3YWxsZXQtdWkiLCJyb290RGlyZWN0b3J5IjoiYXBwcy93YWxsZXQtdWkiLCJpbnNwZWN0b3JVcmwiOiJodHRwczovL3ZlcmNlbC5jb20vdGhpcmR3ZWIvd2FsbGV0LXVpL0VxWGVYUFVvWm9kQ1FGUTEyYkRRTmZUOG9qZWkiLCJwcmV2aWV3VXJsIjoiIiwibmV4dENvbW1pdFN0YXR1cyI6IlNLSVBQRUQiLCJsaXZlRmVlZGJhY2siOnsicmVzb2x2ZWQiOjAsInVucmVzb2x2ZWQiOjAsInRvdGFsIjowLCJsaW5rIjoiIn19LHsibmFtZSI6InRoaXJkd2ViX3BsYXlncm91bmQiLCJyb290RGlyZWN0b3J5IjoiYXBwcy9wbGF5Z3JvdW5kLXdlYiIsImluc3BlY3RvclVybCI6Imh0dHBzOi8vdmVyY2VsLmNvbS90aGlyZHdlYi90aGlyZHdlYl9wbGF5Z3JvdW5kL2JVNXRYY0Y2Z0pNaUU0TnRoS2FzaFpZR1h4WXgiLCJwcmV2aWV3VXJsIjoiIiwibmV4dENvbW1pdFN0YXR1cyI6IlNLSVBQRUQiLCJsaXZlRmVlZGJhY2siOnsicmVzb2x2ZWQiOjAsInVucmVzb2x2ZWQiOjAsInRvdGFsIjowLCJsaW5rIjoiIn19LHsibmFtZSI6InRoaXJkd2ViLXd3dyIsInJvb3REaXJlY3RvcnkiOiJhcHBzL2Rhc2hib2FyZCIsImluc3BlY3RvclVybCI6Imh0dHBzOi8vdmVyY2VsLmNvbS90aGlyZHdlYi90aGlyZHdlYi13d3cvNFBKVFJzRUNGWWRCdDh0QXhMUTRBS0VKeDZieiIsInByZXZpZXdVcmwiOiJ0aGlyZHdlYi13d3ctZ2l0LWFkZHRlYW1jYXBhYmlsaXRpZXN0b3NlcnZpY2UtdXRpbHMudGhpcmR3ZWItcHJldmlldy5jb20iLCJuZXh0Q29tbWl0U3RhdHVzIjoiREVQTE9ZRUQiLCJsaXZlRmVlZGJhY2siOnsicmVzb2x2ZWQiOjAsInVucmVzb2x2ZWQiOjAsInRvdGFsIjowLCJsaW5rIjoidGhpcmR3ZWItd3d3LWdpdC1hZGR0ZWFtY2FwYWJpbGl0aWVzdG9zZXJ2aWNlLXV0aWxzLnRoaXJkd2ViLXByZXZpZXcuY29tIn19LHsibmFtZSI6ImRvY3MtdjIiLCJyb290RGlyZWN0b3J5IjoiYXBwcy9wb3J0YWwiLCJpbnNwZWN0b3JVcmwiOiJodHRwczovL3ZlcmNlbC5jb20vdGhpcmR3ZWIvZG9jcy12Mi81czhFdGdTVVdHTVVqYzg5UG5UQmFBaFRBdlMyIiwicHJldmlld1VybCI6IiIsIm5leHRDb21taXRTdGF0dXMiOiJTS0lQUEVEIiwibGl2ZUZlZWRiYWNrIjp7InJlc29sdmVkIjowLCJ1bnJlc29sdmVkIjowLCJ0b3RhbCI6MCwibGluayI6IiJ9fSx7Im5hbWUiOiJsb2dpbiIsInJvb3REaXJlY3RvcnkiOiJhcHBzL2xvZ2luIiwiaW5zcGVjdG9yVXJsIjoiaHR0cHM6Ly92ZXJjZWwuY29tL3RoaXJkd2ViL2xvZ2luLzZ5QlJ2TW5xZHNLSG95YUdFdDRySlo5ZGdYa2IiLCJwcmV2aWV3VXJsIjoiIiwibmV4dENvbW1pdFN0YXR1cyI6IlNLSVBQRUQiLCJsaXZlRmVlZGJhY2siOnsicmVzb2x2ZWQiOjAsInVucmVzb2x2ZWQiOjAsInRvdGFsIjowLCJsaW5rIjoiIn19XX0=
**The latest updates on your projects**. Learn more about [Vercel for Git ↗︎](https://vercel.link/github-learn-more)

| Name | Status | Preview | Comments | Updated (UTC) |
| :--- | :----- | :------ | :------- | :------ |
| **thirdweb-www** | ✅ Ready ([Inspect](https://vercel.com/thirdweb/thirdweb-www/4PJTRsECFYdBt8tAxLQ4AKEJx6bz)) | [Visit Preview](https://thirdweb-www-git-addteamcapabilitiestoservice-utils.thirdweb-preview.com) | 💬 [**Add feedback**](https://vercel.live/open-feedback/thirdweb-www-git-addteamcapabilitiestoservice-utils.thirdweb-preview.com?via=pr-comment-feedback-link) | Apr 4, 2025 4:39am |

<details><summary>4 Skipped Deployments</summary>

| Name | Status | Preview | Comments | Updated (UTC) |
| :--- | :----- | :------ | :------- | :------ |
| **docs-v2** | ⬜️ Skipped ([Inspect](https://vercel.com/thirdweb/docs-v2/5s8EtgSUWGMUjc89PnTBaAhTAvS2)) |  |  | Apr 4, 2025 4:39am |
| **login** | ⬜️ Skipped ([Inspect](https://vercel.com/thirdweb/login/6yBRvMnqdsKHoyaGEt4rJZ9dgXkb)) |  |  | Apr 4, 2025 4:39am |
| **thirdweb_playground** | ⬜️ Skipped ([Inspect](https://vercel.com/thirdweb/thirdweb_playground/bU5tXcF6gJMiE4NthKashZYGXxYx)) |  |  | Apr 4, 2025 4:39am |
| **wallet-ui** | ⬜️ Skipped ([Inspect](https://vercel.com/thirdweb/wallet-ui/EqXeXPUoZodCQFQ12bDQNfT8ojei)) |  |  | Apr 4, 2025 4:39am |
</details>






author_association: NONE
user: login: jnsdls
created_at: 2025-04-03T21:25:01Z
body: * **#6641** <a href="https://app.graphite.dev/github/pr/thirdweb-dev/js/6641?utm_source=stack-comment-icon" target="_blank"><img src="https://static.graphite.dev/graphite-32x32-black.png" alt="Graphite" width="10px" height="10px"/></a> 👈 <a href="https://app.graphite.dev/github/pr/thirdweb-dev/js/6641?utm_source=stack-comment-view-in-graphite" target="_blank">(View in Graphite)</a>
* `main`
<hr />

## How to use the Graphite Merge Queue

Add either label to this PR to merge it via the merge queue:
- <kbd>merge-queue</kbd> - adds this PR to the back of the merge queue
- <kbd>hotfix</kbd> - for urgent hot fixes, skip the queue and merge this PR next

You must have a Graphite account in order to use the merge queue. Sign up using [this link](https://app.graphite.dev/invite/github/thirdweb-dev?ref=merge-queue-instructions-in-stack-comment&prId=6641).

<h2></h2>
<sub>

An organization admin has enabled the [Graphite Merge Queue](https://graphite.dev/docs/graphite-merge-queue) in this repository.

</sub>

<sub>

Please do not merge from GitHub as this will restart CI on PRs being processed by the merge queue.

</sub>

<h2></h2>

This stack of pull requests is managed by <a href="https://graphite.dev?utm-source=stack-comment"><b>Graphite</b></a>. Learn more about <a href="https://stacking.dev/?utm_source=stack-comment">stacking</a>.
<!-- Current dependencies on/for this PR: -->
author_association: MEMBER
user: login: codecov[bot]
created_at: 2025-04-03T21:26:30Z
body: ## [Codecov](https://app.codecov.io/gh/thirdweb-dev/js/pull/6641?dropdown=coverage&src=pr&el=h1&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=thirdweb-dev) Report
All modified and coverable lines are covered by tests :white_check_mark:
> Project coverage is 54.97%. Comparing base [(`04d2bc7`)](https://app.codecov.io/gh/thirdweb-dev/js/commit/04d2bc72e8993e883d1b79a78972b52291b8e27b?dropdown=coverage&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=thirdweb-dev) to head [(`8264670`)](https://app.codecov.io/gh/thirdweb-dev/js/commit/826467012119ed0e975794abab924937ce0334fd?dropdown=coverage&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=thirdweb-dev).
> Report is 1 commits behind head on main.

<details><summary>Additional details and impacted files</summary>


```diff
@@           Coverage Diff           @@
##             main    #6641   +/-   ##
=======================================
  Coverage   54.97%   54.97%           
=======================================
  Files         888      888           
  Lines       55755    55755           
  Branches     3816     3816           
=======================================
  Hits        30652    30652           
  Misses      25008    25008           
  Partials       95       95           
```

| [Flag](https://app.codecov.io/gh/thirdweb-dev/js/pull/6641/flags?src=pr&el=flags&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=thirdweb-dev) | Coverage Δ | |
|---|---|---|
| [packages](https://app.codecov.io/gh/thirdweb-dev/js/pull/6641/flags?src=pr&el=flag&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=thirdweb-dev) | `54.97% <ø> (ø)` | |

</details>
<details><summary> :rocket: New features to boost your workflow: </summary>

- :snowflake: [Test Analytics](https://docs.codecov.com/docs/test-analytics): Detect flaky tests, report on failures, and find test suite problems.
- :package: [JS Bundle Analysis](https://docs.codecov.com/docs/javascript-bundle-analysis): Save yourself from yourself by tracking and limiting bundle sizes in JS merges.
</details>
author_association: NONE
user: login: changeset-bot[bot]
created_at: 2025-04-03T21:27:16Z
body: ###  🦋  Changeset detected

Latest commit: 826467012119ed0e975794abab924937ce0334fd

**The changes in this PR will be included in the next version bump.**

<details><summary>This PR includes changesets to release 1 package</summary>

  | Name                        | Type  |
| --------------------------- | ----- |
| @thirdweb-dev/service-utils | Patch |

</details>

Not sure what this means? [Click here  to learn what changesets are](https://github.com/changesets/changesets/blob/main/docs/adding-a-changeset.md).

[Click here if you're a maintainer who wants to add another changeset to this PR](https://github.com/thirdweb-dev/js/new/Add_team_capabilities_to_service-utils?filename=.changeset/olive-pandas-beam.md&value=---%0A%22%40thirdweb-dev%2Fservice-utils%22%3A%20patch%0A%22thirdweb-dashboard%22%3A%20patch%0A---%0A%0AAdd%20team%20capabilities%20to%20service-utils%0A)


author_association: NONE
user: login: github-actions[bot]
created_at: 2025-04-03T21:33:02Z
body: ## size-limit report 📦 
| Path                                    | Size           | Loading time (3g) | Running time (snapdragon) | Total time |
| --------------------------------------- | -------------- | ----------------- | ------------------------- | ---------- |
| thirdweb (esm)                          | 47.57 KB (0%)  | 952 ms (0%)       | 188 ms (+117.83% 🔺)      | 1.2 s      |
| thirdweb (cjs)                          | 129.95 KB (0%) | 2.6 s (0%)        | 281 ms (+16.29% 🔺)       | 2.9 s      |
| thirdweb (minimal + tree-shaking)       | 5.62 KB (0%)   | 113 ms (0%)       | 91 ms (+1303.72% 🔺)      | 204 ms     |
| thirdweb/chains (tree-shaking)          | 514 B (0%)     | 11 ms (0%)        | 34 ms (+1510.05% 🔺)      | 44 ms      |
| thirdweb/react (minimal + tree-shaking) | 19.37 KB (0%)  | 388 ms (0%)       | 103 ms (+328.54% 🔺)      | 490 ms     |
author_association: CONTRIBUTOR