@solvapay/server 2.4.1 → 2.5.0-preview-4dcf60c628500a6210ba69edbf7ae56ee47fbb3a

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/edge.js CHANGED
@@ -3,6 +3,32 @@ import { SolvaPayError as SolvaPayError7 } from "@solvapay/core";
3
3
 
4
4
  // src/client.ts
5
5
  import { SolvaPayError } from "@solvapay/core";
6
+ var API_ERROR_BODY_MAX = 240;
7
+ function apiDebugLog(...args) {
8
+ if (process.env.SOLVAPAY_DEBUG === "true") {
9
+ console.log(...args);
10
+ }
11
+ }
12
+ function isNonJsonResponseBody(body, contentType) {
13
+ const ct = contentType?.split(";")[0]?.trim().toLowerCase() ?? "";
14
+ if (ct.includes("json")) return false;
15
+ const trimmed = body.trimStart();
16
+ if (trimmed.startsWith("<")) return true;
17
+ if (ct && !ct.includes("json")) return true;
18
+ return false;
19
+ }
20
+ async function throwApiError(operation, res) {
21
+ const body = await res.text();
22
+ const contentType = res.headers.get("content-type");
23
+ const nonJson = isNonJsonResponseBody(body, contentType);
24
+ const snippet = body.length > API_ERROR_BODY_MAX ? `${body.slice(0, API_ERROR_BODY_MAX)}\u2026` : body;
25
+ const detail = nonJson ? `non-JSON response (${contentType ?? "unknown content-type"}) \u2014 the API may not have been reached. Body: ${snippet}` : snippet;
26
+ apiDebugLog(`\u274C API Error: ${res.status} - ${detail}`);
27
+ throw new SolvaPayError(`${operation} failed (${res.status}): ${detail}`, {
28
+ status: res.status,
29
+ code: nonJson ? "non_json_response" : void 0
30
+ });
31
+ }
6
32
  function createSolvaPayClient(opts) {
7
33
  const base = opts.apiBaseUrl ?? "https://api.solvapay.com";
8
34
  if (!opts.apiKey) throw new SolvaPayError("Missing apiKey");
@@ -10,12 +36,6 @@ function createSolvaPayClient(opts) {
10
36
  "Content-Type": "application/json",
11
37
  Authorization: `Bearer ${opts.apiKey}`
12
38
  };
13
- const debug = process.env.SOLVAPAY_DEBUG === "true";
14
- const log = (...args) => {
15
- if (debug) {
16
- console.log(...args);
17
- }
18
- };
19
39
  return {
20
40
  // POST: /v1/sdk/limits
21
41
  async checkLimits(params) {
@@ -26,11 +46,7 @@ function createSolvaPayClient(opts) {
26
46
  body: JSON.stringify(params)
27
47
  });
28
48
  if (!res.ok) {
29
- const error = await res.text();
30
- log(`\u274C API Error: ${res.status} - ${error}`);
31
- throw new SolvaPayError(`Check limits failed (${res.status}): ${error}`, {
32
- status: res.status
33
- });
49
+ await throwApiError("Check limits", res);
34
50
  }
35
51
  const result = await res.json();
36
52
  return result;
@@ -44,11 +60,7 @@ function createSolvaPayClient(opts) {
44
60
  body: JSON.stringify(params)
45
61
  });
46
62
  if (!res.ok) {
47
- const error = await res.text();
48
- log(`\u274C API Error: ${res.status} - ${error}`);
49
- throw new SolvaPayError(`Track usage failed (${res.status}): ${error}`, {
50
- status: res.status
51
- });
63
+ await throwApiError("Track usage", res);
52
64
  }
53
65
  return await res.json();
54
66
  },
@@ -61,11 +73,7 @@ function createSolvaPayClient(opts) {
61
73
  body: JSON.stringify(params)
62
74
  });
63
75
  if (!res.ok) {
64
- const error = await res.text();
65
- log(`\u274C API Error: ${res.status} - ${error}`);
66
- throw new SolvaPayError(`Track usage bulk failed (${res.status}): ${error}`, {
67
- status: res.status
68
- });
76
+ await throwApiError("Track usage bulk", res);
69
77
  }
70
78
  return await res.json();
71
79
  },
@@ -78,11 +86,7 @@ function createSolvaPayClient(opts) {
78
86
  body: JSON.stringify(params)
79
87
  });
80
88
  if (!res.ok) {
81
- const error = await res.text();
82
- log(`\u274C API Error: ${res.status} - ${error}`);
83
- throw new SolvaPayError(`Create customer failed (${res.status}): ${error}`, {
84
- status: res.status
85
- });
89
+ await throwApiError("Create customer", res);
86
90
  }
87
91
  const result = await res.json();
88
92
  return {
@@ -98,11 +102,7 @@ function createSolvaPayClient(opts) {
98
102
  body: JSON.stringify(params)
99
103
  });
100
104
  if (!res.ok) {
101
- const error = await res.text();
102
- log(`\u274C API Error: ${res.status} - ${error}`);
103
- throw new SolvaPayError(`Update customer failed (${res.status}): ${error}`, {
104
- status: res.status
105
- });
105
+ await throwApiError("Update customer", res);
106
106
  }
107
107
  const result = await res.json();
108
108
  return {
@@ -130,11 +130,7 @@ function createSolvaPayClient(opts) {
130
130
  headers
131
131
  });
132
132
  if (!res.ok) {
133
- const error = await res.text();
134
- log(`\u274C API Error: ${res.status} - ${error}`);
135
- throw new SolvaPayError(`Get customer failed (${res.status}): ${error}`, {
136
- status: res.status
137
- });
133
+ await throwApiError("Get customer", res);
138
134
  }
139
135
  const result = await res.json();
140
136
  let customer = result;
@@ -168,11 +164,7 @@ function createSolvaPayClient(opts) {
168
164
  body: JSON.stringify(body)
169
165
  });
