@solvapay/next 1.0.0-preview.10

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js ADDED
@@ -0,0 +1,449 @@
1
+ // src/index.ts
2
+ import { NextResponse as NextResponse8 } from "next/server";
3
+ import { createSolvaPay } from "@solvapay/server";
4
+ import { SolvaPayError } from "@solvapay/core";
5
+
6
+ // src/helpers/auth.ts
7
+ import { NextResponse } from "next/server";
8
+ import {
9
+ getAuthenticatedUserCore,
10
+ isErrorResult
11
+ } from "@solvapay/server";
12
+ async function getAuthenticatedUser(request, options = {}) {
13
+ const result = await getAuthenticatedUserCore(request, options);
14
+ if (isErrorResult(result)) {
15
+ return NextResponse.json(
16
+ { error: result.error, details: result.details },
17
+ { status: result.status }
18
+ );
19
+ }
20
+ return result;
21
+ }
22
+
23
+ // src/helpers/customer.ts
24
+ import { NextResponse as NextResponse2 } from "next/server";
25
+ import {
26
+ syncCustomerCore,
27
+ isErrorResult as isErrorResult2
28
+ } from "@solvapay/server";
29
+ async function syncCustomer(request, options = {}) {
30
+ const result = await syncCustomerCore(request, options);
31
+ if (isErrorResult2(result)) {
32
+ return NextResponse2.json(
33
+ { error: result.error, details: result.details },
34
+ { status: result.status }
35
+ );
36
+ }
37
+ return result;
38
+ }
39
+
40
+ // src/helpers/payment.ts
41
+ import { NextResponse as NextResponse3 } from "next/server";
42
+ import {
43
+ createPaymentIntentCore,
44
+ processPaymentCore,
45
+ isErrorResult as isErrorResult3
46
+ } from "@solvapay/server";
47
+ import { getAuthenticatedUserCore as getAuthenticatedUserCore2 } from "@solvapay/server";
48
+ async function createPaymentIntent(request, body, options = {}) {
49
+ const result = await createPaymentIntentCore(request, body, options);
50
+ if (isErrorResult3(result)) {
51
+ return NextResponse3.json(
52
+ { error: result.error, details: result.details },
53
+ { status: result.status }
54
+ );
55
+ }
56
+ try {
57
+ const userResult = await getAuthenticatedUserCore2(request);
58
+ if (!isErrorResult3(userResult)) {
59
+ clearSubscriptionCache(userResult.userId);
60
+ }
61
+ } catch {
62
+ }
63
+ return result;
64
+ }
65
+ async function processPayment(request, body, options = {}) {
66
+ const result = await processPaymentCore(request, body, options);
67
+ if (isErrorResult3(result)) {
68
+ return NextResponse3.json(
69
+ { error: result.error, details: result.details },
70
+ { status: result.status }
71
+ );
72
+ }
73
+ try {
74
+ const userResult = await getAuthenticatedUserCore2(request);
75
+ if (!isErrorResult3(userResult)) {
76
+ clearSubscriptionCache(userResult.userId);
77
+ }
78
+ } catch {
79
+ }
80
+ return result;
81
+ }
82
+
83
+ // src/helpers/checkout.ts
84
+ import { NextResponse as NextResponse4 } from "next/server";
85
+ import {
86
+ createCheckoutSessionCore,
87
+ createCustomerSessionCore,
88
+ isErrorResult as isErrorResult4
89
+ } from "@solvapay/server";
90
+ async function createCheckoutSession(request, body, options = {}) {
91
+ const result = await createCheckoutSessionCore(request, body, options);
92
+ if (isErrorResult4(result)) {
93
+ return NextResponse4.json(
94
+ { error: result.error, details: result.details },
95
+ { status: result.status }
96
+ );
97
+ }
98
+ return result;
99
+ }
100
+ async function createCustomerSession(request, options = {}) {
101
+ const result = await createCustomerSessionCore(request, options);
102
+ if (isErrorResult4(result)) {
103
+ return NextResponse4.json(
104
+ { error: result.error, details: result.details },
105
+ { status: result.status }
106
+ );
107
+ }
108
+ return result;
109
+ }
110
+
111
+ // src/helpers/subscription.ts
112
+ import { NextResponse as NextResponse5 } from "next/server";
113
+ import {
114
+ cancelSubscriptionCore,
115
+ isErrorResult as isErrorResult5
116
+ } from "@solvapay/server";
117
+ import { getAuthenticatedUserCore as getAuthenticatedUserCore3 } from "@solvapay/server";
118
+ async function cancelSubscription(request, body, options = {}) {
119
+ const result = await cancelSubscriptionCore(request, body, options);
120
+ if (isErrorResult5(result)) {
121
+ return NextResponse5.json(
122
+ { error: result.error, details: result.details },
123
+ { status: result.status }
124
+ );
125
+ }
126
+ try {
127
+ const userResult = await getAuthenticatedUserCore3(request);
128
+ if (!isErrorResult5(userResult)) {
129
+ clearSubscriptionCache(userResult.userId);
130
+ }
131
+ } catch {
132
+ }
133
+ return result;
134
+ }
135
+
136
+ // src/helpers/plans.ts
137
+ import { NextResponse as NextResponse6 } from "next/server";
138
+ import {
139
+ listPlansCore,
140
+ isErrorResult as isErrorResult6
141
+ } from "@solvapay/server";
142
+ async function listPlans(request) {
143
+ const result = await listPlansCore(request);
144
+ if (isErrorResult6(result)) {
145
+ return NextResponse6.json(
146
+ { error: result.error, details: result.details },
147
+ { status: result.status }
148
+ );
149
+ }
150
+ return result;
151
+ }
152
+
153
+ // src/helpers/middleware.ts
154
+ import { NextResponse as NextResponse7 } from "next/server";
155
+ function createAuthMiddleware(options) {
156
+ const {
157
+ adapter,
158
+ publicRoutes = [],
159
+ userIdHeader = "x-user-id"
160
+ } = options;
161
+ return async function middleware(request) {
162
+ const { pathname } = request.nextUrl;
163
+ if (!pathname.startsWith("/api")) {
164
+ return NextResponse7.next();
165
+ }
166
+ const isPublicRoute = publicRoutes.some((route) => pathname.startsWith(route));
167
+ const userId = await adapter.getUserIdFromRequest(request);
168
+ if (isPublicRoute) {
169
+ const requestHeaders2 = new Headers(request.headers);
170
+ if (userId) {
171
+ requestHeaders2.set(userIdHeader, userId);
172
+ }
173
+ return NextResponse7.next({
174
+ request: {
175
+ headers: requestHeaders2
176
+ }
177
+ });
178
+ }
179
+ if (!userId) {
180
+ return NextResponse7.json(
181
+ { error: "Unauthorized", details: "Valid authentication required" },
182
+ { status: 401 }
183
+ );
184
+ }
185
+ const requestHeaders = new Headers(request.headers);
186
+ requestHeaders.set(userIdHeader, userId);
187
+ return NextResponse7.next({
188
+ request: {
189
+ headers: requestHeaders
190
+ }
191
+ });
192
+ };
193
+ }
194
+ function createSupabaseAuthMiddleware(options = {}) {
195
+ const {
196
+ jwtSecret,
197
+ publicRoutes = [],
198
+ userIdHeader = "x-user-id"
199
+ } = options;
200
+ let authAdapter = null;
201
+ let adapterPromise = null;
202
+ const lazyAdapter = {
203
+ async getUserIdFromRequest(req) {
204
+ if (!authAdapter) {
205
+ if (!adapterPromise) {
206
+ adapterPromise = (async () => {
207
+ const secret = jwtSecret || process.env.SUPABASE_JWT_SECRET;
208
+ if (!secret) {
209
+ throw new Error(
210
+ "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"
211
+ );
212
+ }
213
+ const { SupabaseAuthAdapter } = await import("@solvapay/auth/supabase");
214
+ authAdapter = new SupabaseAuthAdapter({ jwtSecret: secret });
215
+ return authAdapter;
216
+ })();
217
+ }
218
+ authAdapter = await adapterPromise;
219
+ }
220
+ return authAdapter.getUserIdFromRequest(req);
221
+ }
222
+ };
223
+ return createAuthMiddleware({
224
+ adapter: lazyAdapter,
225
+ publicRoutes,
226
+ userIdHeader
227
+ });
228
+ }
229
+
230
+ // src/index.ts
231
+ function createRequestDeduplicator(options = {}) {
232
+ const {
233
+ cacheTTL = 2e3,
234
+ maxCacheSize = 1e3,
235
+ cacheErrors = true
236
+ } = options;
237
+ const inFlightRequests = /* @__PURE__ */ new Map();
238
+ const resultCache = /* @__PURE__ */ new Map();
239
+ const cacheInvalidatedAt = /* @__PURE__ */ new Map();
240
+ let cleanupInterval = null;
241
+ if (cacheTTL > 0) {
242
+ cleanupInterval = setInterval(() => {
243
+ const now = Date.now();
244
+ const entriesToDelete = [];
245
+ for (const [key, cached] of resultCache.entries()) {
246
+ if (now - cached.timestamp >= cacheTTL) {
247
+ entriesToDelete.push(key);
248
+ }
249
+ }
250
+ for (const key of entriesToDelete) {
251
+ resultCache.delete(key);
252
+ cacheInvalidatedAt.delete(key);
253
+ }
254
+ if (resultCache.size > maxCacheSize) {
255
+ const sortedEntries = Array.from(resultCache.entries()).sort((a, b) => a[1].timestamp - b[1].timestamp);
256
+ const toRemove = sortedEntries.slice(0, resultCache.size - maxCacheSize);
257
+ for (const [key] of toRemove) {
258
+ resultCache.delete(key);
259
+ cacheInvalidatedAt.delete(key);
260
+ }
261
+ }
262
+ }, Math.min(cacheTTL, 1e3));
263
+ }
264
+ const deduplicate = async (key, fn) => {
265
+ if (cacheTTL > 0) {
266
+ const cached = resultCache.get(key);
267
+ if (cached && Date.now() - cached.timestamp < cacheTTL) {
268
+ return cached.data;
269
+ } else if (cached) {
270
+ resultCache.delete(key);
271
+ }
272
+ }
273
+ let requestPromise = inFlightRequests.get(key);
274
+ if (!requestPromise) {
275
+ const requestStartTime = Date.now();
276
+ requestPromise = (async () => {
277
+ try {
278
+ const result = await fn();
279
+ const invalidatedAt = cacheInvalidatedAt.get(key);
280
+ const shouldCache = !invalidatedAt || invalidatedAt < requestStartTime;
281
+ if (cacheTTL > 0 && shouldCache) {
282
+ resultCache.set(key, {
283
+ data: result,
284
+ timestamp: Date.now()
285
+ });
286
+ }
287
+ return result;
288
+ } catch (error) {
289
+ const invalidatedAt = cacheInvalidatedAt.get(key);
290
+ const shouldCache = !invalidatedAt || invalidatedAt < requestStartTime;
291
+ if (cacheTTL > 0 && cacheErrors && shouldCache) {
292
+ resultCache.set(key, {
293
+ data: error,
294
+ timestamp: Date.now()
295
+ });
296
+ }
297
+ throw error;
298
+ } finally {
299
+ inFlightRequests.delete(key);
300
+ }
301
+ })();
302
+ const existingPromise = inFlightRequests.get(key);
303
+ if (existingPromise) {
304
+ requestPromise = existingPromise;
305
+ } else {
306
+ inFlightRequests.set(key, requestPromise);
307
+ }
308
+ }
309
+ return requestPromise;
310
+ };
311
+ const clearCache = (key) => {
312
+ resultCache.delete(key);
313
+ cacheInvalidatedAt.set(key, Date.now());
314
+ inFlightRequests.delete(key);
315
+ };
316
+ const clearAllCache = () => {
317
+ resultCache.clear();
318
+ cacheInvalidatedAt.clear();
319
+ inFlightRequests.clear();
320
+ };
321
+ const getStats = () => ({
322
+ inFlight: inFlightRequests.size,
323
+ cached: resultCache.size
324
+ });
325
+ return {
326
+ deduplicate,
327
+ clearCache,
328
+ clearAllCache,
329
+ getStats
330
+ };
331
+ }
332
+ var sharedSubscriptionDeduplicator = null;
333
+ function getSharedDeduplicator(options) {
334
+ if (!sharedSubscriptionDeduplicator) {
335
+ sharedSubscriptionDeduplicator = createRequestDeduplicator({
336
+ cacheTTL: 2e3,
337
+ // Cache results for 2 seconds
338
+ maxCacheSize: 1e3,
339
+ // Maximum cache entries
340
+ cacheErrors: true,
341
+ // Cache error results too
342
+ ...options
343
+ });
344
+ }
345
+ return sharedSubscriptionDeduplicator;
346
+ }
347
+ async function checkSubscription(request, options = {}) {
348
+ try {
349
+ const { requireUserId, getUserEmailFromRequest, getUserNameFromRequest } = await import("@solvapay/auth");
350
+ const userIdOrError = requireUserId(request);
351
+ if (userIdOrError instanceof Response) {
352
+ const clonedResponse = userIdOrError.clone();
353
+ const body = await clonedResponse.json().catch(() => ({ error: "Unauthorized" }));
354
+ return NextResponse8.json(body, { status: userIdOrError.status });
355
+ }
356
+ const userId = userIdOrError;
357
+ const email = options.includeEmail !== false ? await getUserEmailFromRequest(request) : null;
358
+ const name = options.includeName !== false ? await getUserNameFromRequest(request) : null;
359
+ const cachedCustomerRef = request.headers.get("x-solvapay-customer-ref");
360
+ const solvaPay = options.solvaPay || createSolvaPay();
361
+ if (cachedCustomerRef) {
362
+ try {
363
+ const customer = await solvaPay.getCustomer({ customerRef: cachedCustomerRef });
364
+ if (customer && customer.customerRef) {
365
+ if (customer.externalRef && customer.externalRef === userId) {
366
+ const filteredSubscriptions = (customer.subscriptions || []).filter(
367
+ (sub) => sub.status === "active"
368
+ );
369
+ return {
370
+ customerRef: customer.customerRef,
371
+ email: customer.email,
372
+ name: customer.name,
373
+ subscriptions: filteredSubscriptions
374
+ };
375
+ }
376
+ }
377
+ } catch (error) {
378
+ }
379
+ }
380
+ const deduplicator = getSharedDeduplicator(options.deduplication);
381
+ const response = await deduplicator.deduplicate(userId, async () => {
382
+ try {
383
+ const ensuredCustomerRef = await solvaPay.ensureCustomer(userId, userId, {
384
+ email: email || void 0,
385
+ name: name || void 0
386
+ });
387
+ const customer = await solvaPay.getCustomer({ customerRef: ensuredCustomerRef });
388
+ const filteredSubscriptions = (customer.subscriptions || []).filter(
389
+ (sub) => sub.status === "active"
390
+ );
391
+ const result = {
392
+ customerRef: customer.customerRef || userId,
393
+ email: customer.email,
394
+ name: customer.name,
395
+ subscriptions: filteredSubscriptions
396
+ };
397
+ return result;
398
+ } catch (error) {
399
+ console.error("[checkSubscription] Error fetching customer:", error);
400
+ return {
401
+ customerRef: userId,
402
+ subscriptions: []
403
+ };
404
+ }
405
+ });
406
+ return response;
407
+ } catch (error) {
408
+ console.error("Check subscription failed:", error);
409
+ if (error instanceof SolvaPayError) {
410
+ return NextResponse8.json(
411
+ { error: error.message },
412
+ { status: 500 }
413
+ );
414
+ }
415
+ const errorMessage = error instanceof Error ? error.message : "Unknown error";
416
+ return NextResponse8.json(
417
+ { error: "Failed to check subscription", details: errorMessage },
418
+ { status: 500 }
419
+ );
420
+ }
421
+ }
422
+ function clearSubscriptionCache(userId) {
423
+ const deduplicator = getSharedDeduplicator();
424
+ deduplicator.clearCache(userId);
425
+ }
426
+ function clearAllSubscriptionCache() {
427
+ const deduplicator = getSharedDeduplicator();
428
+ deduplicator.clearAllCache();
429
+ }
430
+ function getSubscriptionCacheStats() {
431
+ const deduplicator = getSharedDeduplicator();
432
+ return deduplicator.getStats();
433
+ }
434
+ export {
435
+ cancelSubscription,
436
+ checkSubscription,
437
+ clearAllSubscriptionCache,
438
+ clearSubscriptionCache,
439
+ createAuthMiddleware,
440
+ createCheckoutSession,
441
+ createCustomerSession,
442
+ createPaymentIntent,
443
+ createSupabaseAuthMiddleware,
444
+ getAuthenticatedUser,
445
+ getSubscriptionCacheStats,
446
+ listPlans,
447
+ processPayment,
448
+ syncCustomer
449
+ };
package/package.json ADDED
@@ -0,0 +1,52 @@
1
+ {
2
+ "name": "@solvapay/next",
3
+ "version": "1.0.0-preview.10",
4
+ "type": "module",
5
+ "main": "./dist/index.cjs",
6
+ "module": "./dist/index.js",
7
+ "types": "./dist/index.d.ts",
8
+ "exports": {
9
+ ".": {
10
+ "types": "./dist/index.d.ts",
11
+ "import": "./dist/index.js",
12
+ "require": "./dist/index.cjs"
13
+ }
14
+ },
15
+ "files": [
16
+ "dist",
17
+ "README.md"
18
+ ],
19
+ "publishConfig": {
20
+ "access": "public"
21
+ },
22
+ "repository": {
23
+ "type": "git",
24
+ "url": "https://github.com/solvapay/solvapay-sdk.git",
25
+ "directory": "packages/next"
26
+ },
27
+ "engines": {
28
+ "node": ">=18.17"
29
+ },
30
+ "sideEffects": false,
31
+ "dependencies": {
32
+ "@solvapay/auth": "1.0.0-preview.10",
33
+ "@solvapay/core": "1.0.0-preview.10",
34
+ "@solvapay/server": "1.0.0-preview.10"
35
+ },
36
+ "peerDependencies": {
37
+ "next": ">=13.0.0"
38
+ },
39
+ "devDependencies": {
40
+ "next": "^14.0.0",
41
+ "tsup": "^8.0.1",
42
+ "typescript": "^5.5.4",
43
+ "vitest": "^2.0.5",
44
+ "@solvapay/test-utils": "0.0.0"
45
+ },
46
+ "scripts": {
47
+ "build": "tsup src/index.ts --format esm,cjs --dts --tsconfig tsconfig.build.json --external next",
48
+ "test": "vitest run || exit 0",
49
+ "test:watch": "vitest",
50
+ "lint": "echo 'no lint yet'"
51
+ }
52
+ }