@solvapay/server 1.0.8-preview.8 → 1.0.8

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.
Files changed (148) hide show
  1. package/README.md +80 -2
  2. package/dist/edge.d.ts +495 -61
  3. package/dist/edge.js +648 -187
  4. package/dist/fetch/index.cjs +3071 -0
  5. package/dist/fetch/index.d.cts +112 -0
  6. package/dist/fetch/index.d.ts +112 -0
  7. package/dist/fetch/index.js +3017 -0
  8. package/dist/index.cjs +603 -5013
  9. package/dist/index.d.cts +490 -156
  10. package/dist/index.d.ts +490 -156
  11. package/dist/index.js +571 -695
  12. package/package.json +15 -12
  13. package/dist/chunk-MLKGABMK.js +0 -9
  14. package/dist/chunk-R5U7XKVJ.js +0 -16
  15. package/dist/dist-EPVKJAIP.js +0 -94
  16. package/dist/dist-JBJ4HMP7.js +0 -94
  17. package/dist/esm-5GYCIXIY.js +0 -3475
  18. package/dist/esm-UW7WCMEK.js +0 -3475
  19. package/dist/src/adapters/base.d.ts +0 -57
  20. package/dist/src/adapters/base.d.ts.map +0 -1
  21. package/dist/src/adapters/base.js +0 -73
  22. package/dist/src/adapters/base.js.map +0 -1
  23. package/dist/src/adapters/http.d.ts +0 -25
  24. package/dist/src/adapters/http.d.ts.map +0 -1
  25. package/dist/src/adapters/http.js +0 -99
  26. package/dist/src/adapters/http.js.map +0 -1
  27. package/dist/src/adapters/index.d.ts +0 -11
  28. package/dist/src/adapters/index.d.ts.map +0 -1
  29. package/dist/src/adapters/index.js +0 -10
  30. package/dist/src/adapters/index.js.map +0 -1
  31. package/dist/src/adapters/mcp.d.ts +0 -24
  32. package/dist/src/adapters/mcp.d.ts.map +0 -1
  33. package/dist/src/adapters/mcp.js +0 -75
  34. package/dist/src/adapters/mcp.js.map +0 -1
  35. package/dist/src/adapters/next.d.ts +0 -24
  36. package/dist/src/adapters/next.d.ts.map +0 -1
  37. package/dist/src/adapters/next.js +0 -109
  38. package/dist/src/adapters/next.js.map +0 -1
  39. package/dist/src/client.d.ts +0 -58
  40. package/dist/src/client.d.ts.map +0 -1
  41. package/dist/src/client.js +0 -495
  42. package/dist/src/client.js.map +0 -1
  43. package/dist/src/edge.d.ts +0 -22
  44. package/dist/src/edge.d.ts.map +0 -1
  45. package/dist/src/edge.js +0 -91
  46. package/dist/src/edge.js.map +0 -1
  47. package/dist/src/factory.d.ts +0 -605
  48. package/dist/src/factory.d.ts.map +0 -1
  49. package/dist/src/factory.js +0 -215
  50. package/dist/src/factory.js.map +0 -1
  51. package/dist/src/helpers/auth.d.ts +0 -47
  52. package/dist/src/helpers/auth.d.ts.map +0 -1
  53. package/dist/src/helpers/auth.js +0 -73
  54. package/dist/src/helpers/auth.js.map +0 -1
  55. package/dist/src/helpers/checkout.d.ts +0 -45
  56. package/dist/src/helpers/checkout.d.ts.map +0 -1
  57. package/dist/src/helpers/checkout.js +0 -89
  58. package/dist/src/helpers/checkout.js.map +0 -1
  59. package/dist/src/helpers/customer.d.ts +0 -51
  60. package/dist/src/helpers/customer.d.ts.map +0 -1
  61. package/dist/src/helpers/customer.js +0 -77
  62. package/dist/src/helpers/customer.js.map +0 -1
  63. package/dist/src/helpers/error.d.ts +0 -15
  64. package/dist/src/helpers/error.d.ts.map +0 -1
  65. package/dist/src/helpers/error.js +0 -35
  66. package/dist/src/helpers/error.js.map +0 -1
  67. package/dist/src/helpers/index.d.ts +0 -17
  68. package/dist/src/helpers/index.d.ts.map +0 -1
  69. package/dist/src/helpers/index.js +0 -23
  70. package/dist/src/helpers/index.js.map +0 -1
  71. package/dist/src/helpers/payment.d.ts +0 -107
  72. package/dist/src/helpers/payment.d.ts.map +0 -1
  73. package/dist/src/helpers/payment.js +0 -150
  74. package/dist/src/helpers/payment.js.map +0 -1
  75. package/dist/src/helpers/plans.d.ts +0 -19
  76. package/dist/src/helpers/plans.d.ts.map +0 -1
  77. package/dist/src/helpers/plans.js +0 -53
  78. package/dist/src/helpers/plans.js.map +0 -1
  79. package/dist/src/helpers/renewal.d.ts +0 -23
  80. package/dist/src/helpers/renewal.d.ts.map +0 -1
  81. package/dist/src/helpers/renewal.js +0 -90
  82. package/dist/src/helpers/renewal.js.map +0 -1
  83. package/dist/src/helpers/subscription.d.ts +0 -23
  84. package/dist/src/helpers/subscription.d.ts.map +0 -1
  85. package/dist/src/helpers/subscription.js +0 -101
  86. package/dist/src/helpers/subscription.js.map +0 -1
  87. package/dist/src/helpers/types.d.ts +0 -22
  88. package/dist/src/helpers/types.d.ts.map +0 -1
  89. package/dist/src/helpers/types.js +0 -7
  90. package/dist/src/helpers/types.js.map +0 -1
  91. package/dist/src/index.d.ts +0 -73
  92. package/dist/src/index.d.ts.map +0 -1
  93. package/dist/src/index.js +0 -106
  94. package/dist/src/index.js.map +0 -1
  95. package/dist/src/mcp/auth-bridge.d.ts +0 -9
  96. package/dist/src/mcp/auth-bridge.d.ts.map +0 -1
  97. package/dist/src/mcp/auth-bridge.js +0 -46
  98. package/dist/src/mcp/auth-bridge.js.map +0 -1
  99. package/dist/src/mcp/oauth-bridge.d.ts +0 -48
  100. package/dist/src/mcp/oauth-bridge.d.ts.map +0 -1
  101. package/dist/src/mcp/oauth-bridge.js +0 -110
  102. package/dist/src/mcp/oauth-bridge.js.map +0 -1
  103. package/dist/src/mcp-auth.d.ts +0 -17
  104. package/dist/src/mcp-auth.d.ts.map +0 -1
  105. package/dist/src/mcp-auth.js +0 -57
  106. package/dist/src/mcp-auth.js.map +0 -1
  107. package/dist/src/paywall.d.ts +0 -119
  108. package/dist/src/paywall.d.ts.map +0 -1
  109. package/dist/src/paywall.js +0 -700
  110. package/dist/src/paywall.js.map +0 -1
  111. package/dist/src/register-virtual-tools-mcp.d.ts +0 -23
  112. package/dist/src/register-virtual-tools-mcp.d.ts.map +0 -1
  113. package/dist/src/register-virtual-tools-mcp.js +0 -86
  114. package/dist/src/register-virtual-tools-mcp.js.map +0 -1
  115. package/dist/src/types/client.d.ts +0 -216
  116. package/dist/src/types/client.d.ts.map +0 -1
  117. package/dist/src/types/client.js +0 -7
  118. package/dist/src/types/client.js.map +0 -1
  119. package/dist/src/types/generated.d.ts +0 -2834
  120. package/dist/src/types/generated.d.ts.map +0 -1
  121. package/dist/src/types/generated.js +0 -6
  122. package/dist/src/types/generated.js.map +0 -1
  123. package/dist/src/types/index.d.ts +0 -13
  124. package/dist/src/types/index.d.ts.map +0 -1
  125. package/dist/src/types/index.js +0 -8
  126. package/dist/src/types/index.js.map +0 -1
  127. package/dist/src/types/options.d.ts +0 -126
  128. package/dist/src/types/options.d.ts.map +0 -1
  129. package/dist/src/types/options.js +0 -8
  130. package/dist/src/types/options.js.map +0 -1
  131. package/dist/src/types/paywall.d.ts +0 -64
  132. package/dist/src/types/paywall.d.ts.map +0 -1
  133. package/dist/src/types/paywall.js +0 -7
  134. package/dist/src/types/paywall.js.map +0 -1
  135. package/dist/src/types/webhook.d.ts +0 -50
  136. package/dist/src/types/webhook.d.ts.map +0 -1
  137. package/dist/src/types/webhook.js +0 -2
  138. package/dist/src/types/webhook.js.map +0 -1
  139. package/dist/src/utils.d.ts +0 -110
  140. package/dist/src/utils.d.ts.map +0 -1
  141. package/dist/src/utils.js +0 -217
  142. package/dist/src/utils.js.map +0 -1
  143. package/dist/src/virtual-tools.d.ts +0 -44
  144. package/dist/src/virtual-tools.d.ts.map +0 -1
  145. package/dist/src/virtual-tools.js +0 -140
  146. package/dist/src/virtual-tools.js.map +0 -1
  147. package/dist/tsconfig.tsbuildinfo +0 -1
  148. package/dist/webapi-K5XBCEO6.js +0 -3775
