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