-
Notifications
You must be signed in to change notification settings - Fork 546
Add design-system tier-limit gating and 402 handling #5285
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Draft
liamdebeasi
wants to merge
7
commits into
main
Choose a base branch
from
ai_main_00b22e04d17f465288cd
base: main
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
Draft
Changes from all commits
Commits
Show all changes
7 commits
Select commit
Hold shift + click to select a range
d1adb5b
feat(core): expose design-system tier limits and failures
builderio-bot b5c6335
fix(design): gate system indexing by tier limits
builderio-bot 868d740
test: cover Design System tier-limit gating and source locks
builderio-bot 12b9255
chore: format Design system tier-limit changes
builderio-bot f48da7a
Merge remote-tracking branch 'refs/remotes/origin/main' into ai_main_…
builderio-bot fbc003e
Commit unstaged changes
builderio-bot fb554e2
Merge remote-tracking branch 'refs/remotes/origin/main' into ai_main_…
builderio-bot File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,5 @@ | ||
| --- | ||
| "@agent-native/core": minor | ||
| --- | ||
|
|
||
| Add `fetchBuilderDesignSystemTierLimit`, `designSystemTierUpgradeUrl`, and the `@agent-native/core/client/design-system-tier-limit` helpers so apps can show a design-system plan/tier cap and an upgrade link before create, and surface the same information from a 402 on the create/index call. |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
105 changes: 105 additions & 0 deletions
105
packages/core/src/client/design-system-tier-limit.spec.ts
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,105 @@ | ||
| import { describe, expect, it } from "vitest"; | ||
|
|
||
| import { | ||
| DESIGN_SYSTEM_TIER_LIMIT_ERROR_CODE, | ||
| isDesignSystemCodeIndexingAllowed, | ||
| isDesignSystemTierAtMax, | ||
| readDesignSystemTierLimitFailure, | ||
| type DesignSystemTierLimit, | ||
| } from "./design-system-tier-limit.js"; | ||
|
|
||
| function tierLimit( | ||
| overrides: Partial<DesignSystemTierLimit> = {}, | ||
| ): DesignSystemTierLimit { | ||
| return { | ||
| status: "ok", | ||
| plan: "free", | ||
| current: 0, | ||
| max: 1, | ||
| atMax: false, | ||
| codeIndexingAllowed: false, | ||
| upgradeUrl: null, | ||
| ...overrides, | ||
| }; | ||
| } | ||
|
|
||
| describe("isDesignSystemTierAtMax", () => { | ||
| it("is false while the lookup is unresolved or unavailable", () => { | ||
| expect(isDesignSystemTierAtMax(undefined)).toBe(false); | ||
| expect(isDesignSystemTierAtMax(tierLimit({ status: "unavailable" }))).toBe( | ||
| false, | ||
| ); | ||
| }); | ||
|
|
||
| it("reflects atMax only once the lookup resolves", () => { | ||
| expect(isDesignSystemTierAtMax(tierLimit({ atMax: true }))).toBe(true); | ||
| expect(isDesignSystemTierAtMax(tierLimit({ atMax: false }))).toBe(false); | ||
| }); | ||
| }); | ||
|
|
||
| describe("isDesignSystemCodeIndexingAllowed", () => { | ||
| it("fails closed while the lookup is unresolved or unavailable, even if a stale value says allowed", () => { | ||
| expect(isDesignSystemCodeIndexingAllowed(undefined)).toBe(false); | ||
| expect( | ||
| isDesignSystemCodeIndexingAllowed( | ||
| tierLimit({ status: "unavailable", codeIndexingAllowed: true }), | ||
| ), | ||
| ).toBe(false); | ||
| }); | ||
|
|
||
| it("allows code indexing only once the plan is confirmed to permit it", () => { | ||
| expect( | ||
| isDesignSystemCodeIndexingAllowed( | ||
| tierLimit({ plan: "enterprise", codeIndexingAllowed: true }), | ||
| ), | ||
| ).toBe(true); | ||
| expect( | ||
| isDesignSystemCodeIndexingAllowed( | ||
| tierLimit({ plan: "pro", codeIndexingAllowed: false }), | ||
| ), | ||
| ).toBe(false); | ||
| }); | ||
| }); | ||
|
|
||
| describe("readDesignSystemTierLimitFailure", () => { | ||
| it("returns null for errors that are not the tier-limit contract error", () => { | ||
| expect( | ||
| readDesignSystemTierLimitFailure(new Error("boom"), "fallback"), | ||
| ).toBeNull(); | ||
| expect(readDesignSystemTierLimitFailure(null, "fallback")).toBeNull(); | ||
| }); | ||
|
|
||
| it("recovers plan/current/max/upgradeUrl from the error details", () => { | ||
| const error = Object.assign( | ||
| new Error("You have reached your design-system limit"), | ||
| { | ||
| errorCode: DESIGN_SYSTEM_TIER_LIMIT_ERROR_CODE, | ||
| details: { | ||
| plan: "pro", | ||
| current: 3, | ||
| max: 3, | ||
| upgradeUrl: "https://builder.io/account/subscription", | ||
| }, | ||
| }, | ||
| ); | ||
|
|
||
| expect(readDesignSystemTierLimitFailure(error, "fallback")).toEqual({ | ||
| message: "You have reached your design-system limit", | ||
| plan: "pro", | ||
| current: 3, | ||
| max: 3, | ||
| upgradeUrl: "https://builder.io/account/subscription", | ||
| }); | ||
| }); | ||
|
|
||
| it("falls back to the provided message when the error carries no message", () => { | ||
| const error = { | ||
| errorCode: DESIGN_SYSTEM_TIER_LIMIT_ERROR_CODE, | ||
| details: {}, | ||
| }; | ||
|
|
||
| expect(readDesignSystemTierLimitFailure(error, "fallback")).toMatchObject( | ||
| { message: "fallback", plan: null, current: null, max: null }, | ||
| ); | ||
| }); | ||
| }); |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,89 @@ | ||
| /** | ||
| * DSI tier-restriction contract shared by every design-system creation | ||
| * surface (Design, Slides). Both the proactive `get-design-system-tier-limit` | ||
| * action read and the reactive 402 from `index-design-system-with-builder` / | ||
| * `create-design-system` carry the same shape -- see | ||
| * `fetchBuilderDesignSystemTierLimit` and `assertBuilderDesignSystemIndexOk` | ||
| * in `@agent-native/core/server`. Kept in one place so a UI never has to | ||
| * re-derive "which plans allow code indexing" from a plan string. | ||
| */ | ||
|
|
||
| import { actionErrorMessage } from "./use-action.js"; | ||
|
|
||
| export const DESIGN_SYSTEM_TIER_LIMIT_ERROR_CODE = | ||
| "design_system_tier_limit_exceeded"; | ||
|
|
||
| /** Response shape of the `get-design-system-tier-limit` action. */ | ||
| export interface DesignSystemTierLimit { | ||
| status: "ok" | "unavailable"; | ||
| plan: string | null; | ||
| current: number | null; | ||
| max: number | null; | ||
| atMax: boolean; | ||
| codeIndexingAllowed: boolean; | ||
| upgradeUrl: string | null; | ||
| } | ||
|
|
||
| export interface DesignSystemTierLimitFailure { | ||
| message: string; | ||
| plan: string | null; | ||
| current: number | null; | ||
| max: number | null; | ||
| upgradeUrl: string | null; | ||
| } | ||
|
|
||
| /** | ||
| * Read a 402 design-system tier-limit failure off a thrown action error, or | ||
| * `null` when the error is something else. `errorCode`/`details` are the only | ||
| * fields the action transport preserves from `fail()` -- see | ||
| * `readFigmaImportFailure` for the same pattern applied to Figma import. | ||
| */ | ||
| export function readDesignSystemTierLimitFailure( | ||
| error: unknown, | ||
| fallbackMessage: string, | ||
| ): DesignSystemTierLimitFailure | null { | ||
| const source = error as | ||
| | { errorCode?: unknown; details?: Record<string, unknown> } | ||
| | undefined; | ||
| if (source?.errorCode !== DESIGN_SYSTEM_TIER_LIMIT_ERROR_CODE) return null; | ||
|
|
||
| const details = source.details ?? {}; | ||
| const text = (value: unknown) => | ||
| typeof value === "string" && value ? value : null; | ||
| const num = (value: unknown) => (typeof value === "number" ? value : null); | ||
|
|
||
| return { | ||
| message: | ||
| actionErrorMessage(error) ?? | ||
| (error instanceof Error ? error.message : undefined) ?? | ||
| fallbackMessage, | ||
| plan: text(details.plan), | ||
| current: num(details.current), | ||
| max: num(details.max), | ||
| upgradeUrl: text(details.upgradeUrl), | ||
| }; | ||
| } | ||
|
|
||
| /** True once `current` has reached `max` (unlimited plans never report true). */ | ||
| export function isDesignSystemTierAtMax( | ||
| limit: Pick<DesignSystemTierLimit, "status" | "atMax"> | null | undefined, | ||
| ): boolean { | ||
| return limit?.status === "ok" && limit.atMax === true; | ||
| } | ||
|
|
||
| /** | ||
| * True only once the plan is confirmed to allow code/GitHub indexing. | ||
| * Unlike {@link isDesignSystemTierAtMax}, an unresolved or `"unavailable"` | ||
| * lookup must read as `false`: nothing re-checks this Enterprise-only | ||
| * entitlement server-side at create time, so an unknown answer has to block | ||
| * the UI rather than let a non-Enterprise plan through while the tier-limit | ||
| * endpoint is loading or down. | ||
| */ | ||
| export function isDesignSystemCodeIndexingAllowed( | ||
| limit: | ||
| | Pick<DesignSystemTierLimit, "status" | "codeIndexingAllowed"> | ||
| | null | ||
| | undefined, | ||
| ): boolean { | ||
| return limit?.status === "ok" && limit.codeIndexingAllowed === true; | ||
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🟡 public core export has no changeset
This adds a new public
@agent-native/core/client/design-system-tier-limitexport, but the PR contains no.changeset/*.md. Sincepackages/coreis publishable, the required changeset check will fail and the export will not receive a versioned release.Additional Info