@@ -1,700 +0,0 @@
1
- /**
2
- * SolvaPay SDK - Universal Paywall Protection
3
- *
4
- * One API that works everywhere:
5
- * - HTTP frameworks (Fastify, Express)
6
- * - MCP servers
7
- * - Class-based and functional programming
8
- */
9
- import { withRetry, createRequestDeduplicator } from './utils';
10
- /**
11
- * Error thrown when a paywall is triggered (purchase required or usage limit exceeded).
12
- *
13
- * This error is automatically thrown by the paywall protection system when:
14
- * - Customer doesn't have required purchase
15
- * - Customer has exceeded usage limits
16
- * - Customer needs to upgrade their plan
17
- *
18
- * The error includes structured content with checkout URLs and metadata for
19
- * building custom paywall UIs.
20
- *
21
- * @example
22
- * ```typescript
23
- * import { PaywallError } from '@solvapay/server';
24
- *
25
- * try {
26
- * const result = await payable.http(createTask)(req, res);
27
- * return result;
28
- * } catch (error) {
29
- * if (error instanceof PaywallError) {
30
- * // Custom paywall handling
31
- * return res.status(402).json({
32
- * error: error.message,
33
- * checkoutUrl: error.structuredContent.checkoutUrl,
34
- * // Additional metadata available in error.structuredContent
35
- * });
36
- * }
37
- * throw error;
38
- * }
39
- * ```
40
- *
41
- * @see {@link PaywallStructuredContent} for the structured content format
42
- * @since 1.0.0
43
- */
44
- export class PaywallError extends Error {
45
- structuredContent;
46
- /**
47
- * Creates a new PaywallError instance.
48
- *
49
- * @param message - Error message
50
- * @param structuredContent - Structured content with checkout URLs and metadata
51
- */
52
- constructor(message, structuredContent) {
53
- super(message);
54
- this.structuredContent = structuredContent;
55
- this.name = 'PaywallError';
56
- }
57
- }
58
- /** JSON body shape for HTTP adapters and MCP text content (stable fields for clients). */
59
- export function paywallErrorToClientPayload(error) {
60
- const sc = error.structuredContent;
61
- const base = {
62
- success: false,
63
- error: sc.kind === 'activation_required' ? 'Activation required' : 'Payment required',
64
- product: sc.product,
65
- checkoutUrl: sc.checkoutUrl,
66
- message: sc.message,
67
- };
68
- if (sc.kind === 'activation_required') {
69
- base.kind = 'activation_required';
70
- if (sc.plans !== undefined)
71
- base.plans = sc.plans;
72
- if (sc.balance !== undefined)
73
- base.balance = sc.balance;
74
- if (sc.productDetails !== undefined)
75
- base.productDetails = sc.productDetails;
76
- if (sc.confirmationUrl !== undefined)
77
- base.confirmationUrl = sc.confirmationUrl;
78
- }
79
- return base;
80
- }
81
- /**
82
- * Shared customer lookup deduplicator across all SolvaPay instances
83
- *
84
- * This prevents duplicate customer lookups when multiple SolvaPay instances
85
- * are created in the same process (e.g., in different API routes).
86
- *
87
- * Features:
88
- * - Deduplicates concurrent requests (multiple requests share the same promise)
89
- * - Caches results for 60 seconds (prevents duplicate sequential requests)
90
- * - Automatic cleanup of expired cache entries
91
- * - Memory-safe with max cache size
92
- */
93
- const sharedCustomerLookupDeduplicator = createRequestDeduplicator({
94
- cacheTTL: 60000, // Cache results for 60 seconds (reduces API calls significantly)
95
- maxCacheSize: 1000, // Maximum cache entries
96
- cacheErrors: false, // Don't cache errors - retry on next request
97
- });
98
- /**
99
- * Universal SolvaPay Protection - One API for everything
100
- */
101
- export class SolvaPayPaywall {
102
- apiClient;
103
- customerCreationAttempts = new Set();
104
- customerRefMapping = new Map();
105
- debug;
106
- limitsCache = new Map();
107
- limitsCacheTTL;
108
- constructor(apiClient, options = {}) {
109
- this.apiClient = apiClient;
110
- this.debug = options.debug ?? process.env.SOLVAPAY_DEBUG === 'true';
111
- this.limitsCacheTTL = options.limitsCacheTTL ?? 10_000;
112
- }
113
- log(...args) {
114
- if (this.debug) {
115
- // eslint-disable-next-line no-console
116
- console.log(...args);
117
- }
118
- }
119
- resolveProduct(metadata) {
120
- return metadata.product || process.env.SOLVAPAY_PRODUCT || 'default-product';
121
- }
122
- generateRequestId() {
123
- const timestamp = Date.now();
124
- const random = Math.random().toString(36).substring(2, 11);
125
- return `solvapay_${timestamp}_${random}`;
126
- }
127
- /**
128
- * Core protection method - works for both MCP and HTTP
129
- */
130
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
131
- async protect(handler, metadata = {}, getCustomerRef) {
132
- const product = this.resolveProduct(metadata);
133
- const configuredPlanRef = metadata.plan?.trim();
134
- const usagePlanRef = configuredPlanRef || 'unspecified';
135
- const usageType = metadata.usageType || 'requests';
136
- return async (args) => {
137
- const startTime = Date.now();
138
- const requestId = this.generateRequestId();
139
- const inputCustomerRef = getCustomerRef
140
- ? getCustomerRef(args)
141
- : args.auth?.customer_ref || 'anonymous';
142
- // Auto-create customer if needed and get the backend reference
143
- // Pass inputCustomerRef as both customerRef (cache key) and externalRef (for backend lookup)
144
- let backendCustomerRef;
145
- // If the input ref is already a SolvaPay customer ID (starts with 'cus_'),
146
- // use it directly without attempting lookup/creation by externalRef.
147
- if (inputCustomerRef.startsWith('cus_')) {
148
- backendCustomerRef = inputCustomerRef;
149
- }
150
- else {
151
- // Auto-create customer if needed and get the backend reference
152
- // Pass inputCustomerRef as both customerRef (cache key) and externalRef (for backend lookup)
153
- backendCustomerRef = await this.ensureCustomer(inputCustomerRef, inputCustomerRef);
154
- }
155
- let resolvedMeterName;
156
- try {
157
- const limitsCacheKey = `${backendCustomerRef}:${product}:${configuredPlanRef || ''}:${usageType}`;
158
- const cachedLimits = this.limitsCache.get(limitsCacheKey);
159
- const now = Date.now();
160
- let withinLimits;
161
- let remaining;
162
- let checkoutUrl;
163
- let lastLimitsCheck;
164
- const hasFreshCachedLimits = cachedLimits && now - cachedLimits.timestamp < this.limitsCacheTTL;
165
- if (hasFreshCachedLimits) {
166
- checkoutUrl = cachedLimits.checkoutUrl;
167
- resolvedMeterName = cachedLimits.meterName;
168
- if (cachedLimits.remaining > 0) {
169
- cachedLimits.remaining--;
170
- if (cachedLimits.remaining <= 0) {
171
- this.limitsCache.delete(limitsCacheKey);
172
- }
173
- withinLimits = true;
174
- remaining = cachedLimits.remaining;
175
- }
176
- else {
177
- // A zero-remaining entry indicates we already consumed the final unit.
178
- // Block one immediate follow-up request from cache, then force re-check next time.
179
- withinLimits = false;
180
- remaining = 0;
181
- this.limitsCache.delete(limitsCacheKey);
182
- }
183
- }
184
- else {
185
- if (cachedLimits) {
186
- this.limitsCache.delete(limitsCacheKey);
187
- }
188
- const limitsCheck = await this.apiClient.checkLimits({
189
- customerRef: backendCustomerRef,
190
- productRef: product,
191
- ...(configuredPlanRef ? { planRef: configuredPlanRef } : {}),
192
- meterName: usageType,
193
- });
194
- lastLimitsCheck = limitsCheck;
195
- withinLimits = limitsCheck.withinLimits;
196
- remaining = limitsCheck.remaining;
197
- checkoutUrl = limitsCheck.checkoutUrl;
198
- resolvedMeterName = limitsCheck.meterName;
199
- const consumedAllowance = withinLimits && remaining > 0;
200
- if (consumedAllowance) {
201
- // checkLimits reflects pre-request allowance. Consume one unit for this in-flight request
202
- // so cached follow-up requests don't get an extra free call.
203
- remaining = Math.max(0, remaining - 1);
204
- }
205
- if (consumedAllowance) {
206
- this.limitsCache.set(limitsCacheKey, {
207
- remaining,
208
- checkoutUrl,
209
- meterName: resolvedMeterName,
210
- timestamp: now,
211
- });
212
- }
213
- }
214
- if (!withinLimits) {
215
- const latencyMs = Date.now() - startTime;
216
- this.trackUsage(backendCustomerRef, product, usagePlanRef, resolvedMeterName || usageType, 'paywall', requestId, latencyMs);
217
- if (lastLimitsCheck?.activationRequired) {
218
- const confirmationUrl = lastLimitsCheck.confirmationUrl;
219
- const payCheckoutUrl = confirmationUrl || lastLimitsCheck.checkoutUrl || checkoutUrl || '';
220
- throw new PaywallError('Activation required', {
221
- kind: 'activation_required',
222
- product,
223
- message: 'Product activation is required before this tool can be used.',
224
- checkoutUrl: payCheckoutUrl,
225
- confirmationUrl,
226
- plans: lastLimitsCheck.plans,
227
- balance: lastLimitsCheck.balance,
228
- productDetails: lastLimitsCheck.product,
229
- });
230
- }
231
- throw new PaywallError('Payment required', {
232
- kind: 'payment_required',
233
- product,
234
- checkoutUrl: checkoutUrl || '',
235
- message: `Purchase required. Remaining: ${remaining}`,
236
- });
237
- }
238
- const result = await handler(args);
239
- const latencyMs = Date.now() - startTime;
240
- this.trackUsage(backendCustomerRef, product, usagePlanRef, resolvedMeterName || usageType, 'success', requestId, latencyMs);
241
- return result;
242
- }
243
- catch (error) {
244
- if (error instanceof Error) {
245
- const errorType = error instanceof PaywallError ? 'PaywallError' : 'API Error';
246
- this.log(`❌ Error in paywall [${errorType}]: ${error.message}`);
247
- }
248
- else {
249
- this.log(`❌ Error in paywall:`, error);
250
- }
251
- if (!(error instanceof PaywallError)) {
252
- const latencyMs = Date.now() - startTime;
253
- this.trackUsage(backendCustomerRef, product, usagePlanRef, resolvedMeterName || usageType, 'fail', requestId, latencyMs);
254
- }
255
- throw error;
256
- }
257
- };
258
- }
259
- /**
260
- * Ensures a customer exists in the backend, creating them if necessary.
261
- * This is a public helper for testing, pre-creating customers, and internal use.
262
- * Only attempts creation once per customer (idempotent).
263
- * Returns the backend customer reference to use in API calls.
264
- *
265
- * @param customerRef - The customer reference used as a cache key (e.g., Supabase user ID)
266
- * @param externalRef - Optional external reference for backend lookup (e.g., Supabase user ID)
267
- * If provided, will lookup existing customer by externalRef before creating new one.
268
- * The externalRef is stored on the SolvaPay backend for customer lookup.
269
- * @param options - Optional customer details (email, name) for customer creation
270
- */
271
- async ensureCustomer(customerRef, externalRef, options) {
272
- // Return cached mapping if exists (per-instance cache)
273
- if (this.customerRefMapping.has(customerRef)) {
274
- return this.customerRefMapping.get(customerRef);
275
- }
276
- // Skip for anonymous users
277
- if (customerRef === 'anonymous') {
278
- return customerRef;
279
- }
280
- // If customerRef is already a SolvaPay ID (starts with 'cus_'),
281
- // return it directly. We cannot "ensure" (create) a customer with a specific ID,
282
- // and using it as an externalRef causes issues.
283
- if (customerRef.startsWith('cus_')) {
284
- return customerRef;
285
- }
286
- // Use shared deduplicator to prevent duplicate lookups across all instances
287
- // This is especially important when multiple routes call ensureCustomer concurrently
288
- const cacheKey = externalRef || customerRef;
289
- // Check if we have a cached result in per-instance cache first (fast path)
290
- if (this.customerRefMapping.has(customerRef)) {
291
- const cached = this.customerRefMapping.get(customerRef);
292
- return cached;
293
- }
294
- // Use shared deduplicator (handles both concurrent requests and cache)
295
- const backendRef = await sharedCustomerLookupDeduplicator.deduplicate(cacheKey, async () => {
296
- // If externalRef is provided, try to lookup existing customer first
297
- if (externalRef) {
298
- try {
299
- const existingCustomer = await this.apiClient.getCustomer({ externalRef });
300
- if (existingCustomer && existingCustomer.customerRef) {
301
- const ref = existingCustomer.customerRef;
302
- // Store the mapping for future use (per-instance cache)
303
- this.customerRefMapping.set(customerRef, ref);
304
- // Also track that we've attempted creation for this externalRef to prevent duplicates
305
- this.customerCreationAttempts.add(customerRef);
306
- if (externalRef !== customerRef) {
307
- this.customerCreationAttempts.add(externalRef);
308
- }
309
- return ref;
310
- }
311
- }
312
- catch (error) {
313
- // 404 means customer doesn't exist yet - this is expected, continue to creation
314
- const errorMessage = error instanceof Error ? error.message : String(error);
315
- if (!errorMessage.includes('404') && !errorMessage.includes('not found')) {
316
- // Unexpected error - log but continue to fallback behavior
317
- this.log(`⚠️ Error looking up customer by externalRef: ${errorMessage}`);
318
- }
319
- }
320
- }
321
- // If already attempted but no mapping, use original ref
322
- // Check both customerRef and externalRef to prevent duplicates
323
- if (this.customerCreationAttempts.has(customerRef) ||
324
- (externalRef && this.customerCreationAttempts.has(externalRef))) {
325
- // If we have a mapping, use it; otherwise return the original ref
326
- const mappedRef = this.customerRefMapping.get(customerRef);
327
- return mappedRef || customerRef;
328
- }
329
- // Skip if createCustomer is not available
330
- if (!this.apiClient.createCustomer) {
331
- console.warn(`⚠️ Cannot auto-create customer ${customerRef}: createCustomer method not available on API client`);
332
- return customerRef;
333
- }
334
- this.customerCreationAttempts.add(customerRef);
335
- try {
336
- // Prepare customer creation params
337
- // Use provided email/name, or fallback to auto-generated values
338
- // Use a timestamp-based email to avoid conflicts with old orphaned records
339
- const createParams = {
340
- email: options?.email || `${customerRef}-${Date.now()}@auto-created.local`,
341
- metadata: {},
342
- };
343
- if (options?.name) {
344
- createParams.name = options.name;
345
- }
346
- if (externalRef) {
347
- createParams.externalRef = externalRef;
348
- }
349
- const result = await this.apiClient.createCustomer(createParams);
350
- // Extract the backend reference from the response
351
- const resultObj = result;
352
- const ref = resultObj.customerRef || resultObj.reference || customerRef;
353
- // Store the mapping (per-instance cache)
354
- this.customerRefMapping.set(customerRef, ref);
355
- return ref;
356
- }
357
- catch (error) {
358
- const errorMessage = error instanceof Error ? error.message : String(error);
359
- if (errorMessage.includes('409') || errorMessage.includes('already exists')) {
360
- // Try to lookup by externalRef first if available
361
- if (externalRef) {
362
- try {
363
- const searchResult = await this.apiClient.getCustomer({ externalRef });
364
- if (searchResult && searchResult.customerRef) {
365
- this.customerRefMapping.set(customerRef, searchResult.customerRef);
366
- return searchResult.customerRef;
367
- }
368
- }
369
- catch (lookupError) {
370
- this.log(`⚠️ Failed to lookup existing customer by externalRef after 409:`, lookupError instanceof Error ? lookupError.message : lookupError);
371
- }
372
- }
373
- // If conflict is due to email uniqueness but externalRef lookup failed,
374
- // retry creation with a generated email while preserving externalRef.
375
- // This allows resolving stale customers created before externalRef was set.
376
- const isEmailConflict = errorMessage.includes('email') || errorMessage.includes('identifier email');
377
- if (externalRef && isEmailConflict && options?.email) {
378
- try {
379
- const byEmail = await this.apiClient.getCustomer({ email: options.email });
380
- if (byEmail && byEmail.customerRef) {
381
- this.customerRefMapping.set(customerRef, byEmail.customerRef);
382
- this.log(`⚠️ Resolved customer ${customerRef} by email after conflict; using existing customer ${byEmail.customerRef}`);
383
- return byEmail.customerRef;
384
- }
385
- }
386
- catch (emailLookupError) {
387
- this.log(`⚠️ Email lookup failed after customer conflict for ${customerRef}:`, emailLookupError instanceof Error ? emailLookupError.message : emailLookupError);
388
- }
389
- try {
390
- const retryParams = {
391
- email: `${customerRef}-${Date.now()}@auto-created.local`,
392
- externalRef,
393
- metadata: {},
394
- };
395
- if (options?.name) {
396
- retryParams.name = options.name;
397
- }
398
- const retryResult = await this.apiClient.createCustomer(retryParams);
399
- const retryObj = retryResult;
400
- const retryRef = retryObj.customerRef || retryObj.reference || customerRef;
401
- this.customerRefMapping.set(customerRef, retryRef);
402
- this.log(`⚠️ Retried customer creation for ${customerRef} with generated email after email conflict`);
403
- return retryRef;
404
- }
405
- catch (retryError) {
406
- this.log(`⚠️ Retry create customer with generated email failed for ${customerRef}:`, retryError instanceof Error ? retryError.message : retryError);
407
- }
408
- }
409
- // We have a known conflict but could not resolve the existing customer reference.
410
- // Returning the original app user ID here causes downstream 404s in payment APIs.
411
- const unresolvedMessage = errorMessage || 'Customer already exists but could not be resolved';
412
- throw new Error(`Failed to resolve existing customer for ${customerRef} after conflict: ${unresolvedMessage}. ` +
413
- 'Ensure the existing customer is linked to this externalRef.');
414
- }
415
- this.log(`❌ Failed to auto-create customer ${customerRef}:`, error instanceof Error ? error.message : error);
416
- throw error;
417
- }
418
- });
419
- // Store the mapping in per-instance cache for faster subsequent lookups
420
- if (backendRef !== customerRef) {
421
- this.customerRefMapping.set(customerRef, backendRef);
422
- }
423
- return backendRef;
424
- }
425
- async trackUsage(customerRef, _productRef, _planRef, action, outcome, requestId, actionDuration) {
426
- await withRetry(() => this.apiClient.trackUsage({
427
- customerRef,
428
- actionType: 'api_call',
429
- units: 1,
430
- outcome,
431
- productRef: _productRef,
432
- duration: actionDuration,
433
- metadata: { action: action || 'api_requests', requestId },
434
- timestamp: new Date().toISOString(),
435
- }), {
436
- maxRetries: 2,
437
- initialDelay: 500,
438
- shouldRetry: error => error.message.includes('Customer not found'),
439
- onRetry: (_error, attempt) => {
440
- console.warn(`⚠️ Customer not found (attempt ${attempt + 1}/3), retrying in 500ms...`);
441
- },
442
- }).catch(error => {
443
- console.error('Usage tracking failed:', error);
444
- });
445
- }
446
- }
447
- /**
448
- * Universal SolvaPay factory - One API for MCP and HTTP
449
- */
450
- export function createPaywall(config) {
451
- const paywall = new SolvaPayPaywall(config.apiClient);
452
- // Functional approach - works for both MCP and HTTP
453
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
454
- function protect(metadata = {}) {
455
- return function (handler) {
456
- return paywall.protect(handler, metadata);
457
- };
458
- }
459
- // Class-based decorator
460
- function Paywall(metadata = {}) {
461
- return function (target, propertyKey, descriptor) {
462
- // Handle both descriptor and direct property assignment
463
- const method = descriptor?.value || target[propertyKey];
464
- if (typeof method !== 'function') {
465
- throw new Error('@Paywall decorator can only be applied to methods');
466
- }
467
- // Store metadata on the method
468
- method._paywallMetadata = metadata;
469
- if (descriptor) {
470
- // Standard method decorator
471
- descriptor.value = method;
472
- return descriptor;
473
- }
474
- else {
475
- // Legacy decorator or direct property
476
- target[propertyKey] = method;
477
- return target;
478
- }
479
- };
480
- }
481
- function createHttpHandler(methodOrMetadata, handlerOrOptions) {
482
- if (typeof methodOrMetadata === 'function') {
483
- const method = methodOrMetadata;
484
- const metadata = method
485
- ._paywallMetadata;
486
- const options = handlerOrOptions;
487
- if (!metadata) {
488
- throw new Error('Method must be decorated with @Paywall');
489
- }
490
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
491
- return async (req, reply) => {
492
- try {
493
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
494
- const extractArgs = options?.extractArgs || defaultExtractArgs;
495
- const getCustomerRef =
496
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
497
- options?.getCustomerRef ||
498
- ((req) => req.auth?.customer_ref || 'anonymous');
499
- const args = extractArgs(req);
500
- const protectedMethod = await paywall.protect(method, metadata, getCustomerRef);
501
- const result = await protectedMethod(args);
502
- const transformResponse = options?.transformResponse ||
503
- ((result) => result);
504
- return transformResponse(result, reply);
505
- }
506
- catch (error) {
507
- return handleHttpError(error, reply);
508
- }
509
- };
510
- }
511
- const metadata = methodOrMetadata;
512
- const handler = handlerOrOptions;
513
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
514
- return async (req, reply) => {
515
- try {
516
- const args = defaultExtractArgs(req);
517
- const getCustomerRef = (args) => args.auth?.customer_ref || 'anonymous';
518
- const protectedHandler = await paywall.protect(handler, metadata, getCustomerRef);
519
- const result = await protectedHandler(args);
520
- // Handle Express response (has res.status) vs Fastify (has reply.code)
521
- if (reply && reply.status && typeof reply.json === 'function') {
522
- // Express: call res.json() and don't return a value
523
- reply.json(result);
524
- return;
525
- }
526
- // Fastify: return the result for auto-serialization
527
- return result;
528
- }
529
- catch (error) {
530
- return handleHttpError(error, reply);
531
- }
532
- };
533
- }
534
- function createMCPHandler(methodOrMetadata, handler) {
535
- if (typeof methodOrMetadata === 'function') {
536
- const method = methodOrMetadata;
537
- const metadata = method
538
- ._paywallMetadata;
539
- if (!metadata) {
540
- throw new Error('Method must be decorated with @Paywall');
541
- }
542
- const getCustomerRef = (args) => args.auth?.customer_ref || 'anonymous';
543
- return paywall.protect(method, metadata, getCustomerRef);
544
- }
545
- const metadata = methodOrMetadata;
546
- const getCustomerRef = (args) => args.auth?.customer_ref || 'anonymous';
547
- return paywall.protect(handler, metadata, getCustomerRef);
548
- }
549
- function createNextHandler(metadata, handler, options) {
550
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
551
- return async (request, context) => {
552
- try {
553
- const extractArgs = options?.extractArgs || defaultExtractNextArgs;
554
- const getCustomerRef = options?.getCustomerRef || defaultGetCustomerRef;
555
- const transformResponse = options?.transformResponse || ((result) => result);
556
- const args = await extractArgs(request, context);
557
- const customerRef = await getCustomerRef(request);
558
- args.auth = { customer_ref: customerRef };
559
- const protectedHandler = await paywall.protect(handler, metadata, (args) => args.auth?.customer_ref || 'anonymous');
560
- const result = await protectedHandler(args);
561
- const transformedResult = transformResponse(result);
562
- return new Response(JSON.stringify(transformedResult), {
563
- headers: { 'Content-Type': 'application/json' },
564
- });
565
- }
566
- catch (error) {
567
- return handleNextError(error);
568
- }
569
- };
570
- }
571
- return {
572
- protect, // Function wrapper
573
- Paywall, // Class decorator
574
- createHttpHandler,
575
- createMCPHandler,
576
- createNextHandler, // Next.js API routes
577
- ensureCustomer: (customerRef) => paywall.ensureCustomer(customerRef), // Customer creation helper
578
- paywall, // Low-level access
579
- };
580
- }
581
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
582
- function defaultExtractArgs(req) {
583
- return {
584
- ...(req.body || {}),
585
- ...(req.params || {}),
586
- ...(req.query || {}),
587
- auth: { customer_ref: req.headers?.['x-customer-ref'] || req.auth?.customer_ref },
588
- };
589
- }
590
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
591
- function handleHttpError(error, reply) {
592
- if (error instanceof PaywallError) {
593
- const errorResponse = paywallErrorToClientPayload(error);
594
- // Express (has reply.status)
595
- if (reply && reply.status && typeof reply.json === 'function') {
596
- reply.status(402).json(errorResponse);
597
- return;
598
- }
599
- // Fastify (has reply.code)
600
- if (reply && reply.code) {
601
- reply.code(402);
602
- }
603
- return errorResponse;
604
- }
605
- const errorResponse = {
606
- success: false,
607
- error: error instanceof Error ? error.message : 'Internal server error',
608
- };
609
- // Express (has reply.status)
610
- if (reply && reply.status && typeof reply.json === 'function') {
611
- reply.status(500).json(errorResponse);
612
- return;
613
- }
614
- // Fastify (has reply.code)
615
- if (reply && reply.code) {
616
- reply.code(500);
617
- }
618
- return errorResponse;
619
- }
620
- // Next.js helper functions
621
- async function defaultExtractNextArgs(request, context) {
622
- const url = new URL(request.url);
623
- const query = Object.fromEntries(url.searchParams.entries());
624
- // Parse request body if present
625
- let body = {};
626
- try {
627
- if (request.method !== 'GET' &&
628
- request.headers.get('content-type')?.includes('application/json')) {
629
- body = await request.json();
630
- }
631
- }
632
- catch {
633
- // If parsing fails, continue with empty body
634
- }
635
- let routeParams = {};
636
- if (context?.params) {
637
- const params = context.params;
638
- if (typeof params === 'object' && params !== null && 'then' in params) {
639
- routeParams = (await params) || {};
640
- }
641
- else {
642
- routeParams = params || {};
643
- }
644
- }
645
- return {
646
- ...body,
647
- ...query,
648
- ...routeParams,
649
- };
650
- }
651
- async function defaultGetCustomerRef(request) {
652
- // Try to get from JWT token first
653
- const authHeader = request.headers.get('authorization');
654
- if (authHeader && authHeader.startsWith('Bearer ')) {
655
- try {
656
- // Dynamic import to avoid requiring jose if not used
657
- const { jwtVerify } = await import('jose');
658
- const token = authHeader.substring(7);
659
- const jwtSecret = new TextEncoder().encode(process.env.OAUTH_JWKS_SECRET);
660
- const { payload } = await jwtVerify(token, jwtSecret, {
661
- issuer: process.env.OAUTH_ISSUER,
662
- audience: process.env.OAUTH_CLIENT_ID || 'test-client-id',
663
- });
664
- if (payload.sub) {
665
- return ensureCustomerRef(payload.sub);
666
- }
667
- }
668
- catch {
669
- // Fall through to use header fallback
670
- }
671
- }
672
- // Fallback to x-customer-ref header or default
673
- const customerRef = request.headers.get('x-customer-ref') || 'demo_user';
674
- return ensureCustomerRef(customerRef);
675
- }
676
- export function ensureCustomerRef(customerRef) {
677
- // Ensure customer ref is properly formatted
678
- // Return customer ref as-is (preserve UUIDs with hyphens, etc.)
679
- if (!customerRef || customerRef === 'anonymous') {
680
- return 'anonymous';
681
- }
682
- return customerRef;
683
- }
684
- function handleNextError(error) {
685
- if (error instanceof PaywallError) {
686
- return new Response(JSON.stringify(paywallErrorToClientPayload(error)), {
687
- status: 402,
688
- headers: { 'Content-Type': 'application/json' },
689
- });
690
- }
691
- return new Response(JSON.stringify({
692
- success: false,
693
- error: error instanceof Error ? error.message : 'Internal server error',
694
- }), {
695
- status: 500,
696
- headers: { 'Content-Type': 'application/json' },
697
- });
698
- }
699
- // All exports are already defined above where each item is declared
700
- //# sourceMappingURL=paywall.js.map