170
166
  if (!res.ok) {
171
- const error = await res.text();
172
- log(`\u274C API Error: ${res.status} - ${error}`);
173
- throw new SolvaPayError(`Assign credits failed (${res.status}): ${error}`, {
174
- status: res.status
175
- });
167
+ await throwApiError("Assign credits", res);
176
168
  }
177
169
  return await res.json();
178
170
  },
@@ -184,11 +176,7 @@ function createSolvaPayClient(opts) {
184
176
  headers
185
177
  });
186
178
  if (!res.ok) {
187
- const error = await res.text();
188
- log(`\u274C API Error: ${res.status} - ${error}`);
189
- throw new SolvaPayError(`Get merchant failed (${res.status}): ${error}`, {
190
- status: res.status
191
- });
179
+ await throwApiError("Get merchant", res);
192
180
  }
193
181
  return res.json();
194
182
  },
@@ -200,11 +188,7 @@ function createSolvaPayClient(opts) {
200
188
  headers
201
189
  });
202
190
  if (!res.ok) {
203
- const error = await res.text();
204
- log(`\u274C API Error: ${res.status} - ${error}`);
205
- throw new SolvaPayError(`Get platform config failed (${res.status}): ${error}`, {
206
- status: res.status
207
- });
191
+ await throwApiError("Get platform config", res);
208
192
  }
209
193
  return res.json();
210
194
  },
@@ -216,11 +200,7 @@ function createSolvaPayClient(opts) {
216
200
  headers
217
201
  });
218
202
  if (!res.ok) {
219
- const error = await res.text();
220
- log(`\u274C API Error: ${res.status} - ${error}`);
221
- throw new SolvaPayError(`Get product failed (${res.status}): ${error}`, {
222
- status: res.status
223
- });
203
+ await throwApiError("Get product", res);
224
204
  }
225
205
  const result = await res.json();
226
206
  const data = result.data || {};
@@ -235,11 +215,7 @@ function createSolvaPayClient(opts) {
235
215
  headers
236
216
  });
237
217
  if (!res.ok) {
238
- const error = await res.text();
239
- log(`\u274C API Error: ${res.status} - ${error}`);
240
- throw new SolvaPayError(`List products failed (${res.status}): ${error}`, {
241
- status: res.status
242
- });
218
+ await throwApiError("List products", res);
243
219
  }
244
220
  const result = await res.json();
245
221
  const products = Array.isArray(result) ? result : result.products || [];
@@ -257,11 +233,7 @@ function createSolvaPayClient(opts) {
257
233
  body: JSON.stringify(params)
258
234
  });
259
235
  if (!res.ok) {
260
- const error = await res.text();
261
- log(`\u274C API Error: ${res.status} - ${error}`);
262
- throw new SolvaPayError(`Create product failed (${res.status}): ${error}`, {
263
- status: res.status
264
- });
236
+ await throwApiError("Create product", res);
265
237
  }
266
238
  const result = await res.json();
267
239
  return result;
@@ -275,11 +247,7 @@ function createSolvaPayClient(opts) {
275
247
  body: JSON.stringify(params)
276
248
  });
277
249
  if (!res.ok) {
278
- const error = await res.text();
279
- log(`\u274C API Error: ${res.status} - ${error}`);
280
- throw new SolvaPayError(`Bootstrap MCP product failed (${res.status}): ${error}`, {
281
- status: res.status
282
- });
250
+ await throwApiError("Bootstrap MCP product", res);
283
251
  }
284
252
  return await res.json();
285
253
  },
@@ -292,11 +260,7 @@ function createSolvaPayClient(opts) {
292
260
  body: JSON.stringify(params)
293
261
  });
294
262
  if (!res.ok) {
295
- const error = await res.text();
296
- log(`\u274C API Error: ${res.status} - ${error}`);
297
- throw new SolvaPayError(`Configure MCP plans failed (${res.status}): ${error}`, {
298
- status: res.status
299
- });
263
+ await throwApiError("Configure MCP plans", res);
300
264
  }
301
265
  return await res.json();
302
266
  },
@@ -308,11 +272,7 @@ function createSolvaPayClient(opts) {
308
272
  headers
309
273
  });
310
274
  if (!res.ok && res.status !== 404) {
311
- const error = await res.text();
312
- log(`\u274C API Error: ${res.status} - ${error}`);
313
- throw new SolvaPayError(`Delete product failed (${res.status}): ${error}`, {
314
- status: res.status
315
- });
275
+ await throwApiError("Delete product", res);
316
276
  }
317
277
  },
318
278
  // POST: /v1/sdk/products/{productRef}/clone
@@ -324,11 +284,7 @@ function createSolvaPayClient(opts) {
324
284
  body: JSON.stringify(overrides || {})
325
285
  });
326
286
  if (!res.ok) {
327
- const error = await res.text();
328
- log(`\u274C API Error: ${res.status} - ${error}`);
329
- throw new SolvaPayError(`Clone product failed (${res.status}): ${error}`, {
330
- status: res.status
331
- });
287
+ await throwApiError("Clone product", res);
332
288
  }
333
289
  return await res.json();
334
290
  },
@@ -340,11 +296,7 @@ function createSolvaPayClient(opts) {
340
296
  headers
341
297
  });
342
298
  if (!res.ok) {
343
- const error = await res.text();
344
- log(`\u274C API Error: ${res.status} - ${error}`);
345
- throw new SolvaPayError(`List plans failed (${res.status}): ${error}`, {
346
- status: res.status
347
- });
299
+ await throwApiError("List plans", res);
348
300
  }
349
301
  const result = await res.json();
350
302
  const plans = Array.isArray(result) ? result : result.plans || [];
@@ -369,11 +321,7 @@ function createSolvaPayClient(opts) {
369
321
  body: JSON.stringify(params)
370
322
  });
371
323
  if (!res.ok) {
372
- const error = await res.text();
373
- log(`\u274C API Error: ${res.status} - ${error}`);
374
- throw new SolvaPayError(`Create plan failed (${res.status}): ${error}`, {
375
- status: res.status
376
- });
324
+ await throwApiError("Create plan", res);
377
325
  }
