@usequota/nextjs 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +462 -0
- package/dist/chunk-BMI3VFWV.mjs +120 -0
- package/dist/chunk-RSPDHXC2.mjs +119 -0
- package/dist/chunk-SJ3X4KTV.mjs +117 -0
- package/dist/chunk-ZF7WJBQC.mjs +114 -0
- package/dist/errors-CmNx3kSz.d.mts +109 -0
- package/dist/errors-CmNx3kSz.d.ts +109 -0
- package/dist/errors-DVurmYT7.d.mts +109 -0
- package/dist/errors-DVurmYT7.d.ts +109 -0
- package/dist/index.d.mts +585 -0
- package/dist/index.d.ts +585 -0
- package/dist/index.js +1079 -0
- package/dist/index.mjs +968 -0
- package/dist/server.d.mts +477 -0
- package/dist/server.d.ts +477 -0
- package/dist/server.js +1068 -0
- package/dist/server.mjs +916 -0
- package/dist/styles.css +439 -0
- package/package.json +69 -0
package/dist/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
|
+
});
|