@sweepconsole/node 0.1.0 → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -91,7 +91,7 @@ app.post("/subscribe/usdc", async (req, res) => {
91
91
  email: req.user.email,
92
92
  maxAmount: usdc("15.00"), // the CEILING, not the price
93
93
  interval: "monthly",
94
- chains: ["base", "arbitrum", "optimism"],
94
+ chains: ["base", "arbitrum", "optimism"], // what to OFFER — they pick
95
95
  expiresAt: new Date("2027-01-01"),
96
96
  returnUrl: "https://shop.example.com/thanks",
97
97
  });
@@ -101,6 +101,11 @@ app.post("/subscribe/usdc", async (req, res) => {
101
101
  });
102
102
  ```
103
103
 
104
+ The payer chooses which of those chains to sign, so `chains` is a maximum rather
105
+ than a promise. What they actually granted comes back as `mandate.grantedChains`,
106
+ and on the `mandate.authorized` event as `chain_ids`. Charge against those — a
107
+ mandate offered on three chains is often live on one.
108
+
104
109
  **2 — react to what happens**
105
110
 
106
111
  ```ts
package/dist/client.js CHANGED
@@ -72,8 +72,9 @@ export class Sweep {
72
72
  catch {
73
73
  const looksLikeHtml = /^\s*<(!doctype|html)/i.test(text);
74
74
  throw new SweepError(looksLikeHtml
75
- ? `Expected JSON from ${res.url} but got an HTML page. This usually means baseUrl points at the ` +
76
- `website rather than the API — check the API base URL in your Sweep portal.`
75
+ ? `Expected JSON from ${res.url} but got an HTML page. Either baseUrl points at the website ` +
76
+ `rather than the API — api.sweepconsole.xyz, not www — or something between you and the API ` +
77
+ `answered instead of it.`
77
78
  : `Expected JSON from ${res.url} but got ${res.headers.get("content-type") ?? "an unknown type"}.`, { code: "invalid_response", status: res.status });
78
79
  }
79
80
  }
@@ -105,6 +106,13 @@ export class Sweep {
105
106
  });
106
107
  if (res.ok)
107
108
  return (await this.parse(res));
109
+ const blocked = edgeBlock(res);
110
+ if (blocked) {
111
+ throw new SweepError(`Blocked before reaching the Sweep API: ${res.status} from a proxy in front of ` +
112
+ `${res.url}, answering HTML rather than JSON. Server-to-server requests trip bot ` +
113
+ `protection tuned for browsers, so this is infrastructure rather than your code. ` +
114
+ `Quote this trace id to support: ${blocked.trace}`, { code: "blocked_upstream", status: res.status, reference: blocked.trace });
115
+ }
108
116
  // 5xx and 429 may succeed on a retry; a 4xx is a decision, not a blip.
109
117
  if ((res.status >= 500 || res.status === 429) && attempt < this.maxRetries) {
110
118
  lastError = errorFrom(res.status, await safeJson(res));
@@ -239,6 +247,7 @@ function toMandate(raw) {
239
247
  interval: raw.interval,
240
248
  periodDuration: raw.period_duration,
241
249
  chains: raw.chains ?? [],
250
+ grantedChains: raw.granted_chains ?? [],
242
251
  testMode: !!raw.test_mode,
243
252
  expiresAt: date(raw.expires_at),
244
253
  authorizationUrl: raw.authorization_url ?? null,
@@ -271,6 +280,32 @@ function toCharge(raw) {
271
280
  function safeJson(res) {
272
281
  return res.json().catch(() => null);
273
282
  }
283
+ /**
284
+ * A proxy in front of the API refused the request before it got there.
285
+ *
286
+ * Server-to-server calls are what this catches. A request from a datacenter
287
+ * carries no browser fingerprint, so bot protection tuned for browsers treats
288
+ * it as exactly the thing it is looking for and answers a challenge page —
289
+ * HTML, with a 403, and a trace id of its own.
290
+ *
291
+ * Worth separating from every other 403, because nothing about the caller's
292
+ * code is wrong and no amount of re-reading their API key will help. The trace
293
+ * id is the one piece of information support actually needs.
294
+ */
295
+ function edgeBlock(res) {
296
+ if (res.status !== 403 && res.status !== 503)
297
+ return null;
298
+ const trace = res.headers.get("cf-ray") ??
299
+ res.headers.get("x-amz-cf-id") ??
300
+ res.headers.get("x-akamai-request-id");
301
+ if (!trace)
302
+ return null;
303
+ // Our own 403s are JSON. An HTML body means something else answered.
304
+ const type = res.headers.get("content-type") ?? "";
305
+ if (type.includes("json"))
306
+ return null;
307
+ return { trace };
308
+ }
274
309
  function sleep(ms) {
275
310
  return new Promise((r) => setTimeout(r, ms));
276
311
  }
package/dist/types.d.ts CHANGED
@@ -33,7 +33,22 @@ export interface Mandate {
33
33
  currency: "USDC";
34
34
  interval: Interval;
35
35
  periodDuration: number;
36
+ /**
37
+ * The chains you OFFERED when you created the mandate.
38
+ *
39
+ * Not what the payer granted. The authorization page lets them choose which
40
+ * of these to sign, so a mandate offered on three chains can be live on one
41
+ * — and this still reads three. Charge against `grantedChains`.
42
+ */
36
43
  chains: string[];
44
+ /**
45
+ * The chains the payer actually signed, and what a charge can draw from.
46
+ *
47
+ * Empty while the mandate is pending. It shrinks if they later disable a
48
+ * chain in their own wallet, which is the same set `grant.revoked` reports
49
+ * as `remaining_chains`.
50
+ */
51
+ grantedChains: string[];
37
52
  testMode: boolean;
38
53
  /** When the authorization stops being redeemable. */
39
54
  expiresAt: Date;
@@ -78,6 +93,12 @@ export interface CreateMandateParams {
78
93
  /** The ceiling per interval — NOT the price. Give it headroom. */
79
94
  maxAmount: Usdc;
80
95
  interval: Interval;
96
+ /**
97
+ * The chains to OFFER the payer. They choose which to sign, so this is a
98
+ * maximum, not a promise — read `grantedChains` back to see what you got.
99
+ * More chains means more ways for a charge to succeed, so offer all you
100
+ * support unless you have a reason not to.
101
+ */
81
102
  chains: Chain[];
82
103
  expiresAt: Date;
83
104
  returnUrl?: string;
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@sweepconsole/node",
3
- "version": "0.1.0",
4
- "description": "Node/TypeScript client for the Sweep Console payment rail \u2014 charge a USDC wallet from your own app",
3
+ "version": "0.2.0",
4
+ "description": "Node/TypeScript client for the Sweep Console payment rail — charge a USDC wallet from your own app",
5
5
  "license": "MIT",
6
6
  "type": "module",
7
7
  "main": "./dist/index.js",