@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/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
+ };