@usequota/nextjs 0.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/README.md +462 -0
- package/dist/chunk-BMI3VFWV.mjs +120 -0
- package/dist/chunk-RSPDHXC2.mjs +119 -0
- package/dist/chunk-SJ3X4KTV.mjs +117 -0
- package/dist/chunk-ZF7WJBQC.mjs +114 -0
- package/dist/errors-CmNx3kSz.d.mts +109 -0
- package/dist/errors-CmNx3kSz.d.ts +109 -0
- package/dist/errors-DVurmYT7.d.mts +109 -0
- package/dist/errors-DVurmYT7.d.ts +109 -0
- package/dist/index.d.mts +585 -0
- package/dist/index.d.ts +585 -0
- package/dist/index.js +1079 -0
- package/dist/index.mjs +968 -0
- package/dist/server.d.mts +477 -0
- package/dist/server.d.ts +477 -0
- package/dist/server.js +1068 -0
- package/dist/server.mjs +916 -0
- package/dist/styles.css +439 -0
- package/package.json +69 -0
package/README.md
ADDED
|
@@ -0,0 +1,462 @@
|
|
|
1
|
+
# @usequota/nextjs
|
|
2
|
+
|
|
3
|
+
Next.js SDK for [Quota](https://quota.io) - AI credit wallet and multi-provider inference API.
|
|
4
|
+
|
|
5
|
+
## Installation
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
npm install @usequota/nextjs
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
## Quick Start
|
|
12
|
+
|
|
13
|
+
### 1. Set up environment variables
|
|
14
|
+
|
|
15
|
+
```env
|
|
16
|
+
# .env.local
|
|
17
|
+
QUOTA_CLIENT_ID=your_client_id
|
|
18
|
+
QUOTA_CLIENT_SECRET=your_client_secret
|
|
19
|
+
NEXT_PUBLIC_QUOTA_CLIENT_ID=your_client_id
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
### 2. Configure middleware
|
|
23
|
+
|
|
24
|
+
Create `middleware.ts` in your project root:
|
|
25
|
+
|
|
26
|
+
```typescript
|
|
27
|
+
import { createQuotaMiddleware } from "@usequota/nextjs";
|
|
28
|
+
|
|
29
|
+
export const middleware = createQuotaMiddleware({
|
|
30
|
+
clientId: process.env.QUOTA_CLIENT_ID!,
|
|
31
|
+
clientSecret: process.env.QUOTA_CLIENT_SECRET!,
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
export const config = {
|
|
35
|
+
matcher: "/api/quota/callback",
|
|
36
|
+
};
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
### 3. Add QuotaProvider
|
|
40
|
+
|
|
41
|
+
Wrap your app with the provider in `app/layout.tsx`:
|
|
42
|
+
|
|
43
|
+
```tsx
|
|
44
|
+
import { QuotaProvider } from "@usequota/nextjs";
|
|
45
|
+
|
|
46
|
+
export default function RootLayout({ children }) {
|
|
47
|
+
return (
|
|
48
|
+
<html>
|
|
49
|
+
<body>
|
|
50
|
+
<QuotaProvider clientId={process.env.NEXT_PUBLIC_QUOTA_CLIENT_ID!}>
|
|
51
|
+
{children}
|
|
52
|
+
</QuotaProvider>
|
|
53
|
+
</body>
|
|
54
|
+
</html>
|
|
55
|
+
);
|
|
56
|
+
}
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
### 4. Create API routes
|
|
60
|
+
|
|
61
|
+
Create the following API routes:
|
|
62
|
+
|
|
63
|
+
**`app/api/quota/me/route.ts`** - Fetch user data:
|
|
64
|
+
|
|
65
|
+
```typescript
|
|
66
|
+
import { getQuotaUser } from "@usequota/nextjs/server";
|
|
67
|
+
|
|
68
|
+
export async function GET() {
|
|
69
|
+
const user = await getQuotaUser({
|
|
70
|
+
clientId: process.env.QUOTA_CLIENT_ID!,
|
|
71
|
+
clientSecret: process.env.QUOTA_CLIENT_SECRET!,
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
if (!user) {
|
|
75
|
+
return new Response("Unauthorized", { status: 401 });
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
return Response.json(user);
|
|
79
|
+
}
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
**`app/api/quota/logout/route.ts`** - Handle logout:
|
|
83
|
+
|
|
84
|
+
```typescript
|
|
85
|
+
import { clearQuotaAuth } from "@usequota/nextjs/server";
|
|
86
|
+
|
|
87
|
+
export async function POST() {
|
|
88
|
+
await clearQuotaAuth();
|
|
89
|
+
return Response.json({ success: true });
|
|
90
|
+
}
|
|
91
|
+
```
|
|
92
|
+
|
|
93
|
+
### 5. Use in components
|
|
94
|
+
|
|
95
|
+
```tsx
|
|
96
|
+
"use client";
|
|
97
|
+
|
|
98
|
+
import { useQuota } from "@usequota/nextjs";
|
|
99
|
+
|
|
100
|
+
export function MyComponent() {
|
|
101
|
+
const { user, isLoading, login } = useQuota();
|
|
102
|
+
|
|
103
|
+
if (isLoading) return <div>Loading...</div>;
|
|
104
|
+
if (!user) return <button onClick={login}>Sign in with Quota</button>;
|
|
105
|
+
|
|
106
|
+
return (
|
|
107
|
+
<div>
|
|
108
|
+
<p>Welcome, {user.email}!</p>
|
|
109
|
+
<p>Balance: {user.balance} credits</p>
|
|
110
|
+
</div>
|
|
111
|
+
);
|
|
112
|
+
}
|
|
113
|
+
```
|
|
114
|
+
|
|
115
|
+
## Features
|
|
116
|
+
|
|
117
|
+
- **OAuth 2.0 Authentication** - Secure user authentication flow
|
|
118
|
+
- **Token Management** - Automatic token refresh and cookie management
|
|
119
|
+
- **React Hooks** - Convenient hooks for auth state and user data
|
|
120
|
+
- **Server-Side Utilities** - Functions for API routes and server components
|
|
121
|
+
- **TypeScript Support** - Full type safety with TypeScript
|
|
122
|
+
- **Hosted Mode** - Optional server-side token storage for enhanced security
|
|
123
|
+
|
|
124
|
+
## API Reference
|
|
125
|
+
|
|
126
|
+
### Client-Side
|
|
127
|
+
|
|
128
|
+
#### Hooks
|
|
129
|
+
|
|
130
|
+
- `useQuota()` - Access full Quota context
|
|
131
|
+
- `useQuotaUser()` - Get current user
|
|
132
|
+
- `useQuotaAuth()` - Auth state and actions
|
|
133
|
+
- `useQuotaBalance()` - User credit balance
|
|
134
|
+
|
|
135
|
+
#### Components
|
|
136
|
+
|
|
137
|
+
- `<QuotaProvider>` - React context provider
|
|
138
|
+
|
|
139
|
+
### Server-Side
|
|
140
|
+
|
|
141
|
+
Import from `@usequota/nextjs/server`:
|
|
142
|
+
|
|
143
|
+
- `getQuotaUser(config)` - Get authenticated user
|
|
144
|
+
- `requireQuotaAuth(config)` - Require auth (redirects if not logged in)
|
|
145
|
+
- `getQuotaPackages(config?)` - Fetch available credit packages
|
|
146
|
+
- `createQuotaCheckout(config)` - Create Stripe checkout session
|
|
147
|
+
- `clearQuotaAuth(config?)` - Clear auth cookies
|
|
148
|
+
- `createQuotaRouteHandlers(config)` - Generate all 6 API route handlers in one call
|
|
149
|
+
- `withQuotaAuth(config, handler)` - Wrap a route handler with automatic Quota auth
|
|
150
|
+
|
|
151
|
+
#### Typed Errors
|
|
152
|
+
|
|
153
|
+
- `QuotaError` - Base error class (`code`, `statusCode`, `hint`)
|
|
154
|
+
- `QuotaInsufficientCreditsError` - 402, includes `balance` and `required`
|
|
155
|
+
- `QuotaNotConnectedError` - 401, user has no Quota account connected
|
|
156
|
+
- `QuotaTokenExpiredError` - 401, token expired and refresh failed
|
|
157
|
+
- `QuotaRateLimitError` - 429, includes `retryAfter` (seconds)
|
|
158
|
+
|
|
159
|
+
### Middleware
|
|
160
|
+
|
|
161
|
+
- `createQuotaMiddleware(config)` - Create OAuth callback middleware
|
|
162
|
+
|
|
163
|
+
## Storage Modes
|
|
164
|
+
|
|
165
|
+
### Client Mode (Default)
|
|
166
|
+
|
|
167
|
+
Tokens are stored in httpOnly cookies:
|
|
168
|
+
|
|
169
|
+
```typescript
|
|
170
|
+
export const middleware = createQuotaMiddleware({
|
|
171
|
+
clientId: process.env.QUOTA_CLIENT_ID!,
|
|
172
|
+
clientSecret: process.env.QUOTA_CLIENT_SECRET!,
|
|
173
|
+
storageMode: "client", // default
|
|
174
|
+
});
|
|
175
|
+
```
|
|
176
|
+
|
|
177
|
+
### Hosted Mode
|
|
178
|
+
|
|
179
|
+
Tokens stored server-side by Quota (more secure):
|
|
180
|
+
|
|
181
|
+
```typescript
|
|
182
|
+
export const middleware = createQuotaMiddleware({
|
|
183
|
+
clientId: process.env.QUOTA_CLIENT_ID!,
|
|
184
|
+
clientSecret: process.env.QUOTA_CLIENT_SECRET!,
|
|
185
|
+
storageMode: "hosted",
|
|
186
|
+
getExternalUserId: async (request) => {
|
|
187
|
+
// Return your user's ID from your auth system
|
|
188
|
+
const session = await getSession(request);
|
|
189
|
+
return session.userId;
|
|
190
|
+
},
|
|
191
|
+
});
|
|
192
|
+
```
|
|
193
|
+
|
|
194
|
+
## Advanced Usage
|
|
195
|
+
|
|
196
|
+
### Custom OAuth Flow
|
|
197
|
+
|
|
198
|
+
```typescript
|
|
199
|
+
const { login } = useQuota();
|
|
200
|
+
|
|
201
|
+
// Trigger OAuth flow
|
|
202
|
+
login();
|
|
203
|
+
```
|
|
204
|
+
|
|
205
|
+
### Server Components
|
|
206
|
+
|
|
207
|
+
```tsx
|
|
208
|
+
import { getQuotaUser } from "@usequota/nextjs/server";
|
|
209
|
+
|
|
210
|
+
export default async function DashboardPage() {
|
|
211
|
+
const user = await getQuotaUser({
|
|
212
|
+
clientId: process.env.QUOTA_CLIENT_ID!,
|
|
213
|
+
clientSecret: process.env.QUOTA_CLIENT_SECRET!,
|
|
214
|
+
});
|
|
215
|
+
|
|
216
|
+
if (!user) {
|
|
217
|
+
redirect("/");
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
return <div>Welcome, {user.email}!</div>;
|
|
221
|
+
}
|
|
222
|
+
```
|
|
223
|
+
|
|
224
|
+
### Protected Routes
|
|
225
|
+
|
|
226
|
+
```tsx
|
|
227
|
+
import { requireQuotaAuth } from "@usequota/nextjs/server";
|
|
228
|
+
|
|
229
|
+
export default async function ProtectedPage() {
|
|
230
|
+
// Automatically redirects to login if not authenticated
|
|
231
|
+
const user = await requireQuotaAuth({
|
|
232
|
+
clientId: process.env.QUOTA_CLIENT_ID!,
|
|
233
|
+
clientSecret: process.env.QUOTA_CLIENT_SECRET!,
|
|
234
|
+
});
|
|
235
|
+
|
|
236
|
+
return <div>Protected content for {user.email}</div>;
|
|
237
|
+
}
|
|
238
|
+
```
|
|
239
|
+
|
|
240
|
+
### Credit Packages
|
|
241
|
+
|
|
242
|
+
```tsx
|
|
243
|
+
"use client";
|
|
244
|
+
|
|
245
|
+
import { useState, useEffect } from "react";
|
|
246
|
+
import type { CreditPackage } from "@usequota/nextjs";
|
|
247
|
+
|
|
248
|
+
export function BuyCredits() {
|
|
249
|
+
const [packages, setPackages] = useState<CreditPackage[]>([]);
|
|
250
|
+
|
|
251
|
+
useEffect(() => {
|
|
252
|
+
fetch("/api/packages")
|
|
253
|
+
.then((res) => res.json())
|
|
254
|
+
.then(setPackages);
|
|
255
|
+
}, []);
|
|
256
|
+
|
|
257
|
+
const handleBuy = async (packageId: string) => {
|
|
258
|
+
const res = await fetch("/api/checkout", {
|
|
259
|
+
method: "POST",
|
|
260
|
+
headers: { "Content-Type": "application/json" },
|
|
261
|
+
body: JSON.stringify({ packageId }),
|
|
262
|
+
});
|
|
263
|
+
const { url } = await res.json();
|
|
264
|
+
window.location.href = url;
|
|
265
|
+
};
|
|
266
|
+
|
|
267
|
+
return (
|
|
268
|
+
<div>
|
|
269
|
+
{packages.map((pkg) => (
|
|
270
|
+
<button key={pkg.id} onClick={() => handleBuy(pkg.id)}>
|
|
271
|
+
Buy {pkg.credits} credits for {pkg.price_display}
|
|
272
|
+
</button>
|
|
273
|
+
))}
|
|
274
|
+
</div>
|
|
275
|
+
);
|
|
276
|
+
}
|
|
277
|
+
```
|
|
278
|
+
|
|
279
|
+
### Webhook Verification
|
|
280
|
+
|
|
281
|
+
Quota can send webhooks to notify your application about events. The SDK provides utilities to verify webhook signatures and handle events securely.
|
|
282
|
+
|
|
283
|
+
#### Set up webhook endpoint
|
|
284
|
+
|
|
285
|
+
Add your webhook secret to `.env.local`:
|
|
286
|
+
|
|
287
|
+
```env
|
|
288
|
+
QUOTA_WEBHOOK_SECRET=your_webhook_secret
|
|
289
|
+
```
|
|
290
|
+
|
|
291
|
+
#### Create webhook handler
|
|
292
|
+
|
|
293
|
+
**`app/api/quota/webhook/route.ts`**:
|
|
294
|
+
|
|
295
|
+
```typescript
|
|
296
|
+
import { createWebhookHandler } from "@usequota/nextjs";
|
|
297
|
+
|
|
298
|
+
export const POST = createWebhookHandler(process.env.QUOTA_WEBHOOK_SECRET!, {
|
|
299
|
+
"balance.low": async (event) => {
|
|
300
|
+
// Send email notification when user balance is low
|
|
301
|
+
await sendLowBalanceEmail(event.data.user_id, event.data.current_balance);
|
|
302
|
+
},
|
|
303
|
+
"user.connected": async (event) => {
|
|
304
|
+
// Track new user connection
|
|
305
|
+
await analytics.track("user_connected", event.data);
|
|
306
|
+
},
|
|
307
|
+
"usage.completed": async (event) => {
|
|
308
|
+
// Log usage for analytics
|
|
309
|
+
await logUsage(event.data);
|
|
310
|
+
},
|
|
311
|
+
});
|
|
312
|
+
```
|
|
313
|
+
|
|
314
|
+
#### Webhook event types
|
|
315
|
+
|
|
316
|
+
Quota sends the following webhook events:
|
|
317
|
+
|
|
318
|
+
- `user.connected` - User completed OAuth connection
|
|
319
|
+
- `user.disconnected` - User disconnected their account
|
|
320
|
+
- `balance.updated` - User's credit balance changed
|
|
321
|
+
- `balance.low` - User's balance fell below threshold
|
|
322
|
+
- `usage.completed` - API request completed and billed
|
|
323
|
+
|
|
324
|
+
#### Manual signature verification
|
|
325
|
+
|
|
326
|
+
For custom implementations:
|
|
327
|
+
|
|
328
|
+
```typescript
|
|
329
|
+
import { verifyWebhookSignature, parseWebhook } from "@usequota/nextjs";
|
|
330
|
+
|
|
331
|
+
export async function POST(req: Request) {
|
|
332
|
+
try {
|
|
333
|
+
// Option 1: Parse and verify in one step
|
|
334
|
+
const event = await parseWebhook(req, process.env.QUOTA_WEBHOOK_SECRET!);
|
|
335
|
+
|
|
336
|
+
// Option 2: Manual verification
|
|
337
|
+
const payload = await req.text();
|
|
338
|
+
const signature = req.headers.get("x-quota-signature")!;
|
|
339
|
+
|
|
340
|
+
const isValid = verifyWebhookSignature({
|
|
341
|
+
payload,
|
|
342
|
+
signature,
|
|
343
|
+
secret: process.env.QUOTA_WEBHOOK_SECRET!,
|
|
344
|
+
});
|
|
345
|
+
|
|
346
|
+
if (!isValid) {
|
|
347
|
+
return Response.json({ error: "Invalid signature" }, { status: 400 });
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
const event = JSON.parse(payload);
|
|
351
|
+
|
|
352
|
+
// Handle event...
|
|
353
|
+
|
|
354
|
+
return Response.json({ received: true });
|
|
355
|
+
} catch (error) {
|
|
356
|
+
return Response.json({ error: error.message }, { status: 400 });
|
|
357
|
+
}
|
|
358
|
+
}
|
|
359
|
+
```
|
|
360
|
+
|
|
361
|
+
## Route Handler Factory
|
|
362
|
+
|
|
363
|
+
`createQuotaRouteHandlers` generates all API route handlers for Quota integration in one call, replacing 6 separate route files with boilerplate.
|
|
364
|
+
|
|
365
|
+
```typescript
|
|
366
|
+
// lib/quota.ts
|
|
367
|
+
import { createQuotaRouteHandlers } from "@usequota/nextjs/server";
|
|
368
|
+
|
|
369
|
+
export const {
|
|
370
|
+
authorize, // GET - initiates OAuth flow
|
|
371
|
+
callback, // GET - handles OAuth callback
|
|
372
|
+
status, // GET - returns connection status + balance
|
|
373
|
+
packages, // GET - returns credit packages
|
|
374
|
+
checkout, // POST - creates Stripe checkout session
|
|
375
|
+
disconnect, // POST - disconnects user's Quota account
|
|
376
|
+
} = createQuotaRouteHandlers({
|
|
377
|
+
clientId: process.env.QUOTA_CLIENT_ID!,
|
|
378
|
+
clientSecret: process.env.QUOTA_CLIENT_SECRET!,
|
|
379
|
+
});
|
|
380
|
+
```
|
|
381
|
+
|
|
382
|
+
Then create thin route files:
|
|
383
|
+
|
|
384
|
+
```typescript
|
|
385
|
+
// app/api/quota/authorize/route.ts
|
|
386
|
+
export { authorize as GET } from "@/lib/quota";
|
|
387
|
+
|
|
388
|
+
// app/api/quota/callback/route.ts
|
|
389
|
+
export { callback as GET } from "@/lib/quota";
|
|
390
|
+
|
|
391
|
+
// app/api/quota/status/route.ts
|
|
392
|
+
export { status as GET } from "@/lib/quota";
|
|
393
|
+
```
|
|
394
|
+
|
|
395
|
+
## withQuotaAuth
|
|
396
|
+
|
|
397
|
+
Wraps a route handler with automatic Quota authentication, token refresh, and error handling:
|
|
398
|
+
|
|
399
|
+
```typescript
|
|
400
|
+
// app/api/summarize/route.ts
|
|
401
|
+
import { withQuotaAuth } from "@usequota/nextjs/server";
|
|
402
|
+
|
|
403
|
+
export const POST = withQuotaAuth(
|
|
404
|
+
{
|
|
405
|
+
clientId: process.env.QUOTA_CLIENT_ID!,
|
|
406
|
+
clientSecret: process.env.QUOTA_CLIENT_SECRET!,
|
|
407
|
+
},
|
|
408
|
+
async (request, { user, accessToken }) => {
|
|
409
|
+
// user is guaranteed to exist
|
|
410
|
+
// accessToken can proxy requests through Quota
|
|
411
|
+
const response = await fetch(
|
|
412
|
+
"https://api.usequota.app/v1/chat/completions",
|
|
413
|
+
{
|
|
414
|
+
method: "POST",
|
|
415
|
+
headers: {
|
|
416
|
+
Authorization: `Bearer ${accessToken}`,
|
|
417
|
+
"Content-Type": "application/json",
|
|
418
|
+
},
|
|
419
|
+
body: JSON.stringify({
|
|
420
|
+
model: "gpt-4o-mini",
|
|
421
|
+
messages: [{ role: "user", content: "Hello" }],
|
|
422
|
+
}),
|
|
423
|
+
},
|
|
424
|
+
);
|
|
425
|
+
return Response.json(await response.json());
|
|
426
|
+
},
|
|
427
|
+
);
|
|
428
|
+
```
|
|
429
|
+
|
|
430
|
+
## Typed Errors
|
|
431
|
+
|
|
432
|
+
Handle specific failure modes with `instanceof` checks:
|
|
433
|
+
|
|
434
|
+
```typescript
|
|
435
|
+
import {
|
|
436
|
+
QuotaInsufficientCreditsError,
|
|
437
|
+
QuotaNotConnectedError,
|
|
438
|
+
QuotaRateLimitError,
|
|
439
|
+
QuotaError,
|
|
440
|
+
} from "@usequota/nextjs/server";
|
|
441
|
+
|
|
442
|
+
try {
|
|
443
|
+
await callQuotaAPI();
|
|
444
|
+
} catch (e) {
|
|
445
|
+
if (e instanceof QuotaInsufficientCreditsError) {
|
|
446
|
+
console.log(e.balance, e.required); // current balance, credits needed
|
|
447
|
+
}
|
|
448
|
+
if (e instanceof QuotaNotConnectedError) {
|
|
449
|
+
// redirect to login
|
|
450
|
+
}
|
|
451
|
+
if (e instanceof QuotaRateLimitError) {
|
|
452
|
+
await delay(e.retryAfter * 1000); // seconds until retry
|
|
453
|
+
}
|
|
454
|
+
if (e instanceof QuotaError) {
|
|
455
|
+
console.log(e.code, e.statusCode, e.hint);
|
|
456
|
+
}
|
|
457
|
+
}
|
|
458
|
+
```
|
|
459
|
+
|
|
460
|
+
## License
|
|
461
|
+
|
|
462
|
+
MIT
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
// src/errors.ts
|
|
2
|
+
var QuotaError = class extends Error {
|
|
3
|
+
/** Machine-readable error code */
|
|
4
|
+
code;
|
|
5
|
+
/** HTTP status code associated with this error */
|
|
6
|
+
statusCode;
|
|
7
|
+
/** Optional hint for resolving the error */
|
|
8
|
+
hint;
|
|
9
|
+
constructor(message, code, statusCode, hint) {
|
|
10
|
+
super(message);
|
|
11
|
+
this.name = "QuotaError";
|
|
12
|
+
this.code = code;
|
|
13
|
+
this.statusCode = statusCode;
|
|
14
|
+
this.hint = hint;
|
|
15
|
+
Object.setPrototypeOf(this, new.target.prototype);
|
|
16
|
+
}
|
|
17
|
+
};
|
|
18
|
+
var QuotaInsufficientCreditsError = class extends QuotaError {
|
|
19
|
+
/** Current balance (if available) */
|
|
20
|
+
balance;
|
|
21
|
+
/** Credits required for the operation (if available) */
|
|
22
|
+
required;
|
|
23
|
+
constructor(message, options) {
|
|
24
|
+
super(
|
|
25
|
+
message ?? "Insufficient credits to complete this operation",
|
|
26
|
+
"insufficient_credits",
|
|
27
|
+
402,
|
|
28
|
+
"Purchase more credits or reduce usage"
|
|
29
|
+
);
|
|
30
|
+
this.name = "QuotaInsufficientCreditsError";
|
|
31
|
+
this.balance = options?.balance;
|
|
32
|
+
this.required = options?.required;
|
|
33
|
+
Object.setPrototypeOf(this, new.target.prototype);
|
|
34
|
+
}
|
|
35
|
+
};
|
|
36
|
+
var QuotaNotConnectedError = class extends QuotaError {
|
|
37
|
+
constructor(message) {
|
|
38
|
+
super(
|
|
39
|
+
message ?? "User has not connected a Quota account",
|
|
40
|
+
"not_connected",
|
|
41
|
+
401,
|
|
42
|
+
"Connect your Quota account to use this feature"
|
|
43
|
+
);
|
|
44
|
+
this.name = "QuotaNotConnectedError";
|
|
45
|
+
Object.setPrototypeOf(this, new.target.prototype);
|
|
46
|
+
}
|
|
47
|
+
};
|
|
48
|
+
var QuotaTokenExpiredError = class extends QuotaError {
|
|
49
|
+
constructor(message) {
|
|
50
|
+
super(
|
|
51
|
+
message ?? "Quota access token has expired and could not be refreshed",
|
|
52
|
+
"token_expired",
|
|
53
|
+
401,
|
|
54
|
+
"Reconnect your Quota account"
|
|
55
|
+
);
|
|
56
|
+
this.name = "QuotaTokenExpiredError";
|
|
57
|
+
Object.setPrototypeOf(this, new.target.prototype);
|
|
58
|
+
}
|
|
59
|
+
};
|
|
60
|
+
var QuotaRateLimitError = class extends QuotaError {
|
|
61
|
+
/** Seconds until the rate limit resets */
|
|
62
|
+
retryAfter;
|
|
63
|
+
constructor(message, retryAfter) {
|
|
64
|
+
super(
|
|
65
|
+
message ?? "Rate limit exceeded",
|
|
66
|
+
"rate_limit_exceeded",
|
|
67
|
+
429,
|
|
68
|
+
"Wait before retrying"
|
|
69
|
+
);
|
|
70
|
+
this.name = "QuotaRateLimitError";
|
|
71
|
+
this.retryAfter = retryAfter ?? 60;
|
|
72
|
+
Object.setPrototypeOf(this, new.target.prototype);
|
|
73
|
+
}
|
|
74
|
+
};
|
|
75
|
+
async function errorFromResponse(response) {
|
|
76
|
+
if (response.ok) {
|
|
77
|
+
return null;
|
|
78
|
+
}
|
|
79
|
+
let body;
|
|
80
|
+
try {
|
|
81
|
+
body = await response.json();
|
|
82
|
+
} catch {
|
|
83
|
+
}
|
|
84
|
+
const message = body?.error?.message ?? response.statusText;
|
|
85
|
+
const code = body?.error?.code;
|
|
86
|
+
switch (response.status) {
|
|
87
|
+
case 402: {
|
|
88
|
+
const opts = {};
|
|
89
|
+
if (body?.error?.balance !== void 0) opts.balance = body.error.balance;
|
|
90
|
+
if (body?.error?.required !== void 0)
|
|
91
|
+
opts.required = body.error.required;
|
|
92
|
+
return new QuotaInsufficientCreditsError(message, opts);
|
|
93
|
+
}
|
|
94
|
+
case 429: {
|
|
95
|
+
const parsed = parseInt(response.headers.get("retry-after") ?? "60", 10);
|
|
96
|
+
const retryAfter = Number.isNaN(parsed) ? 60 : parsed;
|
|
97
|
+
return new QuotaRateLimitError(message, retryAfter);
|
|
98
|
+
}
|
|
99
|
+
case 401: {
|
|
100
|
+
if (code === "not_connected") {
|
|
101
|
+
return new QuotaNotConnectedError(message);
|
|
102
|
+
}
|
|
103
|
+
if (code === "token_expired") {
|
|
104
|
+
return new QuotaTokenExpiredError(message);
|
|
105
|
+
}
|
|
106
|
+
return new QuotaTokenExpiredError(message);
|
|
107
|
+
}
|
|
108
|
+
default:
|
|
109
|
+
return new QuotaError(message, code ?? "unknown", response.status);
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
export {
|
|
114
|
+
QuotaError,
|
|
115
|
+
QuotaInsufficientCreditsError,
|
|
116
|
+
QuotaNotConnectedError,
|
|
117
|
+
QuotaTokenExpiredError,
|
|
118
|
+
QuotaRateLimitError,
|
|
119
|
+
errorFromResponse
|
|
120
|
+
};
|
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
// src/errors.ts
|
|
2
|
+
var QuotaError = class extends Error {
|
|
3
|
+
/** Machine-readable error code */
|
|
4
|
+
code;
|
|
5
|
+
/** HTTP status code associated with this error */
|
|
6
|
+
statusCode;
|
|
7
|
+
/** Optional hint for resolving the error */
|
|
8
|
+
hint;
|
|
9
|
+
constructor(message, code, statusCode, hint) {
|
|
10
|
+
super(message);
|
|
11
|
+
this.name = "QuotaError";
|
|
12
|
+
this.code = code;
|
|
13
|
+
this.statusCode = statusCode;
|
|
14
|
+
this.hint = hint;
|
|
15
|
+
Object.setPrototypeOf(this, new.target.prototype);
|
|
16
|
+
}
|
|
17
|
+
};
|
|
18
|
+
var QuotaInsufficientCreditsError = class extends QuotaError {
|
|
19
|
+
/** Current balance (if available) */
|
|
20
|
+
balance;
|
|
21
|
+
/** Credits required for the operation (if available) */
|
|
22
|
+
required;
|
|
23
|
+
constructor(message, options) {
|
|
24
|
+
super(
|
|
25
|
+
message ?? "Insufficient credits to complete this operation",
|
|
26
|
+
"insufficient_credits",
|
|
27
|
+
402,
|
|
28
|
+
"Purchase more credits or reduce usage"
|
|
29
|
+
);
|
|
30
|
+
this.name = "QuotaInsufficientCreditsError";
|
|
31
|
+
this.balance = options?.balance;
|
|
32
|
+
this.required = options?.required;
|
|
33
|
+
Object.setPrototypeOf(this, new.target.prototype);
|
|
34
|
+
}
|
|
35
|
+
};
|
|
36
|
+
var QuotaNotConnectedError = class extends QuotaError {
|
|
37
|
+
constructor(message) {
|
|
38
|
+
super(
|
|
39
|
+
message ?? "User has not connected a Quota account",
|
|
40
|
+
"not_connected",
|
|
41
|
+
401,
|
|
42
|
+
"Connect your Quota account to use this feature"
|
|
43
|
+
);
|
|
44
|
+
this.name = "QuotaNotConnectedError";
|
|
45
|
+
Object.setPrototypeOf(this, new.target.prototype);
|
|
46
|
+
}
|
|
47
|
+
};
|
|
48
|
+
var QuotaTokenExpiredError = class extends QuotaError {
|
|
49
|
+
constructor(message) {
|
|
50
|
+
super(
|
|
51
|
+
message ?? "Quota access token has expired and could not be refreshed",
|
|
52
|
+
"token_expired",
|
|
53
|
+
401,
|
|
54
|
+
"Reconnect your Quota account"
|
|
55
|
+
);
|
|
56
|
+
this.name = "QuotaTokenExpiredError";
|
|
57
|
+
Object.setPrototypeOf(this, new.target.prototype);
|
|
58
|
+
}
|
|
59
|
+
};
|
|
60
|
+
var QuotaRateLimitError = class extends QuotaError {
|
|
61
|
+
/** Seconds until the rate limit resets */
|
|
62
|
+
retryAfter;
|
|
63
|
+
constructor(message, retryAfter) {
|
|
64
|
+
super(
|
|
65
|
+
message ?? "Rate limit exceeded",
|
|
66
|
+
"rate_limit_exceeded",
|
|
67
|
+
429,
|
|
68
|
+
"Wait before retrying"
|
|
69
|
+
);
|
|
70
|
+
this.name = "QuotaRateLimitError";
|
|
71
|
+
this.retryAfter = retryAfter ?? 60;
|
|
72
|
+
Object.setPrototypeOf(this, new.target.prototype);
|
|
73
|
+
}
|
|
74
|
+
};
|
|
75
|
+
async function errorFromResponse(response) {
|
|
76
|
+
if (response.ok) {
|
|
77
|
+
return null;
|
|
78
|
+
}
|
|
79
|
+
let body;
|
|
80
|
+
try {
|
|
81
|
+
body = await response.json();
|
|
82
|
+
} catch {
|
|
83
|
+
}
|
|
84
|
+
const message = body?.error?.message ?? response.statusText;
|
|
85
|
+
const code = body?.error?.code;
|
|
86
|
+
switch (response.status) {
|
|
87
|
+
case 402: {
|
|
88
|
+
const opts = {};
|
|
89
|
+
if (body?.error?.balance !== void 0) opts.balance = body.error.balance;
|
|
90
|
+
if (body?.error?.required !== void 0)
|
|
91
|
+
opts.required = body.error.required;
|
|
92
|
+
return new QuotaInsufficientCreditsError(message, opts);
|
|
93
|
+
}
|
|
94
|
+
case 429: {
|
|
95
|
+
const retryAfter = parseInt(
|
|
96
|
+
response.headers.get("retry-after") ?? "60",
|
|
97
|
+
10
|
|
98
|
+
);
|
|
99
|
+
return new QuotaRateLimitError(message, retryAfter);
|
|
100
|
+
}
|
|
101
|
+
case 401: {
|
|
102
|
+
if (code === "not_connected") {
|
|
103
|
+
return new QuotaNotConnectedError(message);
|
|
104
|
+
}
|
|
105
|
+
return new QuotaTokenExpiredError(message);
|
|
106
|
+
}
|
|
107
|
+
default:
|
|
108
|
+
return new QuotaError(message, code ?? "unknown", response.status);
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
export {
|
|
113
|
+
QuotaError,
|
|
114
|
+
QuotaInsufficientCreditsError,
|
|
115
|
+
QuotaNotConnectedError,
|
|
116
|
+
QuotaTokenExpiredError,
|
|
117
|
+
QuotaRateLimitError,
|
|
118
|
+
errorFromResponse
|
|
119
|
+
};
|