378
326
  const result = await res.json();
379
327
  return result;
@@ -387,11 +335,7 @@ function createSolvaPayClient(opts) {
387
335
  body: JSON.stringify(params)
388
336
  });
389
337
  if (!res.ok) {
390
- const error = await res.text();
391
- log(`\u274C API Error: ${res.status} - ${error}`);
392
- throw new SolvaPayError(`Update plan failed (${res.status}): ${error}`, {
393
- status: res.status
394
- });
338
+ await throwApiError("Update plan", res);
395
339
  }
396
340
  return await res.json();
397
341
  },
@@ -403,11 +347,7 @@ function createSolvaPayClient(opts) {
403
347
  headers
404
348
  });
405
349
  if (!res.ok && res.status !== 404) {
406
- const error = await res.text();
407
- log(`\u274C API Error: ${res.status} - ${error}`);
408
- throw new SolvaPayError(`Delete plan failed (${res.status}): ${error}`, {
409
- status: res.status
410
- });
350
+ await throwApiError("Delete plan", res);
411
351
  }
412
352
  },
413
353
  // POST: /payment-intents
@@ -428,11 +368,7 @@ function createSolvaPayClient(opts) {
428
368
  })
429
369
  });
430
370
  if (!res.ok) {
431
- const error = await res.text();
432
- log(`\u274C API Error: ${res.status} - ${error}`);
433
- throw new SolvaPayError(`Create payment intent failed (${res.status}): ${error}`, {
434
- status: res.status
435
- });
371
+ await throwApiError("Create payment intent", res);
436
372
  }
437
373
  return await res.json();
438
374
  },
@@ -456,11 +392,7 @@ function createSolvaPayClient(opts) {
456
392
  })
457
393
  });
458
394
  if (!res.ok) {
459
- const error = await res.text();
460
- log(`\u274C API Error: ${res.status} - ${error}`);
461
- throw new SolvaPayError(`Create topup payment intent failed (${res.status}): ${error}`, {
462
- status: res.status
463
- });
395
+ await throwApiError("Create topup payment intent", res);
464
396
  }
465
397
  return await res.json();
466
398
  },
@@ -480,11 +412,7 @@ function createSolvaPayClient(opts) {
480
412
  })
481
413
  });
482
414
  if (!res.ok) {
483
- const error = await res.text();
484
- log(`\u274C API Error: ${res.status} - ${error}`);
485
- throw new SolvaPayError(`Process payment failed (${res.status}): ${error}`, {
486
- status: res.status
487
- });
415
+ await throwApiError("Process payment", res);
488
416
  }
489
417
  const result = await res.json();
490
418
  return result;
@@ -505,54 +433,20 @@ function createSolvaPayClient(opts) {
505
433
  })
506
434
  });
507
435
  if (!res.ok) {
508
- const error = await res.text();
509
- log(`\u274C API Error: ${res.status} - ${error}`);
510
- throw new SolvaPayError(
511
- `Attach business details failed (${res.status}): ${error}`,
512
- { status: res.status }
513
- );
514
- }
515
- return await res.json();
516
- },
517
- // POST: /v1/sdk/purchases/{purchaseRef}/cancel
518
- async cancelPurchase(params) {
519
- const url = `${base}/v1/sdk/purchases/${params.purchaseRef}/cancel`;
520
- const requestOptions = {
521
- method: "POST",
522
- headers
523
- };
524
- if (params.reason) {
525
- requestOptions.body = JSON.stringify({ reason: params.reason });
526
- }
527
- const res = await fetch(url, requestOptions);
528
- if (!res.ok) {
529
- const error = await res.text();
530
- log(`\u274C API Error: ${res.status} - ${error}`);
531
- if (res.status === 404) {
532
- throw new SolvaPayError(`Purchase not found: ${error}`, { status: 404 });
533
- }
534
- if (res.status === 400) {
535
- throw new SolvaPayError(
536
- `Purchase cannot be cancelled or does not belong to provider: ${error}`,
537
- { status: 400 }
538
- );
539
- }
540
- throw new SolvaPayError(`Cancel purchase failed (${res.status}): ${error}`, {
541
- status: res.status
542
- });
436
+ await throwApiError("Cancel purchase", res);
543
437
  }
544
438
  const responseText = await res.text();
545
439
  let responseData;
546
440
  try {
547
441
  responseData = JSON.parse(responseText);
548
442
  } catch (parseError) {
549
- log(`\u274C Failed to parse response as JSON: ${parseError}`);
443
+ apiDebugLog(`\u274C Failed to parse response as JSON: ${parseError}`);
550
444
  throw new SolvaPayError(
551
445
  `Invalid JSON response from cancel purchase endpoint: ${responseText.substring(0, 200)}`
552
446
  );
553
447
  }
554
448
  if (!responseData || typeof responseData !== "object") {
555
- log(`\u274C Invalid response structure: ${JSON.stringify(responseData)}`);
449
+ apiDebugLog(`\u274C Invalid response structure: ${JSON.stringify(responseData)}`);
556
450
  throw new SolvaPayError(`Invalid response structure from cancel purchase endpoint`);
557
451
  }
558
452
  let result;
@@ -564,7 +458,7 @@ function createSolvaPayClient(opts) {
564
458
  result = responseData.purchase || responseData;
565
459
  }
566
460
  if (!result || typeof result !== "object") {
567
- log(`\u274C Invalid purchase data in response. Full response:`, responseData);
461
+ apiDebugLog(`\u274C Invalid purchase data in response. Full response:`, responseData);
568
462
  throw new SolvaPayError(`Invalid purchase data in cancel purchase response`);
569
463
  }
570
464
  return result;
@@ -577,30 +471,20 @@ function createSolvaPayClient(opts) {
577
471
  headers
578
472
  });
