@solvapay/next 1.0.0-preview.9 → 1.0.1-preview.2
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/LICENSE.md +21 -0
- package/README.md +271 -20
- package/dist/chunk-F7TBIH6W.js +74 -0
- package/dist/index.cjs +303 -76
- package/dist/index.d.cts +284 -52
- package/dist/index.d.ts +284 -52
- package/dist/index.js +234 -71
- package/dist/middleware.cjs +111 -0
- package/dist/middleware.d.cts +178 -0
- package/dist/middleware.d.ts +178 -0
- package/dist/middleware.js +8 -0
- package/package.json +16 -10
package/dist/index.js
CHANGED
|
@@ -1,39 +1,46 @@
|
|
|
1
|
+
import {
|
|
2
|
+
createAuthMiddleware,
|
|
3
|
+
createSupabaseAuthMiddleware
|
|
4
|
+
} from "./chunk-F7TBIH6W.js";
|
|
5
|
+
|
|
1
6
|
// src/index.ts
|
|
2
|
-
import { NextResponse } from "next/server";
|
|
7
|
+
import { NextResponse as NextResponse7 } from "next/server";
|
|
3
8
|
import { createSolvaPay } from "@solvapay/server";
|
|
4
9
|
import { SolvaPayError } from "@solvapay/core";
|
|
10
|
+
|
|
11
|
+
// src/cache.ts
|
|
5
12
|
function createRequestDeduplicator(options = {}) {
|
|
6
|
-
const {
|
|
7
|
-
cacheTTL = 2e3,
|
|
8
|
-
maxCacheSize = 1e3,
|
|
9
|
-
cacheErrors = true
|
|
10
|
-
} = options;
|
|
13
|
+
const { cacheTTL = 2e3, maxCacheSize = 1e3, cacheErrors = true } = options;
|
|
11
14
|
const inFlightRequests = /* @__PURE__ */ new Map();
|
|
12
15
|
const resultCache = /* @__PURE__ */ new Map();
|
|
13
16
|
const cacheInvalidatedAt = /* @__PURE__ */ new Map();
|
|
14
|
-
let cleanupInterval = null;
|
|
15
17
|
if (cacheTTL > 0) {
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
18
|
+
setInterval(
|
|
19
|
+
() => {
|
|
20
|
+
const now = Date.now();
|
|
21
|
+
const entriesToDelete = [];
|
|
22
|
+
for (const [key, cached] of resultCache.entries()) {
|
|
23
|
+
if (now - cached.timestamp >= cacheTTL) {
|
|
24
|
+
entriesToDelete.push(key);
|
|
25
|
+
}
|
|
22
26
|
}
|
|
23
|
-
|
|
24
|
-
for (const key of entriesToDelete) {
|
|
25
|
-
resultCache.delete(key);
|
|
26
|
-
cacheInvalidatedAt.delete(key);
|
|
27
|
-
}
|
|
28
|
-
if (resultCache.size > maxCacheSize) {
|
|
29
|
-
const sortedEntries = Array.from(resultCache.entries()).sort((a, b) => a[1].timestamp - b[1].timestamp);
|
|
30
|
-
const toRemove = sortedEntries.slice(0, resultCache.size - maxCacheSize);
|
|
31
|
-
for (const [key] of toRemove) {
|
|
27
|
+
for (const key of entriesToDelete) {
|
|
32
28
|
resultCache.delete(key);
|
|
33
29
|
cacheInvalidatedAt.delete(key);
|
|
34
30
|
}
|
|
35
|
-
|
|
36
|
-
|
|
31
|
+
if (resultCache.size > maxCacheSize) {
|
|
32
|
+
const sortedEntries = Array.from(resultCache.entries()).sort(
|
|
33
|
+
(a, b) => a[1].timestamp - b[1].timestamp
|
|
34
|
+
);
|
|
35
|
+
const toRemove = sortedEntries.slice(0, resultCache.size - maxCacheSize);
|
|
36
|
+
for (const [key] of toRemove) {
|
|
37
|
+
resultCache.delete(key);
|
|
38
|
+
cacheInvalidatedAt.delete(key);
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
},
|
|
42
|
+
Math.min(cacheTTL, 1e3)
|
|
43
|
+
);
|
|
37
44
|
}
|
|
38
45
|
const deduplicate = async (key, fn) => {
|
|
39
46
|
if (cacheTTL > 0) {
|
|
@@ -103,10 +110,10 @@ function createRequestDeduplicator(options = {}) {
|
|
|
103
110
|
getStats
|
|
104
111
|
};
|
|
105
112
|
}
|
|
106
|
-
var
|
|
113
|
+
var sharedPurchaseDeduplicator = null;
|
|
107
114
|
function getSharedDeduplicator(options) {
|
|
108
|
-
if (!
|
|
109
|
-
|
|
115
|
+
if (!sharedPurchaseDeduplicator) {
|
|
116
|
+
sharedPurchaseDeduplicator = createRequestDeduplicator({
|
|
110
117
|
cacheTTL: 2e3,
|
|
111
118
|
// Cache results for 2 seconds
|
|
112
119
|
maxCacheSize: 1e3,
|
|
@@ -116,89 +123,245 @@ function getSharedDeduplicator(options) {
|
|
|
116
123
|
...options
|
|
117
124
|
});
|
|
118
125
|
}
|
|
119
|
-
return
|
|
126
|
+
return sharedPurchaseDeduplicator;
|
|
127
|
+
}
|
|
128
|
+
function clearPurchaseCache(userId) {
|
|
129
|
+
const deduplicator = getSharedDeduplicator();
|
|
130
|
+
deduplicator.clearCache(userId);
|
|
131
|
+
}
|
|
132
|
+
function clearAllPurchaseCache() {
|
|
133
|
+
const deduplicator = getSharedDeduplicator();
|
|
134
|
+
deduplicator.clearAllCache();
|
|
135
|
+
}
|
|
136
|
+
function getPurchaseCacheStats() {
|
|
137
|
+
const deduplicator = getSharedDeduplicator();
|
|
138
|
+
return deduplicator.getStats();
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
// src/helpers/auth.ts
|
|
142
|
+
import { NextResponse } from "next/server";
|
|
143
|
+
import {
|
|
144
|
+
getAuthenticatedUserCore,
|
|
145
|
+
isErrorResult
|
|
146
|
+
} from "@solvapay/server";
|
|
147
|
+
async function getAuthenticatedUser(request, options = {}) {
|
|
148
|
+
const result = await getAuthenticatedUserCore(request, options);
|
|
149
|
+
if (isErrorResult(result)) {
|
|
150
|
+
return NextResponse.json(
|
|
151
|
+
{ error: result.error, details: result.details },
|
|
152
|
+
{ status: result.status }
|
|
153
|
+
);
|
|
154
|
+
}
|
|
155
|
+
return result;
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
// src/helpers/customer.ts
|
|
159
|
+
import { NextResponse as NextResponse2 } from "next/server";
|
|
160
|
+
import { syncCustomerCore, isErrorResult as isErrorResult2 } from "@solvapay/server";
|
|
161
|
+
async function syncCustomer(request, options = {}) {
|
|
162
|
+
const result = await syncCustomerCore(request, options);
|
|
163
|
+
if (isErrorResult2(result)) {
|
|
164
|
+
return NextResponse2.json(
|
|
165
|
+
{ error: result.error, details: result.details },
|
|
166
|
+
{ status: result.status }
|
|
167
|
+
);
|
|
168
|
+
}
|
|
169
|
+
return result;
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
// src/helpers/payment.ts
|
|
173
|
+
import { NextResponse as NextResponse3 } from "next/server";
|
|
174
|
+
import {
|
|
175
|
+
createPaymentIntentCore,
|
|
176
|
+
processPaymentIntentCore,
|
|
177
|
+
isErrorResult as isErrorResult3
|
|
178
|
+
} from "@solvapay/server";
|
|
179
|
+
import { getAuthenticatedUserCore as getAuthenticatedUserCore2 } from "@solvapay/server";
|
|
180
|
+
async function createPaymentIntent(request, body, options = {}) {
|
|
181
|
+
const result = await createPaymentIntentCore(request, body, options);
|
|
182
|
+
if (isErrorResult3(result)) {
|
|
183
|
+
return NextResponse3.json(
|
|
184
|
+
{ error: result.error, details: result.details },
|
|
185
|
+
{ status: result.status }
|
|
186
|
+
);
|
|
187
|
+
}
|
|
188
|
+
try {
|
|
189
|
+
const userResult = await getAuthenticatedUserCore2(request);
|
|
190
|
+
if (!isErrorResult3(userResult)) {
|
|
191
|
+
clearPurchaseCache(userResult.userId);
|
|
192
|
+
}
|
|
193
|
+
} catch {
|
|
194
|
+
}
|
|
195
|
+
return result;
|
|
196
|
+
}
|
|
197
|
+
async function processPaymentIntent(request, body, options = {}) {
|
|
198
|
+
const result = await processPaymentIntentCore(request, body, options);
|
|
199
|
+
if (isErrorResult3(result)) {
|
|
200
|
+
return NextResponse3.json(
|
|
201
|
+
{ error: result.error, details: result.details },
|
|
202
|
+
{ status: result.status }
|
|
203
|
+
);
|
|
204
|
+
}
|
|
205
|
+
try {
|
|
206
|
+
const userResult = await getAuthenticatedUserCore2(request);
|
|
207
|
+
if (!isErrorResult3(userResult)) {
|
|
208
|
+
clearPurchaseCache(userResult.userId);
|
|
209
|
+
}
|
|
210
|
+
} catch {
|
|
211
|
+
}
|
|
212
|
+
return result;
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
// src/helpers/checkout.ts
|
|
216
|
+
import { NextResponse as NextResponse4 } from "next/server";
|
|
217
|
+
import {
|
|
218
|
+
createCheckoutSessionCore,
|
|
219
|
+
createCustomerSessionCore,
|
|
220
|
+
isErrorResult as isErrorResult4
|
|
221
|
+
} from "@solvapay/server";
|
|
222
|
+
async function createCheckoutSession(request, body, options = {}) {
|
|
223
|
+
const result = await createCheckoutSessionCore(request, body, options);
|
|
224
|
+
if (isErrorResult4(result)) {
|
|
225
|
+
return NextResponse4.json(
|
|
226
|
+
{ error: result.error, details: result.details },
|
|
227
|
+
{ status: result.status }
|
|
228
|
+
);
|
|
229
|
+
}
|
|
230
|
+
return result;
|
|
231
|
+
}
|
|
232
|
+
async function createCustomerSession(request, options = {}) {
|
|
233
|
+
const result = await createCustomerSessionCore(request, options);
|
|
234
|
+
if (isErrorResult4(result)) {
|
|
235
|
+
return NextResponse4.json(
|
|
236
|
+
{ error: result.error, details: result.details },
|
|
237
|
+
{ status: result.status }
|
|
238
|
+
);
|
|
239
|
+
}
|
|
240
|
+
return result;
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
// src/helpers/renewal.ts
|
|
244
|
+
import { NextResponse as NextResponse5 } from "next/server";
|
|
245
|
+
import { cancelPurchaseCore, isErrorResult as isErrorResult5 } from "@solvapay/server";
|
|
246
|
+
import { getAuthenticatedUserCore as getAuthenticatedUserCore3 } from "@solvapay/server";
|
|
247
|
+
async function cancelRenewal(request, body, options = {}) {
|
|
248
|
+
const result = await cancelPurchaseCore(request, body, options);
|
|
249
|
+
if (isErrorResult5(result)) {
|
|
250
|
+
return NextResponse5.json(
|
|
251
|
+
{ error: result.error, details: result.details },
|
|
252
|
+
{ status: result.status }
|
|
253
|
+
);
|
|
254
|
+
}
|
|
255
|
+
try {
|
|
256
|
+
const userResult = await getAuthenticatedUserCore3(request);
|
|
257
|
+
if (!isErrorResult5(userResult)) {
|
|
258
|
+
clearPurchaseCache(userResult.userId);
|
|
259
|
+
}
|
|
260
|
+
} catch {
|
|
261
|
+
}
|
|
262
|
+
return result;
|
|
120
263
|
}
|
|
121
|
-
|
|
264
|
+
|
|
265
|
+
// src/helpers/plans.ts
|
|
266
|
+
import { NextResponse as NextResponse6 } from "next/server";
|
|
267
|
+
import { listPlansCore, isErrorResult as isErrorResult6 } from "@solvapay/server";
|
|
268
|
+
async function listPlans(request) {
|
|
269
|
+
const result = await listPlansCore(request);
|
|
270
|
+
if (isErrorResult6(result)) {
|
|
271
|
+
return NextResponse6.json(
|
|
272
|
+
{ error: result.error, details: result.details },
|
|
273
|
+
{ status: result.status }
|
|
274
|
+
);
|
|
275
|
+
}
|
|
276
|
+
return result;
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
// src/index.ts
|
|
280
|
+
async function checkPurchase(request, options = {}) {
|
|
122
281
|
try {
|
|
123
282
|
const { requireUserId, getUserEmailFromRequest, getUserNameFromRequest } = await import("@solvapay/auth");
|
|
124
283
|
const userIdOrError = requireUserId(request);
|
|
125
284
|
if (userIdOrError instanceof Response) {
|
|
126
285
|
const clonedResponse = userIdOrError.clone();
|
|
127
286
|
const body = await clonedResponse.json().catch(() => ({ error: "Unauthorized" }));
|
|
128
|
-
return
|
|
287
|
+
return NextResponse7.json(body, { status: userIdOrError.status });
|
|
129
288
|
}
|
|
130
289
|
const userId = userIdOrError;
|
|
131
290
|
const email = options.includeEmail !== false ? await getUserEmailFromRequest(request) : null;
|
|
132
291
|
const name = options.includeName !== false ? await getUserNameFromRequest(request) : null;
|
|
292
|
+
const cachedCustomerRef = request.headers.get("x-solvapay-customer-ref");
|
|
293
|
+
const solvaPay = options.solvaPay || createSolvaPay();
|
|
294
|
+
if (cachedCustomerRef) {
|
|
295
|
+
try {
|
|
296
|
+
const customer = await solvaPay.getCustomer({ customerRef: cachedCustomerRef });
|
|
297
|
+
if (customer && customer.customerRef) {
|
|
298
|
+
if (customer.externalRef && customer.externalRef === userId) {
|
|
299
|
+
const filteredPurchases = (customer.purchases || []).filter(
|
|
300
|
+
(p) => p.status === "active"
|
|
301
|
+
);
|
|
302
|
+
return {
|
|
303
|
+
customerRef: customer.customerRef,
|
|
304
|
+
email: customer.email,
|
|
305
|
+
name: customer.name,
|
|
306
|
+
purchases: filteredPurchases
|
|
307
|
+
};
|
|
308
|
+
}
|
|
309
|
+
}
|
|
310
|
+
} catch {
|
|
311
|
+
}
|
|
312
|
+
}
|
|
133
313
|
const deduplicator = getSharedDeduplicator(options.deduplication);
|
|
134
314
|
const response = await deduplicator.deduplicate(userId, async () => {
|
|
135
315
|
try {
|
|
136
|
-
const solvaPay = options.solvaPay || createSolvaPay();
|
|
137
316
|
const ensuredCustomerRef = await solvaPay.ensureCustomer(userId, userId, {
|
|
138
317
|
email: email || void 0,
|
|
139
318
|
name: name || void 0
|
|
140
319
|
});
|
|
141
320
|
const customer = await solvaPay.getCustomer({ customerRef: ensuredCustomerRef });
|
|
142
|
-
const
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
const isCancelled = sub.status === "cancelled" || subAny.cancelledAt;
|
|
146
|
-
if (!isCancelled) {
|
|
147
|
-
return true;
|
|
148
|
-
}
|
|
149
|
-
if (isCancelled && subAny.endDate) {
|
|
150
|
-
const endDate = new Date(subAny.endDate);
|
|
151
|
-
const isFuture = endDate > now;
|
|
152
|
-
return isFuture;
|
|
153
|
-
}
|
|
154
|
-
return false;
|
|
155
|
-
});
|
|
321
|
+
const filteredPurchases = (customer.purchases || []).filter(
|
|
322
|
+
(p) => p.status === "active"
|
|
323
|
+
);
|
|
156
324
|
const result = {
|
|
157
325
|
customerRef: customer.customerRef || userId,
|
|
158
326
|
email: customer.email,
|
|
159
327
|
name: customer.name,
|
|
160
|
-
|
|
328
|
+
purchases: filteredPurchases
|
|
161
329
|
};
|
|
162
330
|
return result;
|
|
163
331
|
} catch (error) {
|
|
164
|
-
console.error("[
|
|
332
|
+
console.error("[checkPurchase] Error fetching customer:", error);
|
|
165
333
|
return {
|
|
166
334
|
customerRef: userId,
|
|
167
|
-
|
|
335
|
+
purchases: []
|
|
168
336
|
};
|
|
169
337
|
}
|
|
170
338
|
});
|
|
171
339
|
return response;
|
|
172
340
|
} catch (error) {
|
|
173
|
-
console.error("Check
|
|
341
|
+
console.error("Check purchase failed:", error);
|
|
174
342
|
if (error instanceof SolvaPayError) {
|
|
175
|
-
return
|
|
176
|
-
{ error: error.message },
|
|
177
|
-
{ status: 500 }
|
|
178
|
-
);
|
|
343
|
+
return NextResponse7.json({ error: error.message }, { status: 500 });
|
|
179
344
|
}
|
|
180
345
|
const errorMessage = error instanceof Error ? error.message : "Unknown error";
|
|
181
|
-
return
|
|
182
|
-
{ error: "Failed to check
|
|
346
|
+
return NextResponse7.json(
|
|
347
|
+
{ error: "Failed to check purchase", details: errorMessage },
|
|
183
348
|
{ status: 500 }
|
|
184
349
|
);
|
|
185
350
|
}
|
|
186
351
|
}
|
|
187
|
-
function clearSubscriptionCache(userId) {
|
|
188
|
-
const deduplicator = getSharedDeduplicator();
|
|
189
|
-
deduplicator.clearCache(userId);
|
|
190
|
-
}
|
|
191
|
-
function clearAllSubscriptionCache() {
|
|
192
|
-
const deduplicator = getSharedDeduplicator();
|
|
193
|
-
deduplicator.clearAllCache();
|
|
194
|
-
}
|
|
195
|
-
function getSubscriptionCacheStats() {
|
|
196
|
-
const deduplicator = getSharedDeduplicator();
|
|
197
|
-
return deduplicator.getStats();
|
|
198
|
-
}
|
|
199
352
|
export {
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
353
|
+
cancelRenewal,
|
|
354
|
+
checkPurchase,
|
|
355
|
+
clearAllPurchaseCache,
|
|
356
|
+
clearPurchaseCache,
|
|
357
|
+
createAuthMiddleware,
|
|
358
|
+
createCheckoutSession,
|
|
359
|
+
createCustomerSession,
|
|
360
|
+
createPaymentIntent,
|
|
361
|
+
createSupabaseAuthMiddleware,
|
|
362
|
+
getAuthenticatedUser,
|
|
363
|
+
getPurchaseCacheStats,
|
|
364
|
+
listPlans,
|
|
365
|
+
processPaymentIntent,
|
|
366
|
+
syncCustomer
|
|
204
367
|
};
|
|
@@ -0,0 +1,111 @@
|
|
|
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/middleware.ts
|
|
31
|
+
var middleware_exports = {};
|
|
32
|
+
__export(middleware_exports, {
|
|
33
|
+
createAuthMiddleware: () => createAuthMiddleware,
|
|
34
|
+
createSupabaseAuthMiddleware: () => createSupabaseAuthMiddleware
|
|
35
|
+
});
|
|
36
|
+
module.exports = __toCommonJS(middleware_exports);
|
|
37
|
+
|
|
38
|
+
// src/helpers/middleware.ts
|
|
39
|
+
var import_server = require("next/server");
|
|
40
|
+
function createAuthMiddleware(options) {
|
|
41
|
+
const { adapter, publicRoutes = [], userIdHeader = "x-user-id" } = options;
|
|
42
|
+
return async function middleware(request) {
|
|
43
|
+
const req = request;
|
|
44
|
+
const { pathname } = req.nextUrl;
|
|
45
|
+
if (!pathname.startsWith("/api")) {
|
|
46
|
+
return import_server.NextResponse.next();
|
|
47
|
+
}
|
|
48
|
+
const isPublicRoute = publicRoutes.some((route) => pathname.startsWith(route));
|
|
49
|
+
const userId = await adapter.getUserIdFromRequest(req);
|
|
50
|
+
if (isPublicRoute) {
|
|
51
|
+
const requestHeaders2 = new Headers(req.headers);
|
|
52
|
+
if (userId) {
|
|
53
|
+
requestHeaders2.set(userIdHeader, userId);
|
|
54
|
+
}
|
|
55
|
+
return import_server.NextResponse.next({
|
|
56
|
+
request: {
|
|
57
|
+
headers: requestHeaders2
|
|
58
|
+
}
|
|
59
|
+
});
|
|
60
|
+
}
|
|
61
|
+
if (!userId) {
|
|
62
|
+
return import_server.NextResponse.json(
|
|
63
|
+
{ error: "Unauthorized", details: "Valid authentication required" },
|
|
64
|
+
{ status: 401 }
|
|
65
|
+
);
|
|
66
|
+
}
|
|
67
|
+
const requestHeaders = new Headers(req.headers);
|
|
68
|
+
requestHeaders.set(userIdHeader, userId);
|
|
69
|
+
return import_server.NextResponse.next({
|
|
70
|
+
request: {
|
|
71
|
+
headers: requestHeaders
|
|
72
|
+
}
|
|
73
|
+
});
|
|
74
|
+
};
|
|
75
|
+
}
|
|
76
|
+
function createSupabaseAuthMiddleware(options = {}) {
|
|
77
|
+
const { jwtSecret, publicRoutes = [], userIdHeader = "x-user-id" } = options;
|
|
78
|
+
let authAdapter = null;
|
|
79
|
+
let adapterPromise = null;
|
|
80
|
+
const lazyAdapter = {
|
|
81
|
+
async getUserIdFromRequest(req) {
|
|
82
|
+
if (!authAdapter) {
|
|
83
|
+
if (!adapterPromise) {
|
|
84
|
+
adapterPromise = (async () => {
|
|
85
|
+
const secret = jwtSecret || process.env.SUPABASE_JWT_SECRET;
|
|
86
|
+
if (!secret) {
|
|
87
|
+
throw new Error(
|
|
88
|
+
"SUPABASE_JWT_SECRET environment variable is required. Please set it in your .env.local file. Get it from: Supabase Dashboard \u2192 Settings \u2192 API \u2192 JWT Secret"
|
|
89
|
+
);
|
|
90
|
+
}
|
|
91
|
+
const { SupabaseAuthAdapter } = await import("@solvapay/auth/supabase");
|
|
92
|
+
authAdapter = new SupabaseAuthAdapter({ jwtSecret: secret });
|
|
93
|
+
return authAdapter;
|
|
94
|
+
})();
|
|
95
|
+
}
|
|
96
|
+
authAdapter = await adapterPromise;
|
|
97
|
+
}
|
|
98
|
+
return authAdapter.getUserIdFromRequest(req);
|
|
99
|
+
}
|
|
100
|
+
};
|
|
101
|
+
return createAuthMiddleware({
|
|
102
|
+
adapter: lazyAdapter,
|
|
103
|
+
publicRoutes,
|
|
104
|
+
userIdHeader
|
|
105
|
+
});
|
|
106
|
+
}
|
|
107
|
+
// Annotate the CommonJS export names for ESM import in node:
|
|
108
|
+
0 && (module.exports = {
|
|
109
|
+
createAuthMiddleware,
|
|
110
|
+
createSupabaseAuthMiddleware
|
|
111
|
+
});
|
|
@@ -0,0 +1,178 @@
|
|
|
1
|
+
import { NextRequest, NextResponse } from 'next/server';
|
|
2
|
+
import { AuthAdapter } from '@solvapay/auth';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Next.js Middleware Helpers
|
|
6
|
+
*
|
|
7
|
+
* Helpers for creating authentication middleware in Next.js.
|
|
8
|
+
* Works with any AuthAdapter implementation (Supabase, custom, etc.)
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Configuration options for authentication middleware
|
|
13
|
+
*/
|
|
14
|
+
interface AuthMiddlewareOptions {
|
|
15
|
+
/**
|
|
16
|
+
* Auth adapter instance to use for extracting user IDs from requests
|
|
17
|
+
* You can use SupabaseAuthAdapter, MockAuthAdapter, or create your own
|
|
18
|
+
*/
|
|
19
|
+
adapter: AuthAdapter;
|
|
20
|
+
/**
|
|
21
|
+
* Public routes that don't require authentication
|
|
22
|
+
* Routes are matched using pathname.startsWith()
|
|
23
|
+
*/
|
|
24
|
+
publicRoutes?: string[];
|
|
25
|
+
/**
|
|
26
|
+
* Header name to store the user ID (default: 'x-user-id')
|
|
27
|
+
*/
|
|
28
|
+
userIdHeader?: string;
|
|
29
|
+
}
|
|
30
|
+
/**
|
|
31
|
+
* Creates a Next.js middleware function for authentication
|
|
32
|
+
*
|
|
33
|
+
* This helper:
|
|
34
|
+
* 1. Uses the provided AuthAdapter to extract userId from requests
|
|
35
|
+
* 2. Handles public vs protected routes
|
|
36
|
+
* 3. Adds userId to request headers for downstream routes
|
|
37
|
+
* 4. Returns appropriate error responses for auth failures
|
|
38
|
+
*
|
|
39
|
+
* @param options - Configuration options
|
|
40
|
+
* @returns Next.js middleware function (can be exported as `middleware` or `proxy`)
|
|
41
|
+
*
|
|
42
|
+
* @example Next.js 15
|
|
43
|
+
* ```typescript
|
|
44
|
+
* // middleware.ts (at project root)
|
|
45
|
+
* import { createAuthMiddleware } from '@solvapay/next';
|
|
46
|
+
* import { SupabaseAuthAdapter } from '@solvapay/auth/supabase';
|
|
47
|
+
*
|
|
48
|
+
* const adapter = new SupabaseAuthAdapter({
|
|
49
|
+
* jwtSecret: process.env.SUPABASE_JWT_SECRET!,
|
|
50
|
+
* });
|
|
51
|
+
*
|
|
52
|
+
* export const middleware = createAuthMiddleware({
|
|
53
|
+
* adapter,
|
|
54
|
+
* publicRoutes: ['/api/list-plans'],
|
|
55
|
+
* });
|
|
56
|
+
*
|
|
57
|
+
* export const config = {
|
|
58
|
+
* matcher: ['/api/:path*'],
|
|
59
|
+
* };
|
|
60
|
+
* ```
|
|
61
|
+
*
|
|
62
|
+
* @example Next.js 16 with src/ folder
|
|
63
|
+
* ```typescript
|
|
64
|
+
* // src/proxy.ts (in src/ folder, not project root)
|
|
65
|
+
* import { createAuthMiddleware } from '@solvapay/next';
|
|
66
|
+
* import { SupabaseAuthAdapter } from '@solvapay/auth/supabase';
|
|
67
|
+
*
|
|
68
|
+
* const adapter = new SupabaseAuthAdapter({
|
|
69
|
+
* jwtSecret: process.env.SUPABASE_JWT_SECRET!,
|
|
70
|
+
* });
|
|
71
|
+
*
|
|
72
|
+
* // Use 'proxy' export for Next.js 16 (no deprecation warning)
|
|
73
|
+
* export const proxy = createAuthMiddleware({
|
|
74
|
+
* adapter,
|
|
75
|
+
* publicRoutes: ['/api/list-plans'],
|
|
76
|
+
* });
|
|
77
|
+
*
|
|
78
|
+
* export const config = {
|
|
79
|
+
* matcher: ['/api/:path*'],
|
|
80
|
+
* };
|
|
81
|
+
* ```
|
|
82
|
+
*
|
|
83
|
+
* @example Custom adapter
|
|
84
|
+
* ```typescript
|
|
85
|
+
* import { createAuthMiddleware } from '@solvapay/next';
|
|
86
|
+
* import type { AuthAdapter } from '@solvapay/auth';
|
|
87
|
+
*
|
|
88
|
+
* const myAdapter: AuthAdapter = {
|
|
89
|
+
* async getUserIdFromRequest(req) {
|
|
90
|
+
* // Your custom auth logic
|
|
91
|
+
* return userId;
|
|
92
|
+
* },
|
|
93
|
+
* };
|
|
94
|
+
*
|
|
95
|
+
* export const middleware = createAuthMiddleware({
|
|
96
|
+
* adapter: myAdapter,
|
|
97
|
+
* });
|
|
98
|
+
* ```
|
|
99
|
+
*
|
|
100
|
+
* **File Location Notes:**
|
|
101
|
+
* - **Next.js 15**: Place `middleware.ts` at project root
|
|
102
|
+
* - **Next.js 16 without `src/` folder**: Place `middleware.ts` or `proxy.ts` at project root
|
|
103
|
+
* - **Next.js 16 with `src/` folder**: Place `src/proxy.ts` or `src/middleware.ts` (in `src/` folder, not root)
|
|
104
|
+
*
|
|
105
|
+
* **Note:** Next.js 16 renamed "middleware" to "proxy". You can export the return value as either
|
|
106
|
+
* `middleware` or `proxy` - both work, but `proxy` is recommended to avoid deprecation warnings.
|
|
107
|
+
*/
|
|
108
|
+
declare function createAuthMiddleware(options: AuthMiddlewareOptions): (request: NextRequest) => Promise<NextResponse<unknown>>;
|
|
109
|
+
/**
|
|
110
|
+
* Configuration options for Supabase authentication middleware
|
|
111
|
+
*/
|
|
112
|
+
interface SupabaseAuthMiddlewareOptions {
|
|
113
|
+
/**
|
|
114
|
+
* Supabase JWT secret (from Supabase dashboard: Settings → API → JWT Secret)
|
|
115
|
+
* If not provided, will use SUPABASE_JWT_SECRET environment variable
|
|
116
|
+
*/
|
|
117
|
+
jwtSecret?: string;
|
|
118
|
+
/**
|
|
119
|
+
* Public routes that don't require authentication
|
|
120
|
+
* Routes are matched using pathname.startsWith()
|
|
121
|
+
*/
|
|
122
|
+
publicRoutes?: string[];
|
|
123
|
+
/**
|
|
124
|
+
* Header name to store the user ID (default: 'x-user-id')
|
|
125
|
+
*/
|
|
126
|
+
userIdHeader?: string;
|
|
127
|
+
}
|
|
128
|
+
/**
|
|
129
|
+
* Creates a Next.js middleware function for Supabase authentication
|
|
130
|
+
*
|
|
131
|
+
* Convenience function that creates a SupabaseAuthAdapter and wraps it with createAuthMiddleware.
|
|
132
|
+
* Only use this if you're using Supabase - otherwise use createAuthMiddleware with your own adapter.
|
|
133
|
+
*
|
|
134
|
+
* Uses dynamic import to avoid requiring Supabase as a dependency in @solvapay/next.
|
|
135
|
+
*
|
|
136
|
+
* @param options - Configuration options
|
|
137
|
+
* @returns Next.js middleware function (can be exported as `middleware` or `proxy`)
|
|
138
|
+
*
|
|
139
|
+
* @example Next.js 15
|
|
140
|
+
* ```typescript
|
|
141
|
+
* // middleware.ts (at project root)
|
|
142
|
+
* import { createSupabaseAuthMiddleware } from '@solvapay/next';
|
|
143
|
+
*
|
|
144
|
+
* export const middleware = createSupabaseAuthMiddleware({
|
|
145
|
+
* publicRoutes: ['/api/list-plans'],
|
|
146
|
+
* });
|
|
147
|
+
*
|
|
148
|
+
* export const config = {
|
|
149
|
+
* matcher: ['/api/:path*'],
|
|
150
|
+
* };
|
|
151
|
+
* ```
|
|
152
|
+
*
|
|
153
|
+
* @example Next.js 16 with src/ folder
|
|
154
|
+
* ```typescript
|
|
155
|
+
* // src/proxy.ts (in src/ folder, not project root)
|
|
156
|
+
* import { createSupabaseAuthMiddleware } from '@solvapay/next';
|
|
157
|
+
*
|
|
158
|
+
* // Use 'proxy' export for Next.js 16 (no deprecation warning)
|
|
159
|
+
* export const proxy = createSupabaseAuthMiddleware({
|
|
160
|
+
* publicRoutes: ['/api/list-plans'],
|
|
161
|
+
* });
|
|
162
|
+
*
|
|
163
|
+
* export const config = {
|
|
164
|
+
* matcher: ['/api/:path*'],
|
|
165
|
+
* };
|
|
166
|
+
* ```
|
|
167
|
+
*
|
|
168
|
+
* **File Location Notes:**
|
|
169
|
+
* - **Next.js 15**: Place `middleware.ts` at project root
|
|
170
|
+
* - **Next.js 16 without `src/` folder**: Place `middleware.ts` or `proxy.ts` at project root
|
|
171
|
+
* - **Next.js 16 with `src/` folder**: Place `src/proxy.ts` or `src/middleware.ts` (in `src/` folder, not root)
|
|
172
|
+
*
|
|
173
|
+
* **Note:** Next.js 16 renamed "middleware" to "proxy". You can export the return value as either
|
|
174
|
+
* `middleware` or `proxy` - both work, but `proxy` is recommended to avoid deprecation warnings.
|
|
175
|
+
*/
|
|
176
|
+
declare function createSupabaseAuthMiddleware(options?: SupabaseAuthMiddlewareOptions): (request: NextRequest) => Promise<NextResponse<unknown>>;
|
|
177
|
+
|
|
178
|
+
export { type AuthMiddlewareOptions, type SupabaseAuthMiddlewareOptions, createAuthMiddleware, createSupabaseAuthMiddleware };
|