@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/dist/index.mjs
ADDED
|
@@ -0,0 +1,968 @@
|
|
|
1
|
+
import {
|
|
2
|
+
QuotaError,
|
|
3
|
+
QuotaInsufficientCreditsError,
|
|
4
|
+
QuotaNotConnectedError,
|
|
5
|
+
QuotaRateLimitError,
|
|
6
|
+
QuotaTokenExpiredError
|
|
7
|
+
} from "./chunk-BMI3VFWV.mjs";
|
|
8
|
+
|
|
9
|
+
// src/middleware.ts
|
|
10
|
+
import { NextResponse } from "next/server";
|
|
11
|
+
var DEFAULT_BASE_URL = "https://api.usequota.app";
|
|
12
|
+
var DEFAULT_CALLBACK_PATH = "/api/quota/callback";
|
|
13
|
+
var DEFAULT_COOKIE_PREFIX = "quota";
|
|
14
|
+
var DEFAULT_COOKIE_MAX_AGE = 60 * 60 * 24 * 7;
|
|
15
|
+
var DEFAULT_STORAGE_MODE = "client";
|
|
16
|
+
function createQuotaMiddleware(config) {
|
|
17
|
+
const baseUrl = config.baseUrl ?? DEFAULT_BASE_URL;
|
|
18
|
+
const callbackPath = config.callbackPath ?? DEFAULT_CALLBACK_PATH;
|
|
19
|
+
const storageMode = config.storageMode ?? DEFAULT_STORAGE_MODE;
|
|
20
|
+
const cookiePrefix = config.cookie?.prefix ?? DEFAULT_COOKIE_PREFIX;
|
|
21
|
+
const cookieDomain = config.cookie?.domain;
|
|
22
|
+
const cookiePath = config.cookie?.path ?? "/";
|
|
23
|
+
const cookieMaxAge = config.cookie?.maxAge ?? DEFAULT_COOKIE_MAX_AGE;
|
|
24
|
+
return async function quotaMiddleware(request) {
|
|
25
|
+
const { pathname, searchParams } = request.nextUrl;
|
|
26
|
+
if (pathname !== callbackPath) {
|
|
27
|
+
return NextResponse.next();
|
|
28
|
+
}
|
|
29
|
+
const code = searchParams.get("code");
|
|
30
|
+
const state = searchParams.get("state");
|
|
31
|
+
const error = searchParams.get("error");
|
|
32
|
+
const errorDescription = searchParams.get("error_description");
|
|
33
|
+
if (error) {
|
|
34
|
+
const redirectUrl = new URL("/", request.url);
|
|
35
|
+
redirectUrl.searchParams.set("quota_error", error);
|
|
36
|
+
if (errorDescription) {
|
|
37
|
+
redirectUrl.searchParams.set(
|
|
38
|
+
"quota_error_description",
|
|
39
|
+
errorDescription
|
|
40
|
+
);
|
|
41
|
+
}
|
|
42
|
+
return NextResponse.redirect(redirectUrl);
|
|
43
|
+
}
|
|
44
|
+
if (!code || !state) {
|
|
45
|
+
const redirectUrl = new URL("/", request.url);
|
|
46
|
+
redirectUrl.searchParams.set("quota_error", "invalid_callback");
|
|
47
|
+
redirectUrl.searchParams.set(
|
|
48
|
+
"quota_error_description",
|
|
49
|
+
"Missing code or state parameter"
|
|
50
|
+
);
|
|
51
|
+
return NextResponse.redirect(redirectUrl);
|
|
52
|
+
}
|
|
53
|
+
try {
|
|
54
|
+
const tokenBody = {
|
|
55
|
+
grant_type: "authorization_code",
|
|
56
|
+
code,
|
|
57
|
+
client_id: config.clientId,
|
|
58
|
+
client_secret: config.clientSecret,
|
|
59
|
+
redirect_uri: new URL(callbackPath, request.url).toString()
|
|
60
|
+
};
|
|
61
|
+
if (storageMode === "hosted") {
|
|
62
|
+
if (!config.getExternalUserId) {
|
|
63
|
+
throw new Error(
|
|
64
|
+
"getExternalUserId is required for hosted storage mode"
|
|
65
|
+
);
|
|
66
|
+
}
|
|
67
|
+
const externalUserId = await config.getExternalUserId(request);
|
|
68
|
+
tokenBody.storage_mode = "hosted";
|
|
69
|
+
tokenBody.external_user_id = externalUserId;
|
|
70
|
+
}
|
|
71
|
+
const tokenResponse = await fetch(`${baseUrl}/oauth/token`, {
|
|
72
|
+
method: "POST",
|
|
73
|
+
headers: {
|
|
74
|
+
"Content-Type": "application/json"
|
|
75
|
+
},
|
|
76
|
+
body: JSON.stringify(tokenBody)
|
|
77
|
+
});
|
|
78
|
+
if (!tokenResponse.ok) {
|
|
79
|
+
const errorData = await tokenResponse.json();
|
|
80
|
+
const redirectUrl2 = new URL("/", request.url);
|
|
81
|
+
redirectUrl2.searchParams.set("quota_error", errorData.error);
|
|
82
|
+
redirectUrl2.searchParams.set(
|
|
83
|
+
"quota_error_description",
|
|
84
|
+
errorData.error_description
|
|
85
|
+
);
|
|
86
|
+
return NextResponse.redirect(redirectUrl2);
|
|
87
|
+
}
|
|
88
|
+
const tokenData = await tokenResponse.json();
|
|
89
|
+
const redirectUrl = new URL("/", request.url);
|
|
90
|
+
redirectUrl.searchParams.set("quota_success", "true");
|
|
91
|
+
const response = NextResponse.redirect(redirectUrl);
|
|
92
|
+
if ("access_token" in tokenData) {
|
|
93
|
+
const cookieOptions = [
|
|
94
|
+
`Path=${cookiePath}`,
|
|
95
|
+
"HttpOnly",
|
|
96
|
+
"Secure",
|
|
97
|
+
"SameSite=Lax",
|
|
98
|
+
`Max-Age=${cookieMaxAge}`
|
|
99
|
+
];
|
|
100
|
+
if (cookieDomain) {
|
|
101
|
+
cookieOptions.push(`Domain=${cookieDomain}`);
|
|
102
|
+
}
|
|
103
|
+
response.cookies.set(
|
|
104
|
+
`${cookiePrefix}_access_token`,
|
|
105
|
+
tokenData.access_token,
|
|
106
|
+
{
|
|
107
|
+
httpOnly: true,
|
|
108
|
+
secure: true,
|
|
109
|
+
sameSite: "lax",
|
|
110
|
+
path: cookiePath,
|
|
111
|
+
maxAge: cookieMaxAge,
|
|
112
|
+
domain: cookieDomain
|
|
113
|
+
}
|
|
114
|
+
);
|
|
115
|
+
response.cookies.set(
|
|
116
|
+
`${cookiePrefix}_refresh_token`,
|
|
117
|
+
tokenData.refresh_token,
|
|
118
|
+
{
|
|
119
|
+
httpOnly: true,
|
|
120
|
+
secure: true,
|
|
121
|
+
sameSite: "lax",
|
|
122
|
+
path: cookiePath,
|
|
123
|
+
maxAge: cookieMaxAge,
|
|
124
|
+
domain: cookieDomain
|
|
125
|
+
}
|
|
126
|
+
);
|
|
127
|
+
}
|
|
128
|
+
if ("storage_mode" in tokenData && tokenData.storage_mode === "hosted") {
|
|
129
|
+
response.cookies.set(
|
|
130
|
+
`${cookiePrefix}_external_user_id`,
|
|
131
|
+
tokenData.external_user_id,
|
|
132
|
+
{
|
|
133
|
+
httpOnly: true,
|
|
134
|
+
secure: true,
|
|
135
|
+
sameSite: "lax",
|
|
136
|
+
path: cookiePath,
|
|
137
|
+
maxAge: cookieMaxAge,
|
|
138
|
+
domain: cookieDomain
|
|
139
|
+
}
|
|
140
|
+
);
|
|
141
|
+
}
|
|
142
|
+
return response;
|
|
143
|
+
} catch (err) {
|
|
144
|
+
const redirectUrl = new URL("/", request.url);
|
|
145
|
+
redirectUrl.searchParams.set("quota_error", "token_exchange_failed");
|
|
146
|
+
redirectUrl.searchParams.set(
|
|
147
|
+
"quota_error_description",
|
|
148
|
+
err instanceof Error ? err.message : "Unknown error"
|
|
149
|
+
);
|
|
150
|
+
return NextResponse.redirect(redirectUrl);
|
|
151
|
+
}
|
|
152
|
+
};
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
// src/provider.tsx
|
|
156
|
+
import {
|
|
157
|
+
createContext,
|
|
158
|
+
useContext,
|
|
159
|
+
useState,
|
|
160
|
+
useEffect,
|
|
161
|
+
useCallback
|
|
162
|
+
} from "react";
|
|
163
|
+
import { jsx } from "react/jsx-runtime";
|
|
164
|
+
var QuotaContext = createContext(null);
|
|
165
|
+
var DEFAULT_BASE_URL2 = "https://api.usequota.app";
|
|
166
|
+
var DEFAULT_CALLBACK_PATH2 = "/api/quota/callback";
|
|
167
|
+
var DEFAULT_API_PATH = "/api/quota/me";
|
|
168
|
+
function QuotaProvider({
|
|
169
|
+
children,
|
|
170
|
+
clientId,
|
|
171
|
+
baseUrl = DEFAULT_BASE_URL2,
|
|
172
|
+
callbackPath = DEFAULT_CALLBACK_PATH2,
|
|
173
|
+
apiPath = DEFAULT_API_PATH
|
|
174
|
+
}) {
|
|
175
|
+
const [user, setUser] = useState(null);
|
|
176
|
+
const [isLoading, setIsLoading] = useState(true);
|
|
177
|
+
const [error, setError] = useState(null);
|
|
178
|
+
const fetchUser = useCallback(async () => {
|
|
179
|
+
try {
|
|
180
|
+
setIsLoading(true);
|
|
181
|
+
setError(null);
|
|
182
|
+
const response = await fetch(apiPath, {
|
|
183
|
+
credentials: "include"
|
|
184
|
+
});
|
|
185
|
+
if (response.status === 401) {
|
|
186
|
+
setUser(null);
|
|
187
|
+
return;
|
|
188
|
+
}
|
|
189
|
+
if (!response.ok) {
|
|
190
|
+
throw new Error(`Failed to fetch user: ${response.statusText}`);
|
|
191
|
+
}
|
|
192
|
+
const userData = await response.json();
|
|
193
|
+
setUser(userData);
|
|
194
|
+
} catch (err) {
|
|
195
|
+
const errorObj = err instanceof Error ? err : new Error("Unknown error");
|
|
196
|
+
setError(errorObj);
|
|
197
|
+
setUser(null);
|
|
198
|
+
} finally {
|
|
199
|
+
setIsLoading(false);
|
|
200
|
+
}
|
|
201
|
+
}, [apiPath]);
|
|
202
|
+
useEffect(() => {
|
|
203
|
+
void fetchUser();
|
|
204
|
+
}, [fetchUser]);
|
|
205
|
+
const login = useCallback(() => {
|
|
206
|
+
const state = generateRandomState();
|
|
207
|
+
if (typeof window !== "undefined") {
|
|
208
|
+
sessionStorage.setItem("quota_oauth_state", state);
|
|
209
|
+
}
|
|
210
|
+
const authUrl = new URL("/oauth/authorize", baseUrl);
|
|
211
|
+
authUrl.searchParams.set("response_type", "code");
|
|
212
|
+
authUrl.searchParams.set("client_id", clientId);
|
|
213
|
+
authUrl.searchParams.set("redirect_uri", getRedirectUri(callbackPath));
|
|
214
|
+
authUrl.searchParams.set("state", state);
|
|
215
|
+
authUrl.searchParams.set("scope", "credits:use");
|
|
216
|
+
window.location.href = authUrl.toString();
|
|
217
|
+
}, [baseUrl, clientId, callbackPath]);
|
|
218
|
+
const logout = useCallback(async () => {
|
|
219
|
+
try {
|
|
220
|
+
setIsLoading(true);
|
|
221
|
+
const response = await fetch("/api/quota/logout", {
|
|
222
|
+
method: "POST",
|
|
223
|
+
credentials: "include"
|
|
224
|
+
});
|
|
225
|
+
if (!response.ok) {
|
|
226
|
+
throw new Error("Failed to logout");
|
|
227
|
+
}
|
|
228
|
+
setUser(null);
|
|
229
|
+
setError(null);
|
|
230
|
+
} catch (err) {
|
|
231
|
+
const errorObj = err instanceof Error ? err : new Error("Unknown error");
|
|
232
|
+
setError(errorObj);
|
|
233
|
+
} finally {
|
|
234
|
+
setIsLoading(false);
|
|
235
|
+
}
|
|
236
|
+
}, []);
|
|
237
|
+
const refetch = useCallback(async () => {
|
|
238
|
+
await fetchUser();
|
|
239
|
+
}, [fetchUser]);
|
|
240
|
+
const value = {
|
|
241
|
+
user,
|
|
242
|
+
isLoading,
|
|
243
|
+
error,
|
|
244
|
+
login,
|
|
245
|
+
logout,
|
|
246
|
+
refetch
|
|
247
|
+
};
|
|
248
|
+
return /* @__PURE__ */ jsx(QuotaContext.Provider, { value, children });
|
|
249
|
+
}
|
|
250
|
+
function useQuota() {
|
|
251
|
+
const context = useContext(QuotaContext);
|
|
252
|
+
if (!context) {
|
|
253
|
+
throw new Error("useQuota must be used within QuotaProvider");
|
|
254
|
+
}
|
|
255
|
+
return context;
|
|
256
|
+
}
|
|
257
|
+
function generateRandomState() {
|
|
258
|
+
const array = new Uint8Array(32);
|
|
259
|
+
if (typeof window !== "undefined" && window.crypto) {
|
|
260
|
+
window.crypto.getRandomValues(array);
|
|
261
|
+
}
|
|
262
|
+
return Array.from(array, (byte) => byte.toString(16).padStart(2, "0")).join(
|
|
263
|
+
""
|
|
264
|
+
);
|
|
265
|
+
}
|
|
266
|
+
function getRedirectUri(callbackPath) {
|
|
267
|
+
if (typeof window === "undefined") {
|
|
268
|
+
return callbackPath;
|
|
269
|
+
}
|
|
270
|
+
return `${window.location.origin}${callbackPath}`;
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
// src/hooks.ts
|
|
274
|
+
function useQuotaUser() {
|
|
275
|
+
const { user } = useQuota();
|
|
276
|
+
return user;
|
|
277
|
+
}
|
|
278
|
+
function useQuotaAuth() {
|
|
279
|
+
const { user, isLoading, login, logout } = useQuota();
|
|
280
|
+
return {
|
|
281
|
+
isAuthenticated: user !== null,
|
|
282
|
+
isLoading,
|
|
283
|
+
login,
|
|
284
|
+
logout
|
|
285
|
+
};
|
|
286
|
+
}
|
|
287
|
+
function useQuotaBalance() {
|
|
288
|
+
const { user, isLoading, error, refetch } = useQuota();
|
|
289
|
+
return {
|
|
290
|
+
balance: user?.balance ?? null,
|
|
291
|
+
isLoading,
|
|
292
|
+
error,
|
|
293
|
+
refetch
|
|
294
|
+
};
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
// src/components/QuotaConnectButton.tsx
|
|
298
|
+
import { useCallback as useCallback2, useEffect as useEffect2, useRef } from "react";
|
|
299
|
+
import { Fragment, jsx as jsx2, jsxs } from "react/jsx-runtime";
|
|
300
|
+
function QuotaConnectButton({
|
|
301
|
+
children,
|
|
302
|
+
className,
|
|
303
|
+
onSuccess,
|
|
304
|
+
onError,
|
|
305
|
+
variant = "primary",
|
|
306
|
+
showLoadingState = true,
|
|
307
|
+
showWhenConnected = false
|
|
308
|
+
}) {
|
|
309
|
+
const { user, login, isLoading, error } = useQuota();
|
|
310
|
+
const wasLoadingRef = useRef(isLoading);
|
|
311
|
+
const hadUserRef = useRef(!!user);
|
|
312
|
+
useEffect2(() => {
|
|
313
|
+
const wasLoading = wasLoadingRef.current;
|
|
314
|
+
const hadUser = hadUserRef.current;
|
|
315
|
+
wasLoadingRef.current = isLoading;
|
|
316
|
+
hadUserRef.current = !!user;
|
|
317
|
+
if (wasLoading && !isLoading && user && !hadUser) {
|
|
318
|
+
onSuccess?.();
|
|
319
|
+
}
|
|
320
|
+
}, [user, isLoading, onSuccess]);
|
|
321
|
+
const handleClick = useCallback2(() => {
|
|
322
|
+
try {
|
|
323
|
+
login();
|
|
324
|
+
} catch (err) {
|
|
325
|
+
const errorObj = err instanceof Error ? err : new Error("Login failed");
|
|
326
|
+
onError?.(errorObj);
|
|
327
|
+
}
|
|
328
|
+
}, [login, onError]);
|
|
329
|
+
if (user && !showWhenConnected) {
|
|
330
|
+
return null;
|
|
331
|
+
}
|
|
332
|
+
if (user && showWhenConnected) {
|
|
333
|
+
return /* @__PURE__ */ jsxs(
|
|
334
|
+
"div",
|
|
335
|
+
{
|
|
336
|
+
className: `quota-button quota-button--secondary ${className || ""}`,
|
|
337
|
+
role: "status",
|
|
338
|
+
"aria-label": `Connected as ${user.email}`,
|
|
339
|
+
children: [
|
|
340
|
+
/* @__PURE__ */ jsx2(WalletIcon, {}),
|
|
341
|
+
/* @__PURE__ */ jsx2("span", { children: user.email })
|
|
342
|
+
]
|
|
343
|
+
}
|
|
344
|
+
);
|
|
345
|
+
}
|
|
346
|
+
const isButtonLoading = showLoadingState && isLoading;
|
|
347
|
+
return /* @__PURE__ */ jsxs(
|
|
348
|
+
"button",
|
|
349
|
+
{
|
|
350
|
+
type: "button",
|
|
351
|
+
onClick: handleClick,
|
|
352
|
+
disabled: isButtonLoading,
|
|
353
|
+
className: `quota-button quota-button--${variant} ${className || ""}`,
|
|
354
|
+
"aria-busy": isButtonLoading,
|
|
355
|
+
"aria-describedby": error ? "quota-connect-error" : void 0,
|
|
356
|
+
children: [
|
|
357
|
+
isButtonLoading ? /* @__PURE__ */ jsxs(Fragment, { children: [
|
|
358
|
+
/* @__PURE__ */ jsx2("span", { className: "quota-spinner", "aria-hidden": "true" }),
|
|
359
|
+
/* @__PURE__ */ jsx2("span", { children: "Connecting..." })
|
|
360
|
+
] }) : /* @__PURE__ */ jsxs(Fragment, { children: [
|
|
361
|
+
/* @__PURE__ */ jsx2(WalletIcon, {}),
|
|
362
|
+
/* @__PURE__ */ jsx2("span", { children: children || "Connect Wallet" })
|
|
363
|
+
] }),
|
|
364
|
+
error && /* @__PURE__ */ jsxs("span", { id: "quota-connect-error", className: "quota-sr-only", children: [
|
|
365
|
+
"Error: ",
|
|
366
|
+
error.message
|
|
367
|
+
] })
|
|
368
|
+
]
|
|
369
|
+
}
|
|
370
|
+
);
|
|
371
|
+
}
|
|
372
|
+
function WalletIcon() {
|
|
373
|
+
return /* @__PURE__ */ jsxs(
|
|
374
|
+
"svg",
|
|
375
|
+
{
|
|
376
|
+
width: "18",
|
|
377
|
+
height: "18",
|
|
378
|
+
viewBox: "0 0 24 24",
|
|
379
|
+
fill: "none",
|
|
380
|
+
stroke: "currentColor",
|
|
381
|
+
strokeWidth: "2",
|
|
382
|
+
strokeLinecap: "round",
|
|
383
|
+
strokeLinejoin: "round",
|
|
384
|
+
"aria-hidden": "true",
|
|
385
|
+
children: [
|
|
386
|
+
/* @__PURE__ */ jsx2("path", { d: "M19 7V4a1 1 0 0 0-1-1H5a2 2 0 0 0 0 4h15a1 1 0 0 1 1 1v4h-3a2 2 0 0 0 0 4h3a1 1 0 0 0 1-1v-2a1 1 0 0 0-1-1" }),
|
|
387
|
+
/* @__PURE__ */ jsx2("path", { d: "M3 5v14a2 2 0 0 0 2 2h15a1 1 0 0 0 1-1v-4" })
|
|
388
|
+
]
|
|
389
|
+
}
|
|
390
|
+
);
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
// src/components/QuotaBalance.tsx
|
|
394
|
+
import { jsx as jsx3, jsxs as jsxs2 } from "react/jsx-runtime";
|
|
395
|
+
function QuotaBalance({
|
|
396
|
+
format = "credits",
|
|
397
|
+
showIcon = true,
|
|
398
|
+
className,
|
|
399
|
+
ariaLabel = "Credit balance",
|
|
400
|
+
showRefresh = false,
|
|
401
|
+
onClick
|
|
402
|
+
}) {
|
|
403
|
+
const { balance, isLoading, error, refetch } = useQuotaBalance();
|
|
404
|
+
const formatBalance = (value) => {
|
|
405
|
+
if (value === null) return "---";
|
|
406
|
+
if (format === "dollars") {
|
|
407
|
+
const dollars = value / 100;
|
|
408
|
+
return new Intl.NumberFormat("en-US", {
|
|
409
|
+
style: "currency",
|
|
410
|
+
currency: "USD",
|
|
411
|
+
minimumFractionDigits: 2,
|
|
412
|
+
maximumFractionDigits: 2
|
|
413
|
+
}).format(dollars);
|
|
414
|
+
}
|
|
415
|
+
return new Intl.NumberFormat("en-US").format(value);
|
|
416
|
+
};
|
|
417
|
+
if (error) {
|
|
418
|
+
return /* @__PURE__ */ jsxs2(
|
|
419
|
+
"div",
|
|
420
|
+
{
|
|
421
|
+
className: `quota-balance ${className || ""}`,
|
|
422
|
+
role: "status",
|
|
423
|
+
"aria-label": "Error loading balance",
|
|
424
|
+
children: [
|
|
425
|
+
showIcon && /* @__PURE__ */ jsx3(ErrorIcon, {}),
|
|
426
|
+
/* @__PURE__ */ jsx3("span", { className: "quota-balance__error", children: "Error" })
|
|
427
|
+
]
|
|
428
|
+
}
|
|
429
|
+
);
|
|
430
|
+
}
|
|
431
|
+
const Component = onClick ? "button" : "div";
|
|
432
|
+
const interactiveProps = onClick ? {
|
|
433
|
+
onClick,
|
|
434
|
+
type: "button",
|
|
435
|
+
"aria-label": `${ariaLabel}: ${formatBalance(balance)}. Click to manage credits.`
|
|
436
|
+
} : {};
|
|
437
|
+
return /* @__PURE__ */ jsxs2(
|
|
438
|
+
Component,
|
|
439
|
+
{
|
|
440
|
+
className: `quota-balance ${isLoading ? "quota-balance--loading quota-loading-pulse" : ""} ${onClick ? "quota-balance--clickable" : ""} ${className || ""}`,
|
|
441
|
+
role: "status",
|
|
442
|
+
"aria-label": ariaLabel,
|
|
443
|
+
"aria-busy": isLoading,
|
|
444
|
+
...interactiveProps,
|
|
445
|
+
children: [
|
|
446
|
+
showIcon && (format === "dollars" ? /* @__PURE__ */ jsx3(DollarIcon, {}) : /* @__PURE__ */ jsx3(CoinIcon, {})),
|
|
447
|
+
/* @__PURE__ */ jsxs2("span", { className: "quota-balance__value", children: [
|
|
448
|
+
formatBalance(balance),
|
|
449
|
+
format === "credits" && balance !== null && /* @__PURE__ */ jsx3("span", { className: "quota-sr-only", children: " credits" })
|
|
450
|
+
] }),
|
|
451
|
+
showRefresh && !isLoading && /* @__PURE__ */ jsx3(
|
|
452
|
+
"button",
|
|
453
|
+
{
|
|
454
|
+
type: "button",
|
|
455
|
+
onClick: (e) => {
|
|
456
|
+
e.stopPropagation();
|
|
457
|
+
void refetch();
|
|
458
|
+
},
|
|
459
|
+
className: "quota-button quota-button--ghost",
|
|
460
|
+
"aria-label": "Refresh balance",
|
|
461
|
+
style: { padding: "4px", marginLeft: "4px" },
|
|
462
|
+
children: /* @__PURE__ */ jsx3(RefreshIcon, {})
|
|
463
|
+
}
|
|
464
|
+
)
|
|
465
|
+
]
|
|
466
|
+
}
|
|
467
|
+
);
|
|
468
|
+
}
|
|
469
|
+
function CoinIcon() {
|
|
470
|
+
return /* @__PURE__ */ jsxs2(
|
|
471
|
+
"svg",
|
|
472
|
+
{
|
|
473
|
+
className: "quota-balance__icon",
|
|
474
|
+
width: "16",
|
|
475
|
+
height: "16",
|
|
476
|
+
viewBox: "0 0 24 24",
|
|
477
|
+
fill: "none",
|
|
478
|
+
stroke: "currentColor",
|
|
479
|
+
strokeWidth: "2",
|
|
480
|
+
strokeLinecap: "round",
|
|
481
|
+
strokeLinejoin: "round",
|
|
482
|
+
"aria-hidden": "true",
|
|
483
|
+
children: [
|
|
484
|
+
/* @__PURE__ */ jsx3("circle", { cx: "12", cy: "12", r: "8" }),
|
|
485
|
+
/* @__PURE__ */ jsx3("path", { d: "M12 8v8" }),
|
|
486
|
+
/* @__PURE__ */ jsx3("path", { d: "M8 12h8" })
|
|
487
|
+
]
|
|
488
|
+
}
|
|
489
|
+
);
|
|
490
|
+
}
|
|
491
|
+
function DollarIcon() {
|
|
492
|
+
return /* @__PURE__ */ jsxs2(
|
|
493
|
+
"svg",
|
|
494
|
+
{
|
|
495
|
+
className: "quota-balance__icon",
|
|
496
|
+
width: "16",
|
|
497
|
+
height: "16",
|
|
498
|
+
viewBox: "0 0 24 24",
|
|
499
|
+
fill: "none",
|
|
500
|
+
stroke: "currentColor",
|
|
501
|
+
strokeWidth: "2",
|
|
502
|
+
strokeLinecap: "round",
|
|
503
|
+
strokeLinejoin: "round",
|
|
504
|
+
"aria-hidden": "true",
|
|
505
|
+
children: [
|
|
506
|
+
/* @__PURE__ */ jsx3("line", { x1: "12", y1: "2", x2: "12", y2: "22" }),
|
|
507
|
+
/* @__PURE__ */ jsx3("path", { d: "M17 5H9.5a3.5 3.5 0 0 0 0 7h5a3.5 3.5 0 0 1 0 7H6" })
|
|
508
|
+
]
|
|
509
|
+
}
|
|
510
|
+
);
|
|
511
|
+
}
|
|
512
|
+
function RefreshIcon() {
|
|
513
|
+
return /* @__PURE__ */ jsxs2(
|
|
514
|
+
"svg",
|
|
515
|
+
{
|
|
516
|
+
width: "14",
|
|
517
|
+
height: "14",
|
|
518
|
+
viewBox: "0 0 24 24",
|
|
519
|
+
fill: "none",
|
|
520
|
+
stroke: "currentColor",
|
|
521
|
+
strokeWidth: "2",
|
|
522
|
+
strokeLinecap: "round",
|
|
523
|
+
strokeLinejoin: "round",
|
|
524
|
+
"aria-hidden": "true",
|
|
525
|
+
children: [
|
|
526
|
+
/* @__PURE__ */ jsx3("path", { d: "M21 12a9 9 0 0 0-9-9 9.75 9.75 0 0 0-6.74 2.74L3 8" }),
|
|
527
|
+
/* @__PURE__ */ jsx3("path", { d: "M3 3v5h5" }),
|
|
528
|
+
/* @__PURE__ */ jsx3("path", { d: "M3 12a9 9 0 0 0 9 9 9.75 9.75 0 0 0 6.74-2.74L21 16" }),
|
|
529
|
+
/* @__PURE__ */ jsx3("path", { d: "M16 16h5v5" })
|
|
530
|
+
]
|
|
531
|
+
}
|
|
532
|
+
);
|
|
533
|
+
}
|
|
534
|
+
function ErrorIcon() {
|
|
535
|
+
return /* @__PURE__ */ jsxs2(
|
|
536
|
+
"svg",
|
|
537
|
+
{
|
|
538
|
+
className: "quota-balance__icon",
|
|
539
|
+
width: "16",
|
|
540
|
+
height: "16",
|
|
541
|
+
viewBox: "0 0 24 24",
|
|
542
|
+
fill: "none",
|
|
543
|
+
stroke: "currentColor",
|
|
544
|
+
strokeWidth: "2",
|
|
545
|
+
strokeLinecap: "round",
|
|
546
|
+
strokeLinejoin: "round",
|
|
547
|
+
"aria-hidden": "true",
|
|
548
|
+
children: [
|
|
549
|
+
/* @__PURE__ */ jsx3("circle", { cx: "12", cy: "12", r: "10" }),
|
|
550
|
+
/* @__PURE__ */ jsx3("line", { x1: "12", y1: "8", x2: "12", y2: "12" }),
|
|
551
|
+
/* @__PURE__ */ jsx3("line", { x1: "12", y1: "16", x2: "12.01", y2: "16" })
|
|
552
|
+
]
|
|
553
|
+
}
|
|
554
|
+
);
|
|
555
|
+
}
|
|
556
|
+
|
|
557
|
+
// src/components/QuotaBuyCredits.tsx
|
|
558
|
+
import { useState as useState2, useCallback as useCallback3 } from "react";
|
|
559
|
+
import { Fragment as Fragment2, jsx as jsx4, jsxs as jsxs3 } from "react/jsx-runtime";
|
|
560
|
+
function QuotaBuyCredits({
|
|
561
|
+
packageId,
|
|
562
|
+
amount,
|
|
563
|
+
children,
|
|
564
|
+
className,
|
|
565
|
+
onSuccess,
|
|
566
|
+
onError,
|
|
567
|
+
variant = "primary",
|
|
568
|
+
checkoutPath = "/api/quota/checkout",
|
|
569
|
+
disabled = false
|
|
570
|
+
}) {
|
|
571
|
+
const { user } = useQuota();
|
|
572
|
+
const [isLoading, setIsLoading] = useState2(false);
|
|
573
|
+
const [error, setError] = useState2(null);
|
|
574
|
+
const handlePurchase = useCallback3(async () => {
|
|
575
|
+
if (!user) {
|
|
576
|
+
const err = new Error("Must be logged in to purchase credits");
|
|
577
|
+
setError(err);
|
|
578
|
+
onError?.(err);
|
|
579
|
+
return;
|
|
580
|
+
}
|
|
581
|
+
setIsLoading(true);
|
|
582
|
+
setError(null);
|
|
583
|
+
try {
|
|
584
|
+
const body = {};
|
|
585
|
+
if (packageId) {
|
|
586
|
+
body.package_id = packageId;
|
|
587
|
+
} else if (amount) {
|
|
588
|
+
body.amount = amount;
|
|
589
|
+
}
|
|
590
|
+
const response = await fetch(checkoutPath, {
|
|
591
|
+
method: "POST",
|
|
592
|
+
headers: {
|
|
593
|
+
"Content-Type": "application/json"
|
|
594
|
+
},
|
|
595
|
+
credentials: "include",
|
|
596
|
+
body: JSON.stringify(body)
|
|
597
|
+
});
|
|
598
|
+
if (!response.ok) {
|
|
599
|
+
const errorData = await response.json().catch(() => ({}));
|
|
600
|
+
throw new Error(
|
|
601
|
+
errorData.error?.message || `Checkout failed: ${response.statusText}`
|
|
602
|
+
);
|
|
603
|
+
}
|
|
604
|
+
const data = await response.json();
|
|
605
|
+
if (!data.url) {
|
|
606
|
+
throw new Error("No checkout URL returned");
|
|
607
|
+
}
|
|
608
|
+
onSuccess?.();
|
|
609
|
+
window.location.href = data.url;
|
|
610
|
+
} catch (err) {
|
|
611
|
+
const errorObj = err instanceof Error ? err : new Error("Failed to create checkout");
|
|
612
|
+
setError(errorObj);
|
|
613
|
+
onError?.(errorObj);
|
|
614
|
+
} finally {
|
|
615
|
+
setIsLoading(false);
|
|
616
|
+
}
|
|
617
|
+
}, [user, packageId, amount, checkoutPath, onSuccess, onError]);
|
|
618
|
+
const isDisabled = disabled || isLoading || !user;
|
|
619
|
+
return /* @__PURE__ */ jsxs3(
|
|
620
|
+
"button",
|
|
621
|
+
{
|
|
622
|
+
type: "button",
|
|
623
|
+
onClick: handlePurchase,
|
|
624
|
+
disabled: isDisabled,
|
|
625
|
+
className: `quota-button quota-button--${variant} ${className || ""}`,
|
|
626
|
+
"aria-busy": isLoading,
|
|
627
|
+
"aria-describedby": error ? "quota-buy-error" : void 0,
|
|
628
|
+
children: [
|
|
629
|
+
isLoading ? /* @__PURE__ */ jsxs3(Fragment2, { children: [
|
|
630
|
+
/* @__PURE__ */ jsx4("span", { className: "quota-spinner", "aria-hidden": "true" }),
|
|
631
|
+
/* @__PURE__ */ jsx4("span", { children: "Processing..." })
|
|
632
|
+
] }) : /* @__PURE__ */ jsxs3(Fragment2, { children: [
|
|
633
|
+
/* @__PURE__ */ jsx4(CartIcon, {}),
|
|
634
|
+
/* @__PURE__ */ jsx4("span", { children: children || "Buy Credits" })
|
|
635
|
+
] }),
|
|
636
|
+
error && /* @__PURE__ */ jsxs3("span", { id: "quota-buy-error", className: "quota-sr-only", children: [
|
|
637
|
+
"Error: ",
|
|
638
|
+
error.message
|
|
639
|
+
] })
|
|
640
|
+
]
|
|
641
|
+
}
|
|
642
|
+
);
|
|
643
|
+
}
|
|
644
|
+
function CartIcon() {
|
|
645
|
+
return /* @__PURE__ */ jsxs3(
|
|
646
|
+
"svg",
|
|
647
|
+
{
|
|
648
|
+
width: "18",
|
|
649
|
+
height: "18",
|
|
650
|
+
viewBox: "0 0 24 24",
|
|
651
|
+
fill: "none",
|
|
652
|
+
stroke: "currentColor",
|
|
653
|
+
strokeWidth: "2",
|
|
654
|
+
strokeLinecap: "round",
|
|
655
|
+
strokeLinejoin: "round",
|
|
656
|
+
"aria-hidden": "true",
|
|
657
|
+
children: [
|
|
658
|
+
/* @__PURE__ */ jsx4("circle", { cx: "8", cy: "21", r: "1" }),
|
|
659
|
+
/* @__PURE__ */ jsx4("circle", { cx: "19", cy: "21", r: "1" }),
|
|
660
|
+
/* @__PURE__ */ jsx4("path", { d: "M2.05 2.05h2l2.66 12.42a2 2 0 0 0 2 1.58h9.78a2 2 0 0 0 1.95-1.57l1.65-7.43H5.12" })
|
|
661
|
+
]
|
|
662
|
+
}
|
|
663
|
+
);
|
|
664
|
+
}
|
|
665
|
+
|
|
666
|
+
// src/components/QuotaUserMenu.tsx
|
|
667
|
+
import {
|
|
668
|
+
useState as useState3,
|
|
669
|
+
useCallback as useCallback4,
|
|
670
|
+
useRef as useRef2,
|
|
671
|
+
useEffect as useEffect3
|
|
672
|
+
} from "react";
|
|
673
|
+
import { Fragment as Fragment3, jsx as jsx5, jsxs as jsxs4 } from "react/jsx-runtime";
|
|
674
|
+
function QuotaUserMenu({
|
|
675
|
+
className,
|
|
676
|
+
onBuyCredits,
|
|
677
|
+
onLogout,
|
|
678
|
+
showBuyCredits = true,
|
|
679
|
+
children
|
|
680
|
+
}) {
|
|
681
|
+
const { user, logout, isLoading: authLoading } = useQuota();
|
|
682
|
+
const { balance } = useQuotaBalance();
|
|
683
|
+
const [isOpen, setIsOpen] = useState3(false);
|
|
684
|
+
const [isLoggingOut, setIsLoggingOut] = useState3(false);
|
|
685
|
+
const menuRef = useRef2(null);
|
|
686
|
+
const triggerRef = useRef2(null);
|
|
687
|
+
useEffect3(() => {
|
|
688
|
+
if (!isOpen) return;
|
|
689
|
+
const handleClickOutside = (event) => {
|
|
690
|
+
if (menuRef.current && !menuRef.current.contains(event.target)) {
|
|
691
|
+
setIsOpen(false);
|
|
692
|
+
}
|
|
693
|
+
};
|
|
694
|
+
const handleEscape = (event) => {
|
|
695
|
+
if (event.key === "Escape") {
|
|
696
|
+
setIsOpen(false);
|
|
697
|
+
triggerRef.current?.focus();
|
|
698
|
+
}
|
|
699
|
+
};
|
|
700
|
+
document.addEventListener("mousedown", handleClickOutside);
|
|
701
|
+
document.addEventListener("keydown", handleEscape);
|
|
702
|
+
return () => {
|
|
703
|
+
document.removeEventListener("mousedown", handleClickOutside);
|
|
704
|
+
document.removeEventListener("keydown", handleEscape);
|
|
705
|
+
};
|
|
706
|
+
}, [isOpen]);
|
|
707
|
+
const handleToggle = useCallback4(() => {
|
|
708
|
+
setIsOpen((prev) => !prev);
|
|
709
|
+
}, []);
|
|
710
|
+
const handleKeyDown = useCallback4(
|
|
711
|
+
(event) => {
|
|
712
|
+
if (event.key === "Enter" || event.key === " ") {
|
|
713
|
+
event.preventDefault();
|
|
714
|
+
setIsOpen((prev) => !prev);
|
|
715
|
+
}
|
|
716
|
+
if (event.key === "ArrowDown" && !isOpen) {
|
|
717
|
+
event.preventDefault();
|
|
718
|
+
setIsOpen(true);
|
|
719
|
+
}
|
|
720
|
+
},
|
|
721
|
+
[isOpen]
|
|
722
|
+
);
|
|
723
|
+
const handleLogout = useCallback4(async () => {
|
|
724
|
+
setIsLoggingOut(true);
|
|
725
|
+
try {
|
|
726
|
+
await logout();
|
|
727
|
+
onLogout?.();
|
|
728
|
+
} finally {
|
|
729
|
+
setIsLoggingOut(false);
|
|
730
|
+
setIsOpen(false);
|
|
731
|
+
}
|
|
732
|
+
}, [logout, onLogout]);
|
|
733
|
+
const handleBuyCredits = useCallback4(() => {
|
|
734
|
+
setIsOpen(false);
|
|
735
|
+
onBuyCredits?.();
|
|
736
|
+
}, [onBuyCredits]);
|
|
737
|
+
if (!user) {
|
|
738
|
+
return null;
|
|
739
|
+
}
|
|
740
|
+
const initials = getUserInitials(user.email);
|
|
741
|
+
const formattedBalance = balance !== null ? new Intl.NumberFormat("en-US").format(balance) : "---";
|
|
742
|
+
const formattedDollars = balance !== null ? new Intl.NumberFormat("en-US", {
|
|
743
|
+
style: "currency",
|
|
744
|
+
currency: "USD",
|
|
745
|
+
minimumFractionDigits: 2
|
|
746
|
+
}).format(balance / 100) : "---";
|
|
747
|
+
return /* @__PURE__ */ jsxs4("div", { className: `quota-user-menu ${className || ""}`, ref: menuRef, children: [
|
|
748
|
+
/* @__PURE__ */ jsxs4(
|
|
749
|
+
"button",
|
|
750
|
+
{
|
|
751
|
+
ref: triggerRef,
|
|
752
|
+
type: "button",
|
|
753
|
+
className: "quota-user-menu__trigger",
|
|
754
|
+
onClick: handleToggle,
|
|
755
|
+
onKeyDown: handleKeyDown,
|
|
756
|
+
"aria-expanded": isOpen,
|
|
757
|
+
"aria-haspopup": "menu",
|
|
758
|
+
"aria-label": `Account menu for ${user.email}`,
|
|
759
|
+
children: [
|
|
760
|
+
/* @__PURE__ */ jsx5("span", { className: "quota-user-menu__avatar", "aria-hidden": "true", children: initials }),
|
|
761
|
+
/* @__PURE__ */ jsx5("span", { className: "quota-user-menu__email", children: user.email }),
|
|
762
|
+
/* @__PURE__ */ jsx5(ChevronIcon, {})
|
|
763
|
+
]
|
|
764
|
+
}
|
|
765
|
+
),
|
|
766
|
+
/* @__PURE__ */ jsxs4(
|
|
767
|
+
"div",
|
|
768
|
+
{
|
|
769
|
+
className: `quota-user-menu__dropdown ${isOpen ? "quota-user-menu__dropdown--open" : ""}`,
|
|
770
|
+
role: "menu",
|
|
771
|
+
"aria-label": "Account menu",
|
|
772
|
+
children: [
|
|
773
|
+
/* @__PURE__ */ jsxs4("div", { className: "quota-user-menu__section", children: [
|
|
774
|
+
/* @__PURE__ */ jsx5("div", { className: "quota-user-menu__label", children: "Balance" }),
|
|
775
|
+
/* @__PURE__ */ jsxs4("div", { className: "quota-user-menu__balance-value", children: [
|
|
776
|
+
formattedBalance,
|
|
777
|
+
" ",
|
|
778
|
+
/* @__PURE__ */ jsx5("span", { style: { fontSize: "12px", fontWeight: 400 }, children: "credits" })
|
|
779
|
+
] }),
|
|
780
|
+
/* @__PURE__ */ jsx5("div", { className: "quota-user-menu__balance-dollars", children: formattedDollars })
|
|
781
|
+
] }),
|
|
782
|
+
/* @__PURE__ */ jsxs4("div", { className: "quota-user-menu__section", children: [
|
|
783
|
+
showBuyCredits && /* @__PURE__ */ jsxs4(
|
|
784
|
+
"button",
|
|
785
|
+
{
|
|
786
|
+
type: "button",
|
|
787
|
+
className: "quota-user-menu__item",
|
|
788
|
+
role: "menuitem",
|
|
789
|
+
onClick: handleBuyCredits,
|
|
790
|
+
children: [
|
|
791
|
+
/* @__PURE__ */ jsx5(PlusIcon, {}),
|
|
792
|
+
/* @__PURE__ */ jsx5("span", { children: "Buy Credits" })
|
|
793
|
+
]
|
|
794
|
+
}
|
|
795
|
+
),
|
|
796
|
+
children,
|
|
797
|
+
/* @__PURE__ */ jsx5(
|
|
798
|
+
"button",
|
|
799
|
+
{
|
|
800
|
+
type: "button",
|
|
801
|
+
className: "quota-user-menu__item quota-user-menu__item--danger",
|
|
802
|
+
role: "menuitem",
|
|
803
|
+
onClick: handleLogout,
|
|
804
|
+
disabled: isLoggingOut || authLoading,
|
|
805
|
+
children: isLoggingOut ? /* @__PURE__ */ jsxs4(Fragment3, { children: [
|
|
806
|
+
/* @__PURE__ */ jsx5("span", { className: "quota-spinner", "aria-hidden": "true" }),
|
|
807
|
+
/* @__PURE__ */ jsx5("span", { children: "Signing out..." })
|
|
808
|
+
] }) : /* @__PURE__ */ jsxs4(Fragment3, { children: [
|
|
809
|
+
/* @__PURE__ */ jsx5(LogoutIcon, {}),
|
|
810
|
+
/* @__PURE__ */ jsx5("span", { children: "Sign Out" })
|
|
811
|
+
] })
|
|
812
|
+
}
|
|
813
|
+
)
|
|
814
|
+
] })
|
|
815
|
+
]
|
|
816
|
+
}
|
|
817
|
+
)
|
|
818
|
+
] });
|
|
819
|
+
}
|
|
820
|
+
function getUserInitials(email) {
|
|
821
|
+
const localPart = email.split("@")[0];
|
|
822
|
+
if (!localPart) return "?";
|
|
823
|
+
const parts = localPart.split(/[._-]/);
|
|
824
|
+
const firstPart = parts[0];
|
|
825
|
+
const secondPart = parts[1];
|
|
826
|
+
if (parts.length >= 2 && firstPart && secondPart && firstPart[0] && secondPart[0]) {
|
|
827
|
+
return (firstPart[0] + secondPart[0]).toUpperCase();
|
|
828
|
+
}
|
|
829
|
+
return localPart.slice(0, 2).toUpperCase();
|
|
830
|
+
}
|
|
831
|
+
function ChevronIcon() {
|
|
832
|
+
return /* @__PURE__ */ jsx5(
|
|
833
|
+
"svg",
|
|
834
|
+
{
|
|
835
|
+
className: "quota-user-menu__chevron",
|
|
836
|
+
width: "16",
|
|
837
|
+
height: "16",
|
|
838
|
+
viewBox: "0 0 24 24",
|
|
839
|
+
fill: "none",
|
|
840
|
+
stroke: "currentColor",
|
|
841
|
+
strokeWidth: "2",
|
|
842
|
+
strokeLinecap: "round",
|
|
843
|
+
strokeLinejoin: "round",
|
|
844
|
+
"aria-hidden": "true",
|
|
845
|
+
children: /* @__PURE__ */ jsx5("path", { d: "m6 9 6 6 6-6" })
|
|
846
|
+
}
|
|
847
|
+
);
|
|
848
|
+
}
|
|
849
|
+
function PlusIcon() {
|
|
850
|
+
return /* @__PURE__ */ jsxs4(
|
|
851
|
+
"svg",
|
|
852
|
+
{
|
|
853
|
+
className: "quota-user-menu__item-icon",
|
|
854
|
+
width: "18",
|
|
855
|
+
height: "18",
|
|
856
|
+
viewBox: "0 0 24 24",
|
|
857
|
+
fill: "none",
|
|
858
|
+
stroke: "currentColor",
|
|
859
|
+
strokeWidth: "2",
|
|
860
|
+
strokeLinecap: "round",
|
|
861
|
+
strokeLinejoin: "round",
|
|
862
|
+
"aria-hidden": "true",
|
|
863
|
+
children: [
|
|
864
|
+
/* @__PURE__ */ jsx5("circle", { cx: "12", cy: "12", r: "10" }),
|
|
865
|
+
/* @__PURE__ */ jsx5("path", { d: "M8 12h8" }),
|
|
866
|
+
/* @__PURE__ */ jsx5("path", { d: "M12 8v8" })
|
|
867
|
+
]
|
|
868
|
+
}
|
|
869
|
+
);
|
|
870
|
+
}
|
|
871
|
+
function LogoutIcon() {
|
|
872
|
+
return /* @__PURE__ */ jsxs4(
|
|
873
|
+
"svg",
|
|
874
|
+
{
|
|
875
|
+
className: "quota-user-menu__item-icon",
|
|
876
|
+
width: "18",
|
|
877
|
+
height: "18",
|
|
878
|
+
viewBox: "0 0 24 24",
|
|
879
|
+
fill: "none",
|
|
880
|
+
stroke: "currentColor",
|
|
881
|
+
strokeWidth: "2",
|
|
882
|
+
strokeLinecap: "round",
|
|
883
|
+
strokeLinejoin: "round",
|
|
884
|
+
"aria-hidden": "true",
|
|
885
|
+
children: [
|
|
886
|
+
/* @__PURE__ */ jsx5("path", { d: "M9 21H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h4" }),
|
|
887
|
+
/* @__PURE__ */ jsx5("polyline", { points: "16 17 21 12 16 7" }),
|
|
888
|
+
/* @__PURE__ */ jsx5("line", { x1: "21", y1: "12", x2: "9", y2: "12" })
|
|
889
|
+
]
|
|
890
|
+
}
|
|
891
|
+
);
|
|
892
|
+
}
|
|
893
|
+
|
|
894
|
+
// src/webhooks.ts
|
|
895
|
+
import crypto from "crypto";
|
|
896
|
+
function verifyWebhookSignature({
|
|
897
|
+
payload,
|
|
898
|
+
signature,
|
|
899
|
+
secret
|
|
900
|
+
}) {
|
|
901
|
+
const payloadString = typeof payload === "string" ? payload : payload.toString("utf8");
|
|
902
|
+
const hmac = crypto.createHmac("sha256", secret);
|
|
903
|
+
hmac.update(payloadString);
|
|
904
|
+
const expectedSignature = hmac.digest("hex");
|
|
905
|
+
try {
|
|
906
|
+
return crypto.timingSafeEqual(
|
|
907
|
+
Buffer.from(signature, "hex"),
|
|
908
|
+
Buffer.from(expectedSignature, "hex")
|
|
909
|
+
);
|
|
910
|
+
} catch {
|
|
911
|
+
return false;
|
|
912
|
+
}
|
|
913
|
+
}
|
|
914
|
+
async function parseWebhook(req, secret) {
|
|
915
|
+
const signature = req.headers.get("x-quota-signature");
|
|
916
|
+
if (!signature) {
|
|
917
|
+
throw new Error("Missing X-Quota-Signature header");
|
|
918
|
+
}
|
|
919
|
+
const payload = await req.text();
|
|
920
|
+
if (!verifyWebhookSignature({ payload, signature, secret })) {
|
|
921
|
+
throw new Error("Invalid webhook signature");
|
|
922
|
+
}
|
|
923
|
+
return JSON.parse(payload);
|
|
924
|
+
}
|
|
925
|
+
function createWebhookHandler(secret, handlers) {
|
|
926
|
+
return async (req) => {
|
|
927
|
+
try {
|
|
928
|
+
const event = await parseWebhook(req, secret);
|
|
929
|
+
const handler = handlers[event.type];
|
|
930
|
+
if (handler) {
|
|
931
|
+
await handler(event);
|
|
932
|
+
}
|
|
933
|
+
return new Response(JSON.stringify({ received: true }), {
|
|
934
|
+
status: 200,
|
|
935
|
+
headers: { "Content-Type": "application/json" }
|
|
936
|
+
});
|
|
937
|
+
} catch (error) {
|
|
938
|
+
console.error("Webhook error:", error);
|
|
939
|
+
return new Response(
|
|
940
|
+
JSON.stringify({
|
|
941
|
+
error: error instanceof Error ? error.message : "Unknown error"
|
|
942
|
+
}),
|
|
943
|
+
{ status: 400, headers: { "Content-Type": "application/json" } }
|
|
944
|
+
);
|
|
945
|
+
}
|
|
946
|
+
};
|
|
947
|
+
}
|
|
948
|
+
export {
|
|
949
|
+
QuotaBalance,
|
|
950
|
+
QuotaBuyCredits,
|
|
951
|
+
QuotaConnectButton,
|
|
952
|
+
QuotaContext,
|
|
953
|
+
QuotaError,
|
|
954
|
+
QuotaInsufficientCreditsError,
|
|
955
|
+
QuotaNotConnectedError,
|
|
956
|
+
QuotaProvider,
|
|
957
|
+
QuotaRateLimitError,
|
|
958
|
+
QuotaTokenExpiredError,
|
|
959
|
+
QuotaUserMenu,
|
|
960
|
+
createQuotaMiddleware,
|
|
961
|
+
createWebhookHandler,
|
|
962
|
+
parseWebhook,
|
|
963
|
+
useQuota,
|
|
964
|
+
useQuotaAuth,
|
|
965
|
+
useQuotaBalance,
|
|
966
|
+
useQuotaUser,
|
|
967
|
+
verifyWebhookSignature
|
|
968
|
+
};
|