579
473
  if (!res.ok) {
580
- const error = await res.text();
581
- log(`\u274C API Error: ${res.status} - ${error}`);
582
- if (res.status === 404) {
583
- throw new SolvaPayError(`Purchase not found: ${error}`, { status: 404 });
584
- }
585
- if (res.status === 400) {
586
- throw new SolvaPayError(`Purchase cannot be reactivated: ${error}`, { status: 400 });
587
- }
588
- throw new SolvaPayError(`Reactivate purchase failed (${res.status}): ${error}`, {
589
- status: res.status
590
- });
474
+ await throwApiError("Reactivate purchase", res);
591
475
  }
592
476
  const responseText = await res.text();
593
477
  let responseData;
594
478
  try {
595
479
  responseData = JSON.parse(responseText);
596
480
  } catch (parseError) {
597
- log(`\u274C Failed to parse response as JSON: ${parseError}`);
481
+ apiDebugLog(`\u274C Failed to parse response as JSON: ${parseError}`);
598
482
  throw new SolvaPayError(
599
483
  `Invalid JSON response from reactivate purchase endpoint: ${responseText.substring(0, 200)}`
600
484
  );
601
485
  }
602
486
  if (!responseData || typeof responseData !== "object") {
603
- log(`\u274C Invalid response structure: ${JSON.stringify(responseData)}`);
487
+ apiDebugLog(`\u274C Invalid response structure: ${JSON.stringify(responseData)}`);
604
488
  throw new SolvaPayError(`Invalid response structure from reactivate purchase endpoint`);
605
489
  }
606
490
  let result;
@@ -612,7 +496,7 @@ function createSolvaPayClient(opts) {
612
496
  result = responseData.purchase || responseData;
613
497
  }
614
498
  if (!result || typeof result !== "object") {
615
- log(`\u274C Invalid purchase data in response. Full response:`, responseData);
499
+ apiDebugLog(`\u274C Invalid purchase data in response. Full response:`, responseData);
616
500
  throw new SolvaPayError(`Invalid purchase data in reactivate purchase response`);
617
501
  }
618
502
  return result;
@@ -626,11 +510,7 @@ function createSolvaPayClient(opts) {
626
510
  body: JSON.stringify(params)
627
511
  });
628
512
  if (!res.ok) {
629
- const error = await res.text();
630
- log(`\u274C API Error: ${res.status} - ${error}`);
631
- throw new SolvaPayError(`Get user info failed (${res.status}): ${error}`, {
632
- status: res.status
633
- });
513
+ await throwApiError("Get user info", res);
634
514
  }
635
515
  return await res.json();
636
516
  },
@@ -642,11 +522,7 @@ function createSolvaPayClient(opts) {
642
522
  headers
643
523
  });
644
524
  if (!res.ok) {
645
- const error = await res.text();
646
- log(`\u274C API Error: ${res.status} - ${error}`);
647
- throw new SolvaPayError(`Get customer balance failed (${res.status}): ${error}`, {
648
- status: res.status
649
- });
525
+ await throwApiError("Get customer balance", res);
650
526
  }
651
527
  return await res.json();
652
528
  },
@@ -659,11 +535,7 @@ function createSolvaPayClient(opts) {
659
535
  body: JSON.stringify(params)
660
536
  });
661
537
  if (!res.ok) {
662
- const error = await res.text();
663
- log(`\u274C API Error: ${res.status} - ${error}`);
664
- throw new SolvaPayError(`Create checkout session failed (${res.status}): ${error}`, {
665
- status: res.status
666
- });
538
+ await throwApiError("Create checkout session", res);
667
539
  }
668
540
  const result = await res.json();
669
541
  return result;
@@ -677,11 +549,7 @@ function createSolvaPayClient(opts) {
677
549
  body: JSON.stringify(params)
678
550
  });
679
551
  if (!res.ok) {
680
- const error = await res.text();
681
- log(`\u274C API Error: ${res.status} - ${error}`);
682
- throw new SolvaPayError(`Create customer session failed (${res.status}): ${error}`, {
683
- status: res.status
684
- });
552
+ await throwApiError("Create customer session", res);
685
553
  }
686
554
  const result = await res.json();
687
555
  return result;
@@ -695,11 +563,7 @@ function createSolvaPayClient(opts) {
695
563
  body: JSON.stringify(params)
696
564
  });
697
565
  if (!res.ok) {
698
- const error = await res.text();
699
- log(`\u274C API Error: ${res.status} - ${error}`);
700
- throw new SolvaPayError(`Activate plan failed (${res.status}): ${error}`, {
701
- status: res.status
702
- });
566
+ await throwApiError("Activate plan", res);
703
567
  }
704
568
  return await res.json();
705
569
  },
@@ -708,11 +572,7 @@ function createSolvaPayClient(opts) {
708
572
  url.searchParams.set("customerRef", params.customerRef);
709
573
  const res = await fetch(url.toString(), { method: "GET", headers });
710
574
  if (!res.ok) {
711
- const error = await res.text();
712
- log(`\u274C API Error: ${res.status} - ${error}`);
713
- throw new SolvaPayError(`Get payment method failed (${res.status}): ${error}`, {
714
- status: res.status
715
- });
575
+ await throwApiError("Get payment method", res);
716
576
  }
717
577
  return await res.json();
718
578
  },
@@ -721,11 +581,7 @@ function createSolvaPayClient(opts) {
721
581
  url.searchParams.set("customerRef", params.customerRef);
722
582
  const res = await fetch(url.toString(), { method: "GET", headers });
723
583
  if (!res.ok) {
724
- const error = await res.text();
725
- log(`\u274C API Error: ${res.status} - ${error}`);
726
- throw new SolvaPayError(`Get auto-recharge failed (${res.status}): ${error}`, {
727
- status: res.status
728
- });
584
+ await throwApiError("Get auto-recharge", res);
729
585
  }
730
586
  return await res.json();
731
587
  },
@@ -736,11 +592,7 @@ function createSolvaPayClient(opts) {
736
592
  body: JSON.stringify(params)
737
593
  });
