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