@sweepconsole/node 0.1.0 → 0.1.1

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 (2) hide show
  1. package/dist/client.js +36 -2
  2. package/package.json +2 -2
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));
@@ -271,6 +279,32 @@ function toCharge(raw) {
271
279
  function safeJson(res) {
272
280
  return res.json().catch(() => null);
273
281
  }
282
+ /**
283
+ * A proxy in front of the API refused the request before it got there.
284
+ *
285
+ * Server-to-server calls are what this catches. A request from a datacenter
286
+ * carries no browser fingerprint, so bot protection tuned for browsers treats
287
+ * it as exactly the thing it is looking for and answers a challenge page —
288
+ * HTML, with a 403, and a trace id of its own.
289
+ *
290
+ * Worth separating from every other 403, because nothing about the caller's
291
+ * code is wrong and no amount of re-reading their API key will help. The trace
292
+ * id is the one piece of information support actually needs.
293
+ */
294
+ function edgeBlock(res) {
295
+ if (res.status !== 403 && res.status !== 503)
296
+ return null;
297
+ const trace = res.headers.get("cf-ray") ??
298
+ res.headers.get("x-amz-cf-id") ??
299
+ res.headers.get("x-akamai-request-id");
300
+ if (!trace)
301
+ return null;
302
+ // Our own 403s are JSON. An HTML body means something else answered.
303
+ const type = res.headers.get("content-type") ?? "";
304
+ if (type.includes("json"))
305
+ return null;
306
+ return { trace };
307
+ }
274
308
  function sleep(ms) {
275
309
  return new Promise((r) => setTimeout(r, ms));
276
310
  }
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.1.1",
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",