738
594
  if (!res.ok) {
739
- const error = await res.text();
740
- log(`\u274C API Error: ${res.status} - ${error}`);
741
- throw new SolvaPayError(`Save auto-recharge failed (${res.status}): ${error}`, {
742
- status: res.status
743
- });
595
+ await throwApiError("Save auto-recharge", res);
744
596
  }
745
597
  return await res.json();
746
598
  },
@@ -749,11 +601,7 @@ function createSolvaPayClient(opts) {
749
601
  url.searchParams.set("customerRef", params.customerRef);
750
602
  const res = await fetch(url.toString(), { method: "DELETE", headers });
751
603
  if (!res.ok) {
752
- const error = await res.text();
753
- log(`\u274C API Error: ${res.status} - ${error}`);
754
- throw new SolvaPayError(`Disable auto-recharge failed (${res.status}): ${error}`, {
755
- status: res.status
756
- });
604
+ await throwApiError("Disable auto-recharge", res);
757
605
  }
758
606
  return await res.json();
759
607
  },
@@ -767,11 +615,7 @@ function createSolvaPayClient(opts) {
767
615
  }
768
616
  const res = await fetch(url.toString(), { method: "GET", headers });
769
617
  if (!res.ok) {
770
- const error = await res.text();
771
- log(`\u274C API Error: ${res.status} - ${error}`);
772
- throw new SolvaPayError(`List purchases failed (${res.status}): ${error}`, {
773
- status: res.status
774
- });
618
+ await throwApiError("List purchases", res);
775
619
  }
776
620
  const data = await res.json();
777
621
  return { purchases: data.purchases ?? [] };
@@ -782,11 +626,7 @@ function createSolvaPayClient(opts) {
782
626
  if (params.limit !== void 0) url.searchParams.set("limit", String(params.limit));
783
627
  const res = await fetch(url.toString(), { method: "GET", headers });
784
628
  if (!res.ok) {
785
- const error = await res.text();
786
- log(`\u274C API Error: ${res.status} - ${error}`);
787
- throw new SolvaPayError(`Get credit activity failed (${res.status}): ${error}`, {
788
- status: res.status
789
- });
629
+ await throwApiError("Get credit activity", res);
790
630
  }
791
631
  return await res.json();
792
632
  }
@@ -819,6 +659,12 @@ async function verifyProductConfiguration(options) {
819
659
  };
