@secureprocessing/node 0.0.0-stage → 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/LICENSE +21 -0
- package/README.md +248 -2
- package/dist/client.d.ts +94 -0
- package/dist/client.js +196 -0
- package/dist/errors.d.ts +99 -0
- package/dist/errors.js +125 -0
- package/dist/index.d.ts +603 -0
- package/dist/index.js +520 -0
- package/dist/pagination.d.ts +32 -0
- package/dist/pagination.js +52 -0
- package/dist/types.d.ts +892 -0
- package/dist/types.js +3 -0
- package/dist/version.d.ts +8 -0
- package/dist/version.js +9 -0
- package/dist/webhooks.d.ts +22 -0
- package/dist/webhooks.js +79 -0
- package/package.json +32 -4
package/dist/types.js
ADDED
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Sent in every request's User-Agent, so a support request can say which SDK made the call.
|
|
3
|
+
*
|
|
4
|
+
* A constant rather than a read of package.json, because the SDK runs where there is no
|
|
5
|
+
* filesystem. `test/sdk.test.ts` fails if the two disagree — 0.1.0's User-Agent was a literal in two
|
|
6
|
+
* places, and a bump would have left both claiming the old version.
|
|
7
|
+
*/
|
|
8
|
+
export declare const VERSION = "0.2.0";
|
package/dist/version.js
ADDED
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
// @ts-self-types="./version.d.ts"
|
|
2
|
+
/**
|
|
3
|
+
* Sent in every request's User-Agent, so a support request can say which SDK made the call.
|
|
4
|
+
*
|
|
5
|
+
* A constant rather than a read of package.json, because the SDK runs where there is no
|
|
6
|
+
* filesystem. `test/sdk.test.ts` fails if the two disagree — 0.1.0's User-Agent was a literal in two
|
|
7
|
+
* places, and a bump would have left both claiming the old version.
|
|
8
|
+
*/
|
|
9
|
+
export const VERSION = "0.2.0";
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import type { SecureProcessingEvent } from "./types.js";
|
|
2
|
+
export declare const SIGNATURE_HEADER = "Secure-Processing-Signature";
|
|
3
|
+
/**
|
|
4
|
+
* Verify a webhook and return the event.
|
|
5
|
+
*
|
|
6
|
+
* 🔴 Pass the RAW request body, as a string, exactly as it arrived. A body that has been parsed and
|
|
7
|
+
* re-serialised will not match the signature — key order and whitespace both change — and this is
|
|
8
|
+
* the single most common reason a first webhook integration fails.
|
|
9
|
+
*
|
|
10
|
+
* The timestamp is part of what was signed, so an old request cannot be replayed at you: anything
|
|
11
|
+
* outside the tolerance window is rejected even if its signature is genuine.
|
|
12
|
+
*
|
|
13
|
+
* Works anywhere Web Crypto does: Node 20+, Deno, Bun, Cloudflare Workers.
|
|
14
|
+
*/
|
|
15
|
+
export declare function constructEvent<T = Record<string, unknown>>(payload: string, signatureHeader: string, secret: string, options?: {
|
|
16
|
+
toleranceSeconds?: number;
|
|
17
|
+
nowSeconds?: number;
|
|
18
|
+
}): Promise<SecureProcessingEvent<T>>;
|
|
19
|
+
export declare function parseSignatureHeader(header: string): {
|
|
20
|
+
timestamp: number | null;
|
|
21
|
+
signatures: string[];
|
|
22
|
+
};
|
package/dist/webhooks.js
ADDED
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
// @ts-self-types="./webhooks.d.ts"
|
|
2
|
+
import { SecureProcessingError } from "./errors.js";
|
|
3
|
+
export const SIGNATURE_HEADER = "Secure-Processing-Signature";
|
|
4
|
+
const DEFAULT_TOLERANCE_SECONDS = 300;
|
|
5
|
+
/**
|
|
6
|
+
* Verify a webhook and return the event.
|
|
7
|
+
*
|
|
8
|
+
* 🔴 Pass the RAW request body, as a string, exactly as it arrived. A body that has been parsed and
|
|
9
|
+
* re-serialised will not match the signature — key order and whitespace both change — and this is
|
|
10
|
+
* the single most common reason a first webhook integration fails.
|
|
11
|
+
*
|
|
12
|
+
* The timestamp is part of what was signed, so an old request cannot be replayed at you: anything
|
|
13
|
+
* outside the tolerance window is rejected even if its signature is genuine.
|
|
14
|
+
*
|
|
15
|
+
* Works anywhere Web Crypto does: Node 20+, Deno, Bun, Cloudflare Workers.
|
|
16
|
+
*/
|
|
17
|
+
export async function constructEvent(payload, signatureHeader, secret, options = {}) {
|
|
18
|
+
const { timestamp, signatures } = parseSignatureHeader(signatureHeader);
|
|
19
|
+
if (timestamp === null)
|
|
20
|
+
throw signatureError("The signature header carries no timestamp");
|
|
21
|
+
if (signatures.length === 0)
|
|
22
|
+
throw signatureError("The signature header carries no signatures");
|
|
23
|
+
const tolerance = options.toleranceSeconds ?? DEFAULT_TOLERANCE_SECONDS;
|
|
24
|
+
const now = options.nowSeconds ?? Math.floor(Date.now() / 1000);
|
|
25
|
+
if (Math.abs(now - timestamp) > tolerance) {
|
|
26
|
+
throw signatureError(`This webhook is ${Math.abs(now - timestamp)}s old, outside the ${tolerance}s tolerance. ` +
|
|
27
|
+
"If that is unexpected, check your server's clock.");
|
|
28
|
+
}
|
|
29
|
+
const expected = await hmacSha256Hex(secret, `${timestamp}.${payload}`);
|
|
30
|
+
// Every candidate is checked, in constant time: returning early on a match would leak which of a
|
|
31
|
+
// rotating pair of secrets is the current one.
|
|
32
|
+
let matched = false;
|
|
33
|
+
for (const candidate of signatures) {
|
|
34
|
+
if (timingSafeEqual(expected, candidate))
|
|
35
|
+
matched = true;
|
|
36
|
+
}
|
|
37
|
+
if (!matched) {
|
|
38
|
+
throw signatureError("No signature matched. Check that you passed the raw request body, and that the signing " +
|
|
39
|
+
"secret belongs to this endpoint.");
|
|
40
|
+
}
|
|
41
|
+
return JSON.parse(payload);
|
|
42
|
+
}
|
|
43
|
+
export function parseSignatureHeader(header) {
|
|
44
|
+
let timestamp = null;
|
|
45
|
+
const signatures = [];
|
|
46
|
+
for (const part of header.split(",")) {
|
|
47
|
+
const [key, value] = part.trim().split("=", 2);
|
|
48
|
+
if (!key || value === undefined)
|
|
49
|
+
continue;
|
|
50
|
+
if (key === "t") {
|
|
51
|
+
const parsed = Number(value);
|
|
52
|
+
if (Number.isFinite(parsed))
|
|
53
|
+
timestamp = parsed;
|
|
54
|
+
}
|
|
55
|
+
else if (key === "v1") {
|
|
56
|
+
signatures.push(value);
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
return { timestamp, signatures };
|
|
60
|
+
}
|
|
61
|
+
function signatureError(message) {
|
|
62
|
+
return new SecureProcessingError(400, {
|
|
63
|
+
error: { type: "invalid_request_error", code: "signature_verification_failed", message },
|
|
64
|
+
});
|
|
65
|
+
}
|
|
66
|
+
async function hmacSha256Hex(secret, message) {
|
|
67
|
+
const encoder = new TextEncoder();
|
|
68
|
+
const key = await crypto.subtle.importKey("raw", encoder.encode(secret), { name: "HMAC", hash: "SHA-256" }, false, ["sign"]);
|
|
69
|
+
const signature = await crypto.subtle.sign("HMAC", key, encoder.encode(message));
|
|
70
|
+
return [...new Uint8Array(signature)].map((b) => b.toString(16).padStart(2, "0")).join("");
|
|
71
|
+
}
|
|
72
|
+
function timingSafeEqual(a, b) {
|
|
73
|
+
if (a.length !== b.length)
|
|
74
|
+
return false;
|
|
75
|
+
let diff = 0;
|
|
76
|
+
for (let i = 0; i < a.length; i += 1)
|
|
77
|
+
diff |= a.charCodeAt(i) ^ b.charCodeAt(i);
|
|
78
|
+
return diff === 0;
|
|
79
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,34 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@secureprocessing/node",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"
|
|
5
|
-
"
|
|
6
|
-
|
|
3
|
+
"version": "0.2.0",
|
|
4
|
+
"description": "Secure Processing API client. Card payments for Belize.",
|
|
5
|
+
"homepage": "https://docs.secureprocessing.app/sdk",
|
|
6
|
+
"bugs": {
|
|
7
|
+
"url": "https://secureprocessing.app/contact"
|
|
8
|
+
},
|
|
9
|
+
"type": "module",
|
|
10
|
+
"main": "./dist/index.js",
|
|
11
|
+
"types": "./dist/index.d.ts",
|
|
12
|
+
"exports": {
|
|
13
|
+
".": {
|
|
14
|
+
"types": "./dist/index.d.ts",
|
|
15
|
+
"import": "./dist/index.js"
|
|
16
|
+
}
|
|
17
|
+
},
|
|
18
|
+
"files": [
|
|
19
|
+
"dist",
|
|
20
|
+
"README.md",
|
|
21
|
+
"LICENSE"
|
|
22
|
+
],
|
|
23
|
+
"sideEffects": false,
|
|
24
|
+
"engines": {
|
|
25
|
+
"node": ">=20"
|
|
26
|
+
},
|
|
27
|
+
"license": "MIT",
|
|
28
|
+
"keywords": [
|
|
29
|
+
"payments",
|
|
30
|
+
"belize",
|
|
31
|
+
"secure-processing",
|
|
32
|
+
"api"
|
|
33
|
+
]
|
|
34
|
+
}
|