@sweepconsole/node 0.0.0-stage → 0.1.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/LICENSE +21 -0
- package/README.md +190 -2
- package/dist/client.d.ts +147 -0
- package/dist/client.js +433 -0
- package/dist/errors.d.ts +79 -0
- package/dist/errors.js +104 -0
- package/dist/index.d.ts +53 -0
- package/dist/index.js +59 -0
- package/dist/money.d.ts +24 -0
- package/dist/money.js +42 -0
- package/dist/types.d.ts +375 -0
- package/dist/types.js +8 -0
- package/dist/webhooks.d.ts +79 -0
- package/dist/webhooks.js +110 -0
- package/package.json +49 -4
package/dist/webhooks.js
ADDED
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
import { createHmac, timingSafeEqual } from "node:crypto";
|
|
2
|
+
import { WebhookSignatureError } from "./errors.js";
|
|
3
|
+
/**
|
|
4
|
+
* Webhook verification, which is where this integration goes wrong most often.
|
|
5
|
+
*
|
|
6
|
+
* Three traps, all of them silent, all of them producing the same "bad
|
|
7
|
+
* signature" symptom:
|
|
8
|
+
*
|
|
9
|
+
* 1. The HMAC is over the RAW bytes. If `express.json()` has already parsed
|
|
10
|
+
* the body, re-serializing it gives different bytes — key order, spacing,
|
|
11
|
+
* unicode escapes — and nothing you do downstream can recover them.
|
|
12
|
+
* 2. The header is `sha256=<hex>`, not bare hex. Comparing the two fails
|
|
13
|
+
* forever, and the failure looks exactly like a wrong secret.
|
|
14
|
+
* 3. The comparison must be constant-time, or it leaks the expected value a
|
|
15
|
+
* byte at a time to anyone willing to send enough requests.
|
|
16
|
+
*
|
|
17
|
+
* `verify` handles all three. Give it the raw body as a Buffer or string.
|
|
18
|
+
*/
|
|
19
|
+
export function verify(rawBody, signature, secret) {
|
|
20
|
+
if (!signature) {
|
|
21
|
+
throw new WebhookSignatureError("No X-Sweep-Signature header on the request.");
|
|
22
|
+
}
|
|
23
|
+
const body = typeof rawBody === "string" ? Buffer.from(rawBody, "utf8") : rawBody;
|
|
24
|
+
const expected = "sha256=" + createHmac("sha256", secret).update(body).digest("hex");
|
|
25
|
+
const a = Buffer.from(signature);
|
|
26
|
+
const b = Buffer.from(expected);
|
|
27
|
+
if (a.length !== b.length || !timingSafeEqual(a, b)) {
|
|
28
|
+
throw new WebhookSignatureError("Signature did not verify. Check that you are hashing the RAW request body " +
|
|
29
|
+
"(express.raw, not express.json) and using this endpoint's signing secret.");
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
/** Verify, then parse. Returns the event with Dates and camelCase fields. */
|
|
33
|
+
export function construct(rawBody, signature, secret, opts = {}) {
|
|
34
|
+
verify(rawBody, signature, secret);
|
|
35
|
+
const raw = JSON.parse(typeof rawBody === "string" ? rawBody : rawBody.toString("utf8"));
|
|
36
|
+
checkAge(raw.created_at, opts.toleranceSeconds);
|
|
37
|
+
return {
|
|
38
|
+
eventId: raw.event_id,
|
|
39
|
+
eventType: raw.event_type,
|
|
40
|
+
createdAt: new Date(raw.created_at),
|
|
41
|
+
merchantId: raw.merchant_id,
|
|
42
|
+
externalRef: raw.external_ref,
|
|
43
|
+
data: raw.data,
|
|
44
|
+
};
|
|
45
|
+
}
|
|
46
|
+
function checkAge(createdAt, toleranceSeconds) {
|
|
47
|
+
if (toleranceSeconds === undefined)
|
|
48
|
+
return;
|
|
49
|
+
const t = Date.parse(createdAt);
|
|
50
|
+
if (Number.isNaN(t)) {
|
|
51
|
+
throw new WebhookSignatureError(`Event created_at is not a date: ${JSON.stringify(createdAt)}`);
|
|
52
|
+
}
|
|
53
|
+
const ageSeconds = Math.abs(Date.now() - t) / 1000;
|
|
54
|
+
if (ageSeconds > toleranceSeconds) {
|
|
55
|
+
throw new WebhookSignatureError(`Event is ${Math.round(ageSeconds)}s old, outside the ${toleranceSeconds}s tolerance. ` +
|
|
56
|
+
`Note that deliveries retry for up to 11 hours, so a short tolerance rejects genuine retries.`);
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
/**
|
|
60
|
+
* An Express handler that verifies, routes, and acknowledges.
|
|
61
|
+
*
|
|
62
|
+
* Mount it with `express.raw` — the whole point is that the raw bytes survive:
|
|
63
|
+
*
|
|
64
|
+
* app.post("/webhooks/sweep",
|
|
65
|
+
* express.raw({ type: "application/json" }),
|
|
66
|
+
* sweep.webhooks.express(process.env.SWEEP_WEBHOOK_SECRET!, {
|
|
67
|
+
* "charge.succeeded": (e) => db.users.extend(e.externalRef),
|
|
68
|
+
* }));
|
|
69
|
+
*
|
|
70
|
+
* It answers 2xx before running your handler, because Sweep gives you 10
|
|
71
|
+
* seconds and retries otherwise. A handler that throws is logged, not
|
|
72
|
+
* re-raised: the event was accepted, and failing the response would earn a
|
|
73
|
+
* retry of something you have already recorded.
|
|
74
|
+
*/
|
|
75
|
+
export function expressHandler(secret, handlers) {
|
|
76
|
+
return (req, res) => {
|
|
77
|
+
let event;
|
|
78
|
+
try {
|
|
79
|
+
event = construct(req.body, header(req, "x-sweep-signature"), secret, {
|
|
80
|
+
toleranceSeconds: handlers.toleranceSeconds,
|
|
81
|
+
});
|
|
82
|
+
}
|
|
83
|
+
catch (e) {
|
|
84
|
+
handlers.onError?.(e);
|
|
85
|
+
res.status(400).send("bad signature");
|
|
86
|
+
return;
|
|
87
|
+
}
|
|
88
|
+
res.status(200).send("ok");
|
|
89
|
+
// The one place the runtime key and the static types meet. Looking a
|
|
90
|
+
// handler up by event.eventType is correct at runtime, but the union of
|
|
91
|
+
// every Handler<K> has an intersection for its parameter, so TypeScript
|
|
92
|
+
// reduces it to never. Erase it here rather than in the signature, where
|
|
93
|
+
// it is what gives each handler its own payload type.
|
|
94
|
+
const handler = handlers[event.eventType];
|
|
95
|
+
if (!handler)
|
|
96
|
+
return;
|
|
97
|
+
void (async () => {
|
|
98
|
+
try {
|
|
99
|
+
await handler(event);
|
|
100
|
+
}
|
|
101
|
+
catch (e) {
|
|
102
|
+
handlers.onError?.(e);
|
|
103
|
+
}
|
|
104
|
+
})();
|
|
105
|
+
};
|
|
106
|
+
}
|
|
107
|
+
function header(req, name) {
|
|
108
|
+
const v = req.headers[name];
|
|
109
|
+
return Array.isArray(v) ? v[0] : v;
|
|
110
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,51 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@sweepconsole/node",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"
|
|
5
|
-
"
|
|
6
|
-
|
|
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",
|
|
5
|
+
"license": "MIT",
|
|
6
|
+
"type": "module",
|
|
7
|
+
"main": "./dist/index.js",
|
|
8
|
+
"types": "./dist/index.d.ts",
|
|
9
|
+
"exports": {
|
|
10
|
+
".": {
|
|
11
|
+
"types": "./dist/index.d.ts",
|
|
12
|
+
"default": "./dist/index.js"
|
|
13
|
+
}
|
|
14
|
+
},
|
|
15
|
+
"files": [
|
|
16
|
+
"dist",
|
|
17
|
+
"README.md"
|
|
18
|
+
],
|
|
19
|
+
"engines": {
|
|
20
|
+
"node": ">=20.0.0"
|
|
21
|
+
},
|
|
22
|
+
"scripts": {
|
|
23
|
+
"build": "tsc -p tsconfig.json",
|
|
24
|
+
"typecheck": "tsc -p tsconfig.json --noEmit",
|
|
25
|
+
"prepublishOnly": "pnpm build && pnpm test",
|
|
26
|
+
"test": "node test/security.test.mjs"
|
|
27
|
+
},
|
|
28
|
+
"keywords": [
|
|
29
|
+
"usdc",
|
|
30
|
+
"stablecoin",
|
|
31
|
+
"subscriptions",
|
|
32
|
+
"payments",
|
|
33
|
+
"sweep-console"
|
|
34
|
+
],
|
|
35
|
+
"devDependencies": {
|
|
36
|
+
"@types/node": "^22.0.0",
|
|
37
|
+
"typescript": "^5.6.0"
|
|
38
|
+
},
|
|
39
|
+
"repository": {
|
|
40
|
+
"type": "git",
|
|
41
|
+
"url": "git+https://github.com/u4believe/sweep-console.git",
|
|
42
|
+
"directory": "packages/sdk"
|
|
43
|
+
},
|
|
44
|
+
"homepage": "https://github.com/u4believe/sweep-console#readme",
|
|
45
|
+
"bugs": {
|
|
46
|
+
"url": "https://github.com/u4believe/sweep-console/issues"
|
|
47
|
+
},
|
|
48
|
+
"publishConfig": {
|
|
49
|
+
"access": "public"
|
|
50
|
+
}
|
|
51
|
+
}
|