@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 +6 -1
- package/dist/client.js +37 -2
- package/dist/types.d.ts +21 -0
- package/package.json +2 -2
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.
|
|
76
|
-
`
|
|
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.
|
|
4
|
-
"description": "Node/TypeScript client for the Sweep Console payment rail
|
|
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",
|