@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.
@@ -0,0 +1,916 @@
1
+ import {
2
+ QuotaError,
3
+ QuotaInsufficientCreditsError,
4
+ QuotaNotConnectedError,
5
+ QuotaRateLimitError,
6
+ QuotaTokenExpiredError,
7
+ errorFromResponse
8
+ } from "./chunk-BMI3VFWV.mjs";
9
+
10
+ // src/route-handlers.ts
11
+ import { cookies } from "next/headers";
12
+
13
+ // src/token-refresh.ts
14
+ var FETCH_TIMEOUT_MS = 1e4;
15
+ async function refreshAccessTokenWithCredentials(opts) {
16
+ try {
17
+ const response = await fetch(`${opts.baseUrl}/oauth/token`, {
18
+ method: "POST",
19
+ headers: { "Content-Type": "application/json" },
20
+ body: JSON.stringify({
21
+ grant_type: "refresh_token",
22
+ refresh_token: opts.refreshToken,
23
+ client_id: opts.clientId,
24
+ client_secret: opts.clientSecret
25
+ }),
26
+ signal: AbortSignal.timeout(FETCH_TIMEOUT_MS)
27
+ });
28
+ if (!response.ok) {
29
+ return null;
30
+ }
31
+ return await response.json();
32
+ } catch (error) {
33
+ console.error("Failed to refresh Quota access token:", error);
34
+ return null;
35
+ }
36
+ }
37
+ async function refreshAndPersistTokens(opts) {
38
+ const tokenData = await refreshAccessTokenWithCredentials({
39
+ refreshToken: opts.refreshToken,
40
+ clientId: opts.clientId,
41
+ clientSecret: opts.clientSecret,
42
+ baseUrl: opts.baseUrl
43
+ });
44
+ if (!tokenData) {
45
+ return null;
46
+ }
47
+ if (opts.tokenStorage && opts.request) {
48
+ await opts.tokenStorage.setTokens(
49
+ {
50
+ accessToken: tokenData.access_token,
51
+ refreshToken: tokenData.refresh_token,
52
+ expiresIn: tokenData.expires_in
53
+ },
54
+ opts.request
55
+ );
56
+ } else if (opts.writeCookies) {
57
+ await opts.writeCookies(tokenData);
58
+ }
59
+ return tokenData.access_token;
60
+ }
61
+
62
+ // src/route-handlers.ts
63
+ var DEFAULT_BASE_URL = "https://api.usequota.app";
64
+ var DEFAULT_COOKIE_PREFIX = "quota";
65
+ var DEFAULT_COOKIE_MAX_AGE = 60 * 60 * 24 * 7;
66
+ function createQuotaRouteHandlers(config) {
67
+ const baseUrl = config.baseUrl ?? DEFAULT_BASE_URL;
68
+ const cookiePrefix = config.cookiePrefix ?? DEFAULT_COOKIE_PREFIX;
69
+ const cookieMaxAge = config.cookieMaxAge ?? DEFAULT_COOKIE_MAX_AGE;
70
+ const storageMode = config.storageMode ?? "client";
71
+ const callbackPath = config.callbackPath ?? "/api/quota/callback";
72
+ const successRedirect = config.successRedirect ?? "/";
73
+ const errorRedirect = config.errorRedirect ?? "/";
74
+ const tokenStorage = config.tokenStorage;
75
+ if (storageMode === "hosted" && !config.getExternalUserId && !tokenStorage?.getExternalUserId) {
76
+ throw new Error(
77
+ "getExternalUserId (or tokenStorage.getExternalUserId) is required when storageMode is 'hosted'"
78
+ );
79
+ }
80
+ function verifyCsrfOrigin(request) {
81
+ const origin = request.headers.get("origin");
82
+ const referer = request.headers.get("referer");
83
+ const source = origin ?? (referer ? new URL(referer).origin : null);
84
+ if (!source) {
85
+ return false;
86
+ }
87
+ try {
88
+ const requestOrigin = new URL(request.url).origin;
89
+ return source === requestOrigin;
90
+ } catch {
91
+ return false;
92
+ }
93
+ }
94
+ async function authorize(request) {
95
+ try {
96
+ const state = crypto.randomUUID();
97
+ const cookieStore = await cookies();
98
+ cookieStore.set("quota_oauth_state", state, {
99
+ httpOnly: true,
100
+ secure: true,
101
+ sameSite: "lax",
102
+ maxAge: 60 * 10,
103
+ // 10 minutes
104
+ path: "/"
105
+ });
106
+ const url = new URL(request.url);
107
+ const redirectUri = new URL(callbackPath, url.origin).toString();
108
+ const authUrl = new URL("/oauth/authorize", baseUrl);
109
+ authUrl.searchParams.set("response_type", "code");
110
+ authUrl.searchParams.set("client_id", config.clientId);
111
+ authUrl.searchParams.set("redirect_uri", redirectUri);
112
+ authUrl.searchParams.set("state", state);
113
+ authUrl.searchParams.set("scope", "credits:use");
114
+ return Response.redirect(authUrl.toString(), 302);
115
+ } catch (error) {
116
+ console.error("Quota authorize error:", error);
117
+ const redirectUrl = new URL(errorRedirect, new URL(request.url).origin);
118
+ redirectUrl.searchParams.set("quota_error", "auth_failed");
119
+ return Response.redirect(redirectUrl.toString(), 302);
120
+ }
121
+ }
122
+ async function callback(request) {
123
+ const url = new URL(request.url);
124
+ const code = url.searchParams.get("code");
125
+ const state = url.searchParams.get("state");
126
+ const error = url.searchParams.get("error");
127
+ const errorDescription = url.searchParams.get("error_description");
128
+ if (error) {
129
+ const redirectUrl = new URL(errorRedirect, url.origin);
130
+ redirectUrl.searchParams.set("quota_error", error);
131
+ if (errorDescription) {
132
+ redirectUrl.searchParams.set(
133
+ "quota_error_description",
134
+ errorDescription
135
+ );
136
+ }
137
+ return Response.redirect(redirectUrl.toString(), 302);
138
+ }
139
+ if (!code || !state) {
140
+ const redirectUrl = new URL(errorRedirect, url.origin);
141
+ redirectUrl.searchParams.set("quota_error", "invalid_callback");
142
+ return Response.redirect(redirectUrl.toString(), 302);
143
+ }
144
+ const cookieStore = await cookies();
145
+ const storedState = cookieStore.get("quota_oauth_state")?.value;
146
+ if (!storedState || storedState !== state) {
147
+ const redirectUrl = new URL(errorRedirect, url.origin);
148
+ redirectUrl.searchParams.set("quota_error", "state_mismatch");
149
+ return Response.redirect(redirectUrl.toString(), 302);
150
+ }
151
+ cookieStore.delete("quota_oauth_state");
152
+ try {
153
+ const redirectUri = new URL(callbackPath, url.origin).toString();
154
+ const tokenBody = {
155
+ grant_type: "authorization_code",
156
+ code,
157
+ client_id: config.clientId,
158
+ client_secret: config.clientSecret,
159
+ redirect_uri: redirectUri
160
+ };
161
+ if (storageMode === "hosted") {
162
+ const externalUserId = await resolveExternalUserId(request);
163
+ if (!externalUserId) {
164
+ throw new Error(
165
+ "getExternalUserId is required for hosted storage mode"
166
+ );
167
+ }
168
+ tokenBody.storage_mode = "hosted";
169
+ tokenBody.external_user_id = externalUserId;
170
+ }
171
+ const tokenResponse = await fetch(`${baseUrl}/oauth/token`, {
172
+ method: "POST",
173
+ headers: { "Content-Type": "application/json" },
174
+ body: JSON.stringify(tokenBody),
175
+ signal: AbortSignal.timeout(FETCH_TIMEOUT_MS)
176
+ });
177
+ if (!tokenResponse.ok) {
178
+ const redirectUrl2 = new URL(errorRedirect, url.origin);
179
+ redirectUrl2.searchParams.set("quota_error", "token_exchange_failed");
180
+ return Response.redirect(redirectUrl2.toString(), 302);
181
+ }
182
+ const tokenData = await tokenResponse.json();
183
+ if ("access_token" in tokenData) {
184
+ if (tokenStorage) {
185
+ await tokenStorage.setTokens(
186
+ {
187
+ accessToken: tokenData.access_token,
188
+ refreshToken: tokenData.refresh_token,
189
+ expiresIn: tokenData.expires_in
190
+ },
191
+ request
192
+ );
193
+ } else {
194
+ cookieStore.set(
195
+ `${cookiePrefix}_access_token`,
196
+ tokenData.access_token,
197
+ {
198
+ httpOnly: true,
199
+ secure: true,
200
+ sameSite: "lax",
201
+ path: "/",
202
+ maxAge: cookieMaxAge
203
+ }
204
+ );
205
+ cookieStore.set(
206
+ `${cookiePrefix}_refresh_token`,
207
+ tokenData.refresh_token,
208
+ {
209
+ httpOnly: true,
210
+ secure: true,
211
+ sameSite: "lax",
212
+ path: "/",
213
+ maxAge: cookieMaxAge
214
+ }
215
+ );
216
+ }
217
+ if (config.callbacks?.onConnect) {
218
+ const userResponse = await fetch(`${baseUrl}/v1/me`, {
219
+ headers: { Authorization: `Bearer ${tokenData.access_token}` },
220
+ signal: AbortSignal.timeout(FETCH_TIMEOUT_MS)
221
+ });
222
+ if (userResponse.ok) {
223
+ const user = await userResponse.json();
224
+ await config.callbacks.onConnect({
225
+ user,
226
+ accessToken: tokenData.access_token,
227
+ refreshToken: tokenData.refresh_token,
228
+ expiresIn: tokenData.expires_in,
229
+ request
230
+ });
231
+ }
232
+ }
233
+ }
234
+ if ("storage_mode" in tokenData && tokenData.storage_mode === "hosted") {
235
+ cookieStore.set(
236
+ `${cookiePrefix}_external_user_id`,
237
+ tokenData.external_user_id,
238
+ {
239
+ httpOnly: true,
240
+ secure: true,
241
+ sameSite: "lax",
242
+ path: "/",
243
+ maxAge: cookieMaxAge
244
+ }
245
+ );
246
+ if (config.callbacks?.onConnect) {
247
+ await config.callbacks.onConnect({
248
+ user: tokenData.user,
249
+ accessToken: "",
250
+ refreshToken: "",
251
+ expiresIn: 0,
252
+ request
253
+ });
254
+ }
255
+ }
256
+ const redirectUrl = new URL(successRedirect, url.origin);
257
+ redirectUrl.searchParams.set("quota_connected", "true");
258
+ return Response.redirect(redirectUrl.toString(), 302);
259
+ } catch (error2) {
260
+ console.error("Quota callback error:", error2);
261
+ const redirectUrl = new URL(errorRedirect, url.origin);
262
+ redirectUrl.searchParams.set("quota_error", "callback_failed");
263
+ return Response.redirect(redirectUrl.toString(), 302);
264
+ }
265
+ }
266
+ async function status(request) {
267
+ try {
268
+ const accessToken = await getAccessToken(request);
269
+ if (!accessToken) {
270
+ return Response.json({ connected: false });
271
+ }
272
+ const response = await fetch(`${baseUrl}/v1/me`, {
273
+ headers: { Authorization: `Bearer ${accessToken}` },
274
+ signal: AbortSignal.timeout(FETCH_TIMEOUT_MS)
275
+ });
276
+ if (!response.ok) {
277
+ if (response.status === 401) {
278
+ const refreshed = await refreshToken(request);
279
+ if (refreshed) {
280
+ const retryResponse = await fetch(`${baseUrl}/v1/me`, {
281
+ headers: { Authorization: `Bearer ${refreshed}` },
282
+ signal: AbortSignal.timeout(FETCH_TIMEOUT_MS)
283
+ });
284
+ if (retryResponse.ok) {
285
+ const user2 = await retryResponse.json();
286
+ return Response.json({
287
+ connected: true,
288
+ email: user2.email,
289
+ balance: user2.balance
290
+ });
291
+ }
292
+ }
293
+ }
294
+ if (response.status >= 500) {
295
+ return Response.json(
296
+ { error: { code: "upstream_error", message: "Quota API error" } },
297
+ { status: 502 }
298
+ );
299
+ }
300
+ return Response.json({ connected: false });
301
+ }
302
+ const user = await response.json();
303
+ return Response.json({
304
+ connected: true,
305
+ email: user.email,
306
+ balance: user.balance
307
+ });
308
+ } catch (error) {
309
+ console.error("Quota status error:", error);
310
+ return Response.json({ connected: false });
311
+ }
312
+ }
313
+ async function packages(_request) {
314
+ try {
315
+ const response = await fetch(`${baseUrl}/v1/packages`, {
316
+ signal: AbortSignal.timeout(FETCH_TIMEOUT_MS)
317
+ });
318
+ if (!response.ok) {
319
+ return Response.json(
320
+ {
321
+ error: {
322
+ code: "upstream_error",
323
+ message: "Failed to fetch packages"
324
+ }
325
+ },
326
+ { status: 502 }
327
+ );
328
+ }
329
+ const data = await response.json();
330
+ const packageList = Array.isArray(data) ? data : data.packages;
331
+ return Response.json({ packages: packageList });
332
+ } catch (error) {
333
+ console.error("Quota packages error:", error);
334
+ return Response.json(
335
+ {
336
+ error: {
337
+ code: "internal_error",
338
+ message: "Failed to fetch packages"
339
+ }
340
+ },
341
+ { status: 500 }
342
+ );
343
+ }
344
+ }
345
+ async function checkout(request) {
346
+ if (!verifyCsrfOrigin(request)) {
347
+ return Response.json(
348
+ { error: { code: "csrf_rejected", message: "Origin mismatch" } },
349
+ { status: 403 }
350
+ );
351
+ }
352
+ try {
353
+ const accessToken = await getAccessToken(request);
354
+ if (!accessToken) {
355
+ return Response.json(
356
+ { error: { code: "not_connected", message: "Quota not connected" } },
357
+ { status: 401 }
358
+ );
359
+ }
360
+ const body = await request.json();
361
+ const packageId = body.packageId ?? body.package_id;
362
+ if (!packageId) {
363
+ return Response.json(
364
+ {
365
+ error: {
366
+ code: "bad_request",
367
+ message: "packageId is required"
368
+ }
369
+ },
370
+ { status: 400 }
371
+ );
372
+ }
373
+ const url = new URL(request.url);
374
+ const successUrl = `${url.origin}${successRedirect}?quota_purchase=success`;
375
+ const cancelUrl = `${url.origin}${errorRedirect}?quota_purchase=cancelled`;
376
+ const response = await fetch(`${baseUrl}/api/payments/checkout`, {
377
+ method: "POST",
378
+ headers: {
379
+ Authorization: `Bearer ${accessToken}`,
380
+ "Content-Type": "application/json"
381
+ },
382
+ body: JSON.stringify({
383
+ package_id: packageId,
384
+ success_url: successUrl,
385
+ cancel_url: cancelUrl
386
+ }),
387
+ signal: AbortSignal.timeout(FETCH_TIMEOUT_MS)
388
+ });
389
+ if (!response.ok) {
390
+ return Response.json(
391
+ {
392
+ error: {
393
+ code: "checkout_failed",
394
+ message: "Failed to create checkout session"
395
+ }
396
+ },
397
+ { status: response.status }
398
+ );
399
+ }
400
+ const data = await response.json();
401
+ return Response.json({
402
+ checkout_url: data.url ?? data.checkout_url
403
+ });
404
+ } catch (error) {
405
+ console.error("Quota checkout error:", error);
406
+ return Response.json(
407
+ {
408
+ error: {
409
+ code: "internal_error",
410
+ message: "Failed to create checkout"
411
+ }
412
+ },
413
+ { status: 500 }
414
+ );
415
+ }
416
+ }
417
+ async function disconnect(request) {
418
+ if (!verifyCsrfOrigin(request)) {
419
+ return Response.json(
420
+ { error: { code: "csrf_rejected", message: "Origin mismatch" } },
421
+ { status: 403 }
422
+ );
423
+ }
424
+ try {
425
+ if (tokenStorage) {
426
+ await tokenStorage.deleteTokens(request);
427
+ } else {
428
+ const cookieStore = await cookies();
429
+ cookieStore.delete(`${cookiePrefix}_access_token`);
430
+ cookieStore.delete(`${cookiePrefix}_refresh_token`);
431
+ cookieStore.delete(`${cookiePrefix}_external_user_id`);
432
+ }
433
+ if (config.callbacks?.onDisconnect) {
434
+ await config.callbacks.onDisconnect(request);
435
+ }
436
+ return Response.json({ success: true });
437
+ } catch (error) {
438
+ console.error("Quota disconnect error:", error);
439
+ return Response.json(
440
+ { error: { code: "internal_error", message: "Failed to disconnect" } },
441
+ { status: 500 }
442
+ );
443
+ }
444
+ }
445
+ async function resolveExternalUserId(request) {
446
+ if (tokenStorage?.getExternalUserId) {
447
+ return tokenStorage.getExternalUserId(request);
448
+ }
449
+ if (config.getExternalUserId) {
450
+ return config.getExternalUserId(request);
451
+ }
452
+ return null;
453
+ }
454
+ async function getAccessToken(request) {
455
+ if (storageMode === "hosted") {
456
+ return null;
457
+ }
458
+ if (tokenStorage) {
459
+ const tokens = await tokenStorage.getTokens(request);
460
+ return tokens?.accessToken ?? null;
461
+ }
462
+ const cookieStore = await cookies();
463
+ return cookieStore.get(`${cookiePrefix}_access_token`)?.value ?? null;
464
+ }
465
+ async function getRefreshToken(request) {
466
+ if (tokenStorage) {
467
+ const tokens = await tokenStorage.getTokens(request);
468
+ return tokens?.refreshToken ?? null;
469
+ }
470
+ const cookieStore = await cookies();
471
+ return cookieStore.get(`${cookiePrefix}_refresh_token`)?.value ?? null;
472
+ }
473
+ async function refreshToken(request) {
474
+ const refreshTokenValue = await getRefreshToken(request);
475
+ if (!refreshTokenValue) {
476
+ return null;
477
+ }
478
+ return refreshAndPersistTokens({
479
+ refreshToken: refreshTokenValue,
480
+ clientId: config.clientId,
481
+ clientSecret: config.clientSecret,
482
+ baseUrl,
483
+ tokenStorage,
484
+ request,
485
+ writeCookies: async (tokenData) => {
486
+ const cookieStore = await cookies();
487
+ cookieStore.set(
488
+ `${cookiePrefix}_access_token`,
489
+ tokenData.access_token,
490
+ {
491
+ httpOnly: true,
492
+ secure: true,
493
+ sameSite: "lax",
494
+ path: "/",
495
+ maxAge: cookieMaxAge
496
+ }
497
+ );
498
+ cookieStore.set(
499
+ `${cookiePrefix}_refresh_token`,
500
+ tokenData.refresh_token,
501
+ {
502
+ httpOnly: true,
503
+ secure: true,
504
+ sameSite: "lax",
505
+ path: "/",
506
+ maxAge: cookieMaxAge
507
+ }
508
+ );
509
+ }
510
+ });
511
+ }
512
+ return {
513
+ authorize,
514
+ callback,
515
+ status,
516
+ packages,
517
+ checkout,
518
+ disconnect
519
+ };
520
+ }
521
+
522
+ // src/with-quota-auth.ts
523
+ import { cookies as cookies2 } from "next/headers";
524
+ var DEFAULT_BASE_URL2 = "https://api.usequota.app";
525
+ var DEFAULT_COOKIE_PREFIX2 = "quota";
526
+ var DEFAULT_COOKIE_MAX_AGE2 = 60 * 60 * 24 * 7;
527
+ function withQuotaAuth(config, handler) {
528
+ const baseUrl = config.baseUrl ?? DEFAULT_BASE_URL2;
529
+ const cookiePrefix = config.cookiePrefix ?? DEFAULT_COOKIE_PREFIX2;
530
+ const cookieMaxAge = config.cookieMaxAge ?? DEFAULT_COOKIE_MAX_AGE2;
531
+ const storageMode = config.storageMode ?? "client";
532
+ const tokenStorage = config.tokenStorage;
533
+ if (storageMode === "hosted" && !config.getExternalUserId && !tokenStorage?.getExternalUserId) {
534
+ throw new Error(
535
+ "getExternalUserId is required when storageMode is 'hosted'"
536
+ );
537
+ }
538
+ return async (request) => {
539
+ try {
540
+ let accessToken;
541
+ let user;
542
+ if (storageMode === "hosted") {
543
+ let externalUserId = null;
544
+ if (tokenStorage?.getExternalUserId) {
545
+ externalUserId = await tokenStorage.getExternalUserId(request);
546
+ } else if (config.getExternalUserId) {
547
+ externalUserId = await config.getExternalUserId(request);
548
+ }
549
+ if (!externalUserId) {
550
+ throw new QuotaError(
551
+ "getExternalUserId is required for hosted storage mode",
552
+ "configuration_error",
553
+ 500
554
+ );
555
+ }
556
+ const response = await fetch(`${baseUrl}/v1/me`, {
557
+ headers: {
558
+ "X-Quota-Client-Id": config.clientId,
559
+ "X-Quota-Client-Secret": config.clientSecret,
560
+ "X-Quota-User": externalUserId
561
+ },
562
+ signal: AbortSignal.timeout(FETCH_TIMEOUT_MS)
563
+ });
564
+ if (!response.ok) {
565
+ if (response.status === 401 || response.status === 404) {
566
+ throw new QuotaNotConnectedError();
567
+ }
568
+ throw new QuotaError(
569
+ `Failed to fetch user: ${response.statusText}`,
570
+ "api_error",
571
+ response.status
572
+ );
573
+ }
574
+ user = await response.json();
575
+ accessToken = "";
576
+ } else {
577
+ let token;
578
+ let refreshTokenValue;
579
+ if (tokenStorage) {
580
+ const tokens = await tokenStorage.getTokens(request);
581
+ token = tokens?.accessToken ?? null;
582
+ refreshTokenValue = tokens?.refreshToken ?? null;
583
+ } else {
584
+ const cookieStore = await cookies2();
585
+ token = cookieStore.get(`${cookiePrefix}_access_token`)?.value ?? null;
586
+ refreshTokenValue = cookieStore.get(`${cookiePrefix}_refresh_token`)?.value ?? null;
587
+ }
588
+ if (!token) {
589
+ throw new QuotaNotConnectedError();
590
+ }
591
+ let response = await fetch(`${baseUrl}/v1/me`, {
592
+ headers: { Authorization: `Bearer ${token}` },
593
+ signal: AbortSignal.timeout(FETCH_TIMEOUT_MS)
594
+ });
595
+ if (response.status === 401) {
596
+ if (!refreshTokenValue) {
597
+ throw new QuotaTokenExpiredError();
598
+ }
599
+ const newAccessToken = await refreshAndPersistTokens({
600
+ refreshToken: refreshTokenValue,
601
+ clientId: config.clientId,
602
+ clientSecret: config.clientSecret,
603
+ baseUrl,
604
+ tokenStorage,
605
+ request,
606
+ writeCookies: async (tokenData) => {
607
+ const cookieStore = await cookies2();
608
+ cookieStore.set(
609
+ `${cookiePrefix}_access_token`,
610
+ tokenData.access_token,
611
+ {
612
+ httpOnly: true,
613
+ secure: true,
614
+ sameSite: "lax",
615
+ path: "/",
616
+ maxAge: cookieMaxAge
617
+ }
618
+ );
619
+ cookieStore.set(
620
+ `${cookiePrefix}_refresh_token`,
621
+ tokenData.refresh_token,
622
+ {
623
+ httpOnly: true,
624
+ secure: true,
625
+ sameSite: "lax",
626
+ path: "/",
627
+ maxAge: cookieMaxAge
628
+ }
629
+ );
630
+ }
631
+ });
632
+ if (!newAccessToken) {
633
+ throw new QuotaTokenExpiredError();
634
+ }
635
+ response = await fetch(`${baseUrl}/v1/me`, {
636
+ headers: { Authorization: `Bearer ${newAccessToken}` },
637
+ signal: AbortSignal.timeout(FETCH_TIMEOUT_MS)
638
+ });
639
+ if (!response.ok) {
640
+ throw new QuotaTokenExpiredError();
641
+ }
642
+ accessToken = newAccessToken;
643
+ } else if (!response.ok) {
644
+ throw new QuotaError(
645
+ `Failed to fetch user: ${response.statusText}`,
646
+ "api_error",
647
+ response.status
648
+ );
649
+ } else {
650
+ accessToken = token;
651
+ }
652
+ user = await response.json();
653
+ }
654
+ return await handler(request, { user, accessToken });
655
+ } catch (error) {
656
+ if (error instanceof QuotaNotConnectedError) {
657
+ return Response.json(
658
+ { error: { code: error.code, message: error.message } },
659
+ { status: 401 }
660
+ );
661
+ }
662
+ if (error instanceof QuotaTokenExpiredError) {
663
+ return Response.json(
664
+ {
665
+ error: {
666
+ code: error.code,
667
+ message: error.message,
668
+ hint: error.hint
669
+ }
670
+ },
671
+ { status: 401 }
672
+ );
673
+ }
674
+ if (error instanceof QuotaInsufficientCreditsError) {
675
+ return Response.json(
676
+ {
677
+ error: {
678
+ code: error.code,
679
+ message: error.message,
680
+ hint: error.hint
681
+ }
682
+ },
683
+ { status: 402 }
684
+ );
685
+ }
686
+ if (error instanceof QuotaRateLimitError) {
687
+ return new Response(
688
+ JSON.stringify({
689
+ error: {
690
+ code: error.code,
691
+ message: error.message
692
+ }
693
+ }),
694
+ {
695
+ status: 429,
696
+ headers: {
697
+ "Content-Type": "application/json",
698
+ "Retry-After": String(error.retryAfter)
699
+ }
700
+ }
701
+ );
702
+ }
703
+ if (error instanceof QuotaError) {
704
+ return Response.json(
705
+ { error: { code: error.code, message: error.message } },
706
+ { status: error.statusCode }
707
+ );
708
+ }
709
+ console.error("withQuotaAuth unexpected error:", error);
710
+ return Response.json(
711
+ { error: { code: "internal_error", message: "Internal server error" } },
712
+ { status: 500 }
713
+ );
714
+ }
715
+ };
716
+ }
717
+
718
+ // src/server.ts
719
+ import { cookies as cookies3 } from "next/headers";
720
+ var DEFAULT_BASE_URL3 = "https://api.usequota.app";
721
+ var DEFAULT_COOKIE_PREFIX3 = "quota";
722
+ var DEFAULT_STORAGE_MODE = "client";
723
+ var DEFAULT_COOKIE_MAX_AGE3 = 60 * 60 * 24 * 7;
724
+ async function getQuotaUser(config) {
725
+ const baseUrl = config.baseUrl ?? DEFAULT_BASE_URL3;
726
+ const cookiePrefix = config.cookiePrefix ?? DEFAULT_COOKIE_PREFIX3;
727
+ const cookieMaxAge = config.cookieMaxAge ?? DEFAULT_COOKIE_MAX_AGE3;
728
+ const storageMode = config.storageMode ?? DEFAULT_STORAGE_MODE;
729
+ try {
730
+ if (storageMode === "hosted") {
731
+ if (!config.getExternalUserId) {
732
+ throw new Error(
733
+ "getExternalUserId is required for hosted storage mode"
734
+ );
735
+ }
736
+ const externalUserId = await config.getExternalUserId();
737
+ const response2 = await fetch(`${baseUrl}/v1/me`, {
738
+ headers: {
739
+ "X-Quota-Client-Id": config.clientId,
740
+ "X-Quota-Client-Secret": config.clientSecret,
741
+ "X-Quota-User": externalUserId
742
+ },
743
+ signal: AbortSignal.timeout(FETCH_TIMEOUT_MS)
744
+ });
745
+ if (!response2.ok) {
746
+ if (response2.status === 401 || response2.status === 404) {
747
+ return null;
748
+ }
749
+ throw new Error(`Failed to fetch user: ${response2.statusText}`);
750
+ }
751
+ return await response2.json();
752
+ }
753
+ const cookieStore = await cookies3();
754
+ const accessToken = cookieStore.get(`${cookiePrefix}_access_token`)?.value;
755
+ if (!accessToken) {
756
+ return null;
757
+ }
758
+ const response = await fetch(`${baseUrl}/v1/me`, {
759
+ headers: {
760
+ Authorization: `Bearer ${accessToken}`
761
+ },
762
+ signal: AbortSignal.timeout(FETCH_TIMEOUT_MS)
763
+ });
764
+ if (!response.ok) {
765
+ if (response.status === 401) {
766
+ const refreshTokenValue = cookieStore.get(
767
+ `${cookiePrefix}_refresh_token`
768
+ )?.value;
769
+ if (refreshTokenValue) {
770
+ const refreshed = await refreshAccessTokenWithCredentials({
771
+ refreshToken: refreshTokenValue,
772
+ clientId: config.clientId,
773
+ clientSecret: config.clientSecret,
774
+ baseUrl
775
+ });
776
+ if (refreshed) {
777
+ cookieStore.set(
778
+ `${cookiePrefix}_access_token`,
779
+ refreshed.access_token,
780
+ {
781
+ httpOnly: true,
782
+ secure: true,
783
+ sameSite: "lax",
784
+ path: "/",
785
+ maxAge: cookieMaxAge
786
+ }
787
+ );
788
+ cookieStore.set(
789
+ `${cookiePrefix}_refresh_token`,
790
+ refreshed.refresh_token,
791
+ {
792
+ httpOnly: true,
793
+ secure: true,
794
+ sameSite: "lax",
795
+ path: "/",
796
+ maxAge: cookieMaxAge
797
+ }
798
+ );
799
+ const retryResponse = await fetch(`${baseUrl}/v1/me`, {
800
+ headers: {
801
+ Authorization: `Bearer ${refreshed.access_token}`
802
+ },
803
+ signal: AbortSignal.timeout(FETCH_TIMEOUT_MS)
804
+ });
805
+ if (retryResponse.ok) {
806
+ return await retryResponse.json();
807
+ }
808
+ }
809
+ }
810
+ }
811
+ return null;
812
+ }
813
+ return await response.json();
814
+ } catch (error) {
815
+ console.error("Failed to get Quota user:", error);
816
+ return null;
817
+ }
818
+ }
819
+ async function requireQuotaAuth(config) {
820
+ const user = await getQuotaUser(config);
821
+ if (!user) {
822
+ const { redirect } = await import("next/navigation");
823
+ redirect("/");
824
+ throw new QuotaNotConnectedError("Redirecting to /");
825
+ }
826
+ return user;
827
+ }
828
+ async function getQuotaPackages(config) {
829
+ const baseUrl = config?.baseUrl ?? DEFAULT_BASE_URL3;
830
+ try {
831
+ const response = await fetch(`${baseUrl}/v1/packages`, {
832
+ signal: AbortSignal.timeout(FETCH_TIMEOUT_MS)
833
+ });
834
+ if (!response.ok) {
835
+ throw new Error(`Failed to fetch packages: ${response.statusText}`);
836
+ }
837
+ return await response.json();
838
+ } catch (error) {
839
+ console.error("Failed to get Quota packages:", error);
840
+ return [];
841
+ }
842
+ }
843
+ async function createQuotaCheckout(config) {
844
+ const baseUrl = config.baseUrl ?? DEFAULT_BASE_URL3;
845
+ const cookiePrefix = config.cookiePrefix ?? DEFAULT_COOKIE_PREFIX3;
846
+ const storageMode = config.storageMode ?? DEFAULT_STORAGE_MODE;
847
+ try {
848
+ const headers = {
849
+ "Content-Type": "application/json"
850
+ };
851
+ if (storageMode === "hosted") {
852
+ if (!config.getExternalUserId) {
853
+ throw new Error(
854
+ "getExternalUserId is required for hosted storage mode"
855
+ );
856
+ }
857
+ const externalUserId = await config.getExternalUserId();
858
+ headers["X-Quota-Client-Id"] = config.clientId;
859
+ headers["X-Quota-Client-Secret"] = config.clientSecret;
860
+ headers["X-Quota-User"] = externalUserId;
861
+ } else {
862
+ const cookieStore = await cookies3();
863
+ const accessToken = cookieStore.get(
864
+ `${cookiePrefix}_access_token`
865
+ )?.value;
866
+ if (!accessToken) {
867
+ throw new QuotaNotConnectedError("Not authenticated");
868
+ }
869
+ headers["Authorization"] = `Bearer ${accessToken}`;
870
+ }
871
+ const response = await fetch(`${baseUrl}/api/payments/checkout`, {
872
+ method: "POST",
873
+ headers,
874
+ body: JSON.stringify({
875
+ package_id: config.packageId,
876
+ success_url: config.successUrl,
877
+ cancel_url: config.cancelUrl
878
+ }),
879
+ signal: AbortSignal.timeout(FETCH_TIMEOUT_MS)
880
+ });
881
+ if (!response.ok) {
882
+ throw new Error(`Failed to create checkout: ${response.statusText}`);
883
+ }
884
+ const data = await response.json();
885
+ return data.url;
886
+ } catch (error) {
887
+ console.error("Failed to create Quota checkout:", error);
888
+ throw error;
889
+ }
890
+ }
891
+ async function clearQuotaAuth(config) {
892
+ const cookiePrefix = config?.cookiePrefix ?? DEFAULT_COOKIE_PREFIX3;
893
+ try {
894
+ const cookieStore = await cookies3();
895
+ cookieStore.delete(`${cookiePrefix}_access_token`);
896
+ cookieStore.delete(`${cookiePrefix}_refresh_token`);
897
+ cookieStore.delete(`${cookiePrefix}_external_user_id`);
898
+ } catch (error) {
899
+ console.error("Failed to clear Quota auth:", error);
900
+ }
901
+ }
902
+ export {
903
+ QuotaError,
904
+ QuotaInsufficientCreditsError,
905
+ QuotaNotConnectedError,
906
+ QuotaRateLimitError,
907
+ QuotaTokenExpiredError,
908
+ clearQuotaAuth,
909
+ createQuotaCheckout,
910
+ createQuotaRouteHandlers,
911
+ errorFromResponse,
912
+ getQuotaPackages,
913
+ getQuotaUser,
914
+ requireQuotaAuth,
915
+ withQuotaAuth
916
+ };