@lessly/sdk-app 93.0.0 → 94.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +15 -11
- package/dist/_types/index.d.ts +1 -1
- package/dist/_types/runtime/errors.d.ts +13 -0
- package/dist/index.cjs +23 -0
- package/dist/index.cjs.map +1 -1
- package/dist/index.js +22 -1
- package/dist/index.js.map +1 -1
- package/docs/README.md +1 -0
- package/docs/recipes/entitlements.md +96 -0
- package/docs/recipes/sdk-usage.md +5 -0
- package/package.json +1 -1
package/docs/README.md
CHANGED
|
@@ -74,6 +74,7 @@ you need to touch it directly.
|
|
|
74
74
|
| `recipes/sdk-usage.md` | You need to call the platform API: creating the SDK client, handling `LesslyApiError`, using the generated `*QueryOptions`/`*MutationOptions` factories. |
|
|
75
75
|
| `recipes/federation.md` | You're touching the Module Federation setup: shared singletons, the dual-mode build, or the dual-mode CSS graph (the federation vs standalone stylesheets). |
|
|
76
76
|
| `recipes/access.md` | You're gating UI on the caller's permissions: `app.access`, `useCan`, and rendering a 403 as an empty state rather than an error (APP-012). |
|
|
77
|
+
| `recipes/entitlements.md` | A call can fail with a 402 `entitlement_exhausted` / `entitlement_unavailable` (the organization reached its plan): `isEntitlementError` inside the shell, `upgradeUrl` outside it. |
|
|
77
78
|
| `recipes/local-dev.md` | You're running the app locally: the dev proxy, device-code login, `.env.local`, or composing your app under a local shell. |
|
|
78
79
|
|
|
79
80
|
## Keeping this guide current
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
# Recipe: the billing refusal (HTTP 402 `entitlement_exhausted` / `entitlement_unavailable`)
|
|
2
|
+
|
|
3
|
+
Some operations consume something the organization pays for — emails, storage,
|
|
4
|
+
a managed database. Before such an operation the toolkit asks billing whether
|
|
5
|
+
the organization's plan allows it. When it does not, the call fails with
|
|
6
|
+
**HTTP 402** and this body:
|
|
7
|
+
|
|
8
|
+
```json
|
|
9
|
+
{
|
|
10
|
+
"error": "entitlement_exhausted",
|
|
11
|
+
"reason": "trial_cap_reached",
|
|
12
|
+
"meter": "…",
|
|
13
|
+
"pool": "…",
|
|
14
|
+
"limit": 3000,
|
|
15
|
+
"used": 3000,
|
|
16
|
+
"requested": 1,
|
|
17
|
+
"plan": "trial",
|
|
18
|
+
"upgrade_url": "<Workspace web origin>/organization/billing?upgrade=pro&org=<org_id>"
|
|
19
|
+
}
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
| Field | Values |
|
|
23
|
+
| --- | --- |
|
|
24
|
+
| `error` | `entitlement_exhausted` (every reason except `trial_class_unavailable`), `entitlement_unavailable` |
|
|
25
|
+
| `reason` | `trial_cap_reached`, `trial_class_unavailable`, `billing_paused`, `budget_paused` |
|
|
26
|
+
| `plan` | `trial`, `pro`, `enterprise`, `none` |
|
|
27
|
+
|
|
28
|
+
This is not your App's error and not the user's mistake: it is the organization
|
|
29
|
+
reaching the edge of its plan. **The Workspace shell shows it** — one window,
|
|
30
|
+
with the reason and a path to Pro — for every federated App and for its own
|
|
31
|
+
calls. Your App renders nothing for it.
|
|
32
|
+
|
|
33
|
+
---
|
|
34
|
+
|
|
35
|
+
## 1. Inside the shell: recognise it, render nothing
|
|
36
|
+
|
|
37
|
+
```tsx
|
|
38
|
+
import { isAccessDenied, isEntitlementError } from '@lepsto/sdk-app';
|
|
39
|
+
|
|
40
|
+
function RequestError({ error }: { error: unknown }) {
|
|
41
|
+
// The shell already opened its refusal window. A second error here is a duplicate.
|
|
42
|
+
if (isEntitlementError(error)) return null;
|
|
43
|
+
if (isAccessDenied(error)) return <EmptyState title="You don't have access to this" />;
|
|
44
|
+
return <ErrorState error={error} />;
|
|
45
|
+
}
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
Put the branch in the one shared component that classifies errors (see
|
|
49
|
+
`access.md` §3 — state keeps the error *object*, never its message).
|
|
50
|
+
`isEntitlementError` is true for exactly the rule the shell uses: status `402`
|
|
51
|
+
and a body `error` of `entitlement_exhausted` or `entitlement_unavailable`. Any other 402 — a toolkit's
|
|
52
|
+
own payment failure — is false and stays yours to show.
|
|
53
|
+
|
|
54
|
+
Like `isAccessDenied`, it reads `status` and `body` structurally, so an error
|
|
55
|
+
thrown by another copy of this SDK on the page is recognised too.
|
|
56
|
+
|
|
57
|
+
## 2. Outside the shell: link to the upgrade path
|
|
58
|
+
|
|
59
|
+
A standalone App, or an App calling through its own `fetch`, is invisible to the
|
|
60
|
+
shell. Show a short notice with the link from `upgradeUrl(err)`:
|
|
61
|
+
|
|
62
|
+
```tsx
|
|
63
|
+
import { upgradeUrl } from '@lepsto/sdk-app';
|
|
64
|
+
|
|
65
|
+
const href = upgradeUrl(error);
|
|
66
|
+
if (href) {
|
|
67
|
+
return (
|
|
68
|
+
<Notice>
|
|
69
|
+
Your organization has reached its plan limit. <a href={href}>Upgrade to Pro</a>
|
|
70
|
+
</Notice>
|
|
71
|
+
);
|
|
72
|
+
}
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
With your own `fetch`, pass the status and the parsed body:
|
|
76
|
+
|
|
77
|
+
```ts
|
|
78
|
+
const res = await fetch(url, init);
|
|
79
|
+
if (!res.ok) {
|
|
80
|
+
const err = { status: res.status, body: await res.json().catch(() => null) };
|
|
81
|
+
if (isEntitlementError(err)) showUpgradeNotice(upgradeUrl(err));
|
|
82
|
+
}
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
`upgradeUrl` returns the URL billing sent, unchanged, and `null` when the error
|
|
86
|
+
is not an entitlement refusal or the URL is not an absolute `http(s)` URL. Never
|
|
87
|
+
build this URL yourself — it names the refused organization.
|
|
88
|
+
|
|
89
|
+
## What not to do
|
|
90
|
+
|
|
91
|
+
- **Do not retry** the refused call in a loop. Nothing changes until the
|
|
92
|
+
organization upgrades or the next period starts.
|
|
93
|
+
- **Do not cache** the refusal. After an upgrade the next call succeeds.
|
|
94
|
+
- **Do not render it red**, and inside the shell do not render it at all.
|
|
95
|
+
- **Do not branch on `reason`** to decide *whether* to show something — that is
|
|
96
|
+
`isEntitlementError`. `reason`, `used` and `limit` are for wording only.
|
|
@@ -71,6 +71,11 @@ try {
|
|
|
71
71
|
}
|
|
72
72
|
```
|
|
73
73
|
|
|
74
|
+
A **402** whose body `error` is `entitlement_exhausted` or
|
|
75
|
+
`entitlement_unavailable` is billing refusing the
|
|
76
|
+
call over the organization's plan. The Workspace shell shows it; see
|
|
77
|
+
`entitlements.md` for `isEntitlementError` and `upgradeUrl`.
|
|
78
|
+
|
|
74
79
|
### Missing CSRF cookie
|
|
75
80
|
|
|
76
81
|
Mutating calls (`POST`, `PUT`, `PATCH`, `DELETE`) require the `lepsto_csrf`
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@lessly/sdk-app",
|
|
3
3
|
"description": "The Lessly SDK for App is a TypeScript SDK that provides runtime bindings and type-safe generated code for interacting with Lessly Platform APIs and services. This package generates type definitions and client bindings from API catalogs, enabling developers to build applications that consume Lessly platform services with full type safety and autocompletion.",
|
|
4
|
-
"version": "
|
|
4
|
+
"version": "94.1.0",
|
|
5
5
|
"repository": {
|
|
6
6
|
"type": "git",
|
|
7
7
|
"url": "git+https://github.com/lepsto/lepsto-sdk-app.git"
|