820
660
  } catch (error) {
821
661
  const status = error instanceof SolvaPayError2 ? error.status : void 0;
662
+ const code = error instanceof SolvaPayError2 ? error.code : void 0;
663
+ if (status === 404 && code === "non_json_response") {
664
+ throw new Error(
665
+ `Could not reach ${apiBaseUrl} while verifying SOLVAPAY_PRODUCT_REF "${productRef}": the server returned a non-JSON response (often an offline ngrok tunnel or proxy error page). Check SOLVAPAY_API_BASE_URL and that the API tunnel is running.`
666
+ );
667
+ }
822
668
  if (status === 404) {
823
669
  throw new Error(
824
670
  `SOLVAPAY_PRODUCT_REF "${productRef}" does not exist on ${apiBaseUrl}. OAuth dynamic client registration resolves the provider from this ref, so every connection attempt would fail with a 400 "Invalid identifier" and no MCP host could connect. Point SOLVAPAY_PRODUCT_REF at a product that exists on this account (SolvaPay Console \u2192 Products, or \`npx solvapay init\` / \`npx solvapay doctor\`), and check SOLVAPAY_API_BASE_URL is the environment you created the product in.`
@@ -837,46 +683,92 @@ async function verifyProductConfiguration(options) {
837
683
  }
838
684
 
839
685
  // src/paywall-state.ts
686
+ import { minorUnitsPerMajor } from "@solvapay/core";
840
687
  var CHECKOUT_SESSION_TTL_MINUTES = 15;
841
688
  var DOCS_HINT = "See docs://solvapay/overview.md.";
689
+ function creditSignals(limits) {
690
+ if (!limits) return { isCreditBased: false };
691
+ const creditBalance = limits.balance?.creditBalance ?? limits.creditBalance;
692
+ const creditsPerCall = limits.balance?.creditsPerUnit ?? limits.creditsPerUnit;
693
+ const isCreditBased = creditBalance !== void 0 || creditsPerCall !== void 0;
694
+ const shortfallCredits = creditBalance !== void 0 && creditsPerCall !== void 0 ? Math.max(0, creditsPerCall - creditBalance) : void 0;
695
+ let remainingCalls;
696
+ if (limits.balance?.remainingUnits !== void 0) {
697
+ remainingCalls = limits.balance.remainingUnits;
698
+ } else if (creditBalance !== void 0 && creditsPerCall !== void 0 && creditsPerCall > 0) {
699
+ remainingCalls = Math.floor(creditBalance / creditsPerCall);
700
+ } else if (limits.remaining >= 0) {
701
+ remainingCalls = limits.remaining;
702
+ }
703
+ return { creditBalance, creditsPerCall, shortfallCredits, remainingCalls, isCreditBased };
704
+ }
705
+ function nextActionFor(state) {
706
+ switch (state.kind) {
707
+ case "activation_required":
708
+ return "activate";
709
+ case "topup_required":
710
+ return "topup";
711
+ case "upgrade_required":
712
+ case "limit_reached":
713
+ return "checkout";
714
+ case "reactivation_required":
715
+ return "account";
716
+ }
717
+ }
718
+ function activePlanRefOf(limits) {
719
+ const ref = limits.planRef ?? limits.plan;
720
+ return ref && ref.length > 0 ? ref : void 0;
721
+ }
842
722
  function classifyPaywallState(limits) {
843
723
  if (!limits) return { kind: "upgrade_required" };
844
- if (limits.activationRequired === true) {
724
+ if (limits.activationRequired === true || limits.paywallReason === "activation_required") {
845
725
  return { kind: "activation_required" };
846
726
  }
727
+ if (limits.paywallReason === "topup_required") {
728
+ return { kind: "topup_required" };
729
+ }
847
730
  if (limits.needsTopUp === true) {
848
731
  return { kind: "topup_required" };
849
732
  }
850
733
  if (limits.needsUpgrade === true) {
851
734
  return { kind: "upgrade_required" };
852
735
  }
853
- const activePlan = limits.plans?.find((p) => p.reference === limits.plan);
854
- const isUsageBased = activePlan?.type === "usage-based" || limits.balance !== void 0;
855
- const creditBalance = limits.balance?.creditBalance ?? limits.creditBalance;
856
- if (isUsageBased) {
857
- if (creditBalance === 0) return { kind: "topup_required" };
858
- if (creditBalance === void 0 && limits.remaining === 0) {
736
+ const signals = creditSignals(limits);
737
+ if (signals.isCreditBased) {
738
+ if (signals.creditBalance !== void 0 && signals.creditsPerCall !== void 0 && signals.creditBalance < signals.creditsPerCall) {
739
+ return { kind: "topup_required" };
740
+ }
741
+ if (signals.creditBalance === 0) return { kind: "topup_required" };
742
+ if (signals.creditsPerCall === void 0 && limits.remaining === 0) {
859
743
  return { kind: "topup_required" };
860
744
  }
861
745
  }
862
- if (limits.plan.length > 0 && limits.remaining <= 0) {
746
+ const activeRef = activePlanRefOf(limits);
747
+ if ((limits.purchaseRef || activeRef) && limits.remaining <= 0) {
863
748
  return { kind: "limit_reached" };
864
749
  }
750
+ if (signals.isCreditBased) return { kind: "topup_required" };
751
+ if (limits.purchaseRef) return { kind: "limit_reached" };
865
752
  return { kind: "upgrade_required" };
866
753
  }
867
754
  function formatMinor(amountMinor, currency) {
868
- const major = amountMinor / 100;
755
+ const divisor = minorUnitsPerMajor(currency);
756
+ const major = amountMinor / divisor;
757
+ const fractionDigits = divisor === 1 ? 0 : 2;
869
758
  try {
870
759
  return new Intl.NumberFormat("en-US", {
871
760
  style: "currency",
872
761
  currency: currency.toUpperCase(),
873
- minimumFractionDigits: 2,
874
- maximumFractionDigits: 2
762
+ minimumFractionDigits: fractionDigits,
763
+ maximumFractionDigits: fractionDigits
875
764
  }).format(major);
876
765
  } catch {
877
- return `${currency.toUpperCase()} ${major.toFixed(2)}`;
766
+ return `${currency.toUpperCase()} ${divisor === 1 ? String(Math.round(major)) : major.toFixed(2)}`;
878
767
  }
879
768
  }
769
+ function formatCredits(value) {
770
+ return value.toLocaleString("en-US");
771
+ }
880
772
  function meterLabel(gate) {
881
773
  if (!gate.meterName) return "units";
882
774
  return gate.meterName.replace(/_/g, " ");
@@ -884,6 +776,14 @@ function meterLabel(gate) {
884
776
  function namedCheckoutMarkdown(url) {
885
777
  return `[Open checkout](${url})`;
886
778
  }
779
+ function linkLabel(name) {
780
+ return name.replace(/([[\]])/g, "\\$1");
781
+ }
782
+ function planLadder(gate) {
783
+ const linkable = (gate.plans ?? []).filter((p) => typeof p.checkoutUrl === "string" && p.checkoutUrl.length > 0).filter((p) => p.reference !== gate.planRef).sort((a, b) => a.price - b.price).slice(0, 4);
784
+ if (linkable.length === 0) return null;
785
+ return linkable.map((p) => `[${linkLabel(p.name ?? p.reference)}](${p.checkoutUrl})`).join(" \xB7 ");
786
+ }
887
787
  var VIEWER_TOOL = "account";
888
788
  function callViewer(view) {
889
789
  return view ? `call the \`${VIEWER_TOOL}\` tool with view: '${view}'` : `call the \`${VIEWER_TOOL}\` tool`;
@@ -894,25 +794,57 @@ function recoverClause(url, verb, view) {
894
794
  }
895
795
  return ` ${callViewer(view).replace(/^c/, "C")}.`;
896
796
  }
797
+ function hasActivePlan(gate) {
798
+ return Boolean(gate.purchaseRef || gate.planRef && gate.planRef.length > 0);
799
+ }
800
+ function autoRechargeDisabled(gate) {
801
+ return gate.autoRecharge?.enabled === false;
802
+ }
897
803
  function buildGateMessage(state, gate) {
898
804
  const url = gate.checkoutUrl && gate.checkoutUrl.length > 0 ? gate.checkoutUrl : null;
805
+ const ladder = planLadder(gate);
899
806
  switch (state.kind) {
900
807
  case "limit_reached": {
901
808
  const included = gate.included;
902
809
  const price = gate.unitPriceMinor != null && gate.currency ? formatMinor(gate.unitPriceMinor, gate.currency) : null;
903
810
  const usedLine = included ? `You've used ${included.used} of ${included.total} included ${meterLabel(gate)} this period.` : `You've reached the included usage for this period.`;
904
811
  const nextLine = price ? ` The next call is ${price}.` : "";
905
- return `${usedLine}${nextLine}${recoverClause(url, "continue", "checkout")} ${DOCS_HINT}`;
812
+ const planName = gate.planName ?? "This plan";
813
+ const antiTrap = gate.creditBalance !== void 0 && gate.creditBalance > 0 && (gate.creditsPerCall === void 0 || gate.creditsPerCall === 0) ? ` Adding credits will not help, because ${planName} does not spend them.` : "";
814
+ const switchLine = ladder ? ` Or switch plan: ${ladder}.` : recoverClause(url, "continue", "checkout");
815
+ return `${usedLine}${nextLine}${antiTrap}${switchLine} ${DOCS_HINT}`;
906
816
  }
907
817
  case "activation_required":
908
818
  return `Your plan needs activation.${recoverClause(url, "activate", "checkout")} Or call \`activate_plan\` with a \`planRef\`. ${DOCS_HINT}`;
909
819
  case "topup_required": {
910
- const currency = gate.currency ?? gate.balance?.currency ?? "USD";
911
- const presets = [1e3, 2500, 5e3, 1e4].map((m) => formatMinor(m, currency)).join(" \xB7 ");
912
- return `You're out of credits. Top up first (${presets}).${recoverClause(url, "add credits", "topup")} ${DOCS_HINT}`;
913
- }
914
- case "upgrade_required":
820
+ const balance = gate.creditBalance;
821
+ const cost = gate.creditsPerCall;
822
+ const shortfall = gate.shortfallCredits;
823
+ let lead;
824
+ if (balance !== void 0 && cost !== void 0 && shortfall !== void 0) {
825
+ lead = `Out of credits for this call. Balance ${formatCredits(balance)} credits; this call costs ${formatCredits(cost)} credits \u2014 ${formatCredits(shortfall)} short.`;
826
+ } else {
827
+ lead = "Included usage is exhausted.";
828
+ }
829
+ const topup = recoverClause(url, "add credits", "topup");
830
+ const auto = autoRechargeDisabled(gate) ? " Auto-recharge is off \u2014 turn it on from the account tool to avoid this next time." : "";
831
+ const switchLine = ladder ? ` Or switch plan: ${ladder}.` : "";
832
+ return `${lead}${topup}${auto}${switchLine} ${DOCS_HINT}`;
833
+ }
834
+ case "upgrade_required": {
835
+ if (hasActivePlan(gate)) {
836
+ const planName = gate.planName ?? "This plan";
837
+ const lead = `${planName} is active but its included usage is exhausted, and the automatic switch to the next plan did not complete.`;
838
+ if (ladder) {
839
+ return `${lead} Switch here: ${ladder}. ${DOCS_HINT}`;
840
+ }
841
+ return `${lead}${recoverClause(url, "switch plan", "checkout")} ${DOCS_HINT}`;
842
+ }
843
+ if (ladder) {
844
+ return `You don't have an active plan for this tool. Pick a plan to use this tool: ${ladder} (links expire in ${CHECKOUT_SESSION_TTL_MINUTES} minutes), or ${callViewer("checkout")}. ${DOCS_HINT}`;
845
+ }
915
846
  return `You don't have an active plan for this tool.${recoverClause(url, "pick a plan", "checkout")} ${DOCS_HINT}`;
847
+ }
916
848
  case "reactivation_required":
917
849
  return `Your previous plan is no longer active. ${callViewer("account").replace(/^c/, "C")} to reactivate it, or ${callViewer("checkout")} to pick a new plan. ${DOCS_HINT}`;
918
850
  }
@@ -920,11 +852,18 @@ function buildGateMessage(state, gate) {
920
852
  function buildNudgeMessage(state, limits) {
921
853
  const url = limits?.checkoutUrl && limits.checkoutUrl.length > 0 ? limits.checkoutUrl : null;
922
854
  const visitClause = url ? `, or ${namedCheckoutMarkdown(url)}` : "";
855
+ const remaining = creditSignals(limits).remainingCalls;
923
856
  switch (state.kind) {
924
857
  case "topup_required":
858
+ if (remaining === 0) {
859
+ return `Heads up \u2014 0 calls left \u2014 the next call needs a top-up. ${callViewer("topup").replace(/^c/, "C")} to add more${visitClause}.`;
860
+ }
925
861
  return `Heads up \u2014 running low on credits. ${callViewer("topup").replace(/^c/, "C")} to add more${visitClause}.`;
926
862
  case "upgrade_required":
927
863
  case "limit_reached":
864
+ if (remaining === 0) {
865
+ return `Heads up \u2014 0 calls left \u2014 the next call needs a top-up. ${callViewer("checkout").replace(/^c/, "C")} for more headroom${visitClause}.`;
866
+ }
928
867
  return `Heads up \u2014 approaching your plan's limit this period. ${callViewer("checkout").replace(/^c/, "C")} for more headroom${visitClause}.`;
929
868
  case "activation_required":
930
869
  return `Heads up \u2014 this plan still needs activation. Call the \`activate_plan\` tool with a \`planRef\`${visitClause}.`;
@@ -940,11 +879,23 @@ function allPaidPlansArePayg(plans) {
940
879
  if (paidPlans.length === 0) return false;
941
880
  return paidPlans.every((p) => p.type === "usage-based" || p.type === "hybrid");
942
881
  }
882
+ function activePlanRefOf2(limits) {
883
+ const ref = limits.planRef ?? limits.plan;
884
+ return ref && ref.length > 0 ? ref : void 0;
885
+ }
943
886
  function activePlanOf(limits) {
944
- if (!limits.plan) return void 0;
945
- return limits.plans?.find((p) => p.reference === limits.plan);
887
+ const ref = activePlanRefOf2(limits);
888
+ if (!ref) return void 0;
889
+ return limits.plans?.find((p) => p.reference === ref);
946
890
  }
947
891
  function includedFromLimits(limits) {
892
+ if (limits.used !== void 0 && limits.limit !== void 0) {
893
+ return {
894
+ total: limits.limit,
895
+ used: limits.used,
896
+ remaining: limits.remaining
897
+ };
898
+ }
948
899
  const total = activePlanOf(limits)?.freeUnits;
949
900
  if (total == null || total === 0) return void 0;
950
901
  const remaining = limits.remaining;
@@ -954,24 +905,49 @@ function includedFromLimits(limits) {
954
905
  remaining
955
906
  };
956
907
  }
957
- function recoveryFields(limits) {
908
+ function recoveryLinks(limits) {
909
+ const links = {};
910
+ if (limits.checkoutUrl) {
911
+ if (limits.checkoutUrl.includes("/topup")) {
912
+ links.topup = limits.checkoutUrl;
913
+ } else {
914
+ links.checkout = limits.checkoutUrl;
915
+ }
916
+ }
917
+ if (limits.confirmationUrl) {
918
+ links.manage = limits.confirmationUrl;
919
+ }
920
+ return Object.keys(links).length > 0 ? links : void 0;
921
+ }
922
+ function recoveryFields(limits, state) {
958
923
  const plan = activePlanOf(limits);
959
924
  const included = includedFromLimits(limits);
960
- const creditBalance = limits.balance?.creditBalance ?? limits.creditBalance;
925
+ const signals = creditSignals(limits);
926
+ const planRef = activePlanRefOf2(limits);
927
+ const links = recoveryLinks(limits);
961
928
  return {
962
- ...limits.plan ? { planRef: limits.plan } : {},
929
+ ...planRef ? { planRef } : {},
930
+ ...limits.planName !== void 0 ? { planName: limits.planName } : {},
963
931
  ...limits.plans !== void 0 ? { plans: limits.plans } : {},
964
932
  ...limits.meterName !== void 0 ? { meterName: limits.meterName } : {},
965
933
  ...plan?.perUnitChargeMinor != null ? { unitPriceMinor: plan.perUnitChargeMinor, currency: plan.currency } : limits.currency ? { currency: limits.currency } : {},
966
934
  ...included ? { included } : {},
967
- ...creditBalance !== void 0 ? { creditBalance } : {}
935
+ ...signals.creditBalance !== void 0 ? { creditBalance: signals.creditBalance } : {},
936
+ reason: state.kind,
937
+ nextAction: nextActionFor(state),
938
+ ...signals.creditsPerCall !== void 0 ? { creditsPerCall: signals.creditsPerCall } : {},
939
+ ...signals.shortfallCredits !== void 0 ? { shortfallCredits: signals.shortfallCredits } : {},
940
+ ...signals.remainingCalls !== void 0 ? { remainingCalls: signals.remainingCalls } : {},
941
+ ...limits.purchaseRef !== void 0 ? { purchaseRef: limits.purchaseRef } : {},
942
+ ...limits.autoRecharge !== void 0 ? { autoRecharge: limits.autoRecharge } : {},
943
+ ...links ? { links } : {}
968
944
  };
969
945
  }
970
946
  function buildPaywallGate(productRef, limits) {
971
947
  const checkoutUrl = limits.checkoutUrl;
972
- const state = classifyPaywallState({ ...limits, plan: limits.plan ?? "" });
948
+ const state = classifyPaywallState(limits);
973
949
  const useActivationForTopup = !limits.activationRequired && state.kind === "topup_required" && allPaidPlansArePayg(limits.plans);
974
- const recovery = recoveryFields(limits);
950
+ const recovery = recoveryFields(limits, state);
975
951
  const preMessage = limits.activationRequired || useActivationForTopup ? {
976
952
  kind: "activation_required",
977
953
  product: productRef,
@@ -2475,6 +2451,15 @@ async function resolveCustomerRefFromRequest(req, options) {
2475
2451
  }
2476
2452
 
2477
2453
  // src/types/paywall.ts
2454
+ var PAYWALL_REASONS = [
2455
+ "activation_required",
2456
+ "topup_required",
2457
+ "payment_required",
2458
+ "upgrade_required",
2459
+ "limit_reached",
2460
+ "reactivation_required"
2461
+ ];
2462
+ var PAYWALL_NEXT_ACTIONS = ["topup", "checkout", "activate", "account"];
2478
2463
  function isPaywallStructuredContent(value) {
2479
2464
  if (typeof value !== "object" || value === null || !("kind" in value)) {
2480
2465
  return false;
@@ -2492,6 +2477,7 @@ var includedSchema = z.object({
2492
2477
  });
2493
2478
  var recoveryFields2 = {
2494
2479
  planRef: z.string().optional(),
2480
+ planName: z.string().optional(),
2495
2481
  plans: z.array(z.record(z.string(), z.unknown())).optional(),
2496
2482
  meterName: z.string().optional(),
2497
2483
  unitPriceMinor: z.number().optional(),
@@ -2499,7 +2485,23 @@ var recoveryFields2 = {
2499
2485
  included: includedSchema.optional(),
2500
2486
  creditBalance: z.number().optional(),
2501
2487
  balance: z.record(z.string(), z.unknown()).optional(),
2502
- productDetails: z.record(z.string(), z.unknown()).optional()
2488
+ productDetails: z.record(z.string(), z.unknown()).optional(),
2489
+ reason: z.enum(PAYWALL_REASONS).optional(),
2490
+ nextAction: z.enum(PAYWALL_NEXT_ACTIONS).optional(),
2491
+ creditsPerCall: z.number().optional(),
2492
+ shortfallCredits: z.number().optional(),
2493
+ remainingCalls: z.number().optional(),
2494
+ purchaseRef: z.string().optional(),
2495
+ planStatus: z.string().optional(),
2496
+ autoRecharge: z.object({
2497
+ enabled: z.boolean(),
2498
+ status: z.string().optional()
2499
+ }).optional(),
2500
+ links: z.object({
2501
+ topup: z.string().optional(),
2502
+ checkout: z.string().optional(),
2503
+ manage: z.string().optional()
2504
+ }).optional()
2503
2505
  };
2504
2506
  var PaywallStructuredContentSchema = z.discriminatedUnion("kind", [
2505
2507
  z.object({
@@ -3664,6 +3666,7 @@ export {
3664
3666
  createSolvaPay,
3665
3667
  createSolvaPayClient,
3666
3668
  createTopupPaymentIntentCore,
3669
+ creditSignals,
3667
3670
  deriveUsageSnapshot,
3668
3671
  disableAutoRechargeCore,
3669
3672
  getAuthenticatedUserCore,
@@ -3677,8 +3680,11 @@ export {
3677
3680
  handleRouteError,
3678
3681
  isErrorResult,
3679
3682
  isPaywallStructuredContent,
3683
+ linkLabel,
3680
3684
  listPlansCore,
3685
+ nextActionFor,
3681
3686
  paywallErrorToClientPayload,
3687
+ planLadder,
3682
3688
  processPaymentIntentCore,
3683
3689
  processTopupPaymentIntentCore,
3684
3690
  reactivatePurchaseCore,