sebpay-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/README.md +124 -2
- package/dist/client.d.ts +20 -0
- package/dist/client.js +74 -0
- package/dist/errors.d.ts +12 -0
- package/dist/errors.js +22 -0
- package/dist/index.d.ts +7 -0
- package/dist/index.js +12 -0
- package/dist/resources/checkoutSessions.d.ts +50 -0
- package/dist/resources/checkoutSessions.js +22 -0
- package/dist/version.d.ts +2 -0
- package/dist/version.js +5 -0
- package/dist/webhooks.d.ts +24 -0
- package/dist/webhooks.js +25 -0
- package/package.json +40 -4
package/README.md
CHANGED
|
@@ -1,3 +1,125 @@
|
|
|
1
|
-
#
|
|
1
|
+
# sebpay-node
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
Official Node.js SDK for [Sebpay](https://sebpay.africa) **hosted checkout**. It does four things:
|
|
4
|
+
- create a checkout session;
|
|
5
|
+
- redirect your customer to the Sebpay payment page;
|
|
6
|
+
- confirm the payment when they come back;
|
|
7
|
+
- verify Sebpay's webhooks.
|
|
8
|
+
|
|
9
|
+
**Server-side only.** This SDK runs in Node.js (≥ 18). Never bundle it into a browser build: your secret key must never reach the client.
|
|
10
|
+
|
|
11
|
+
It uses Node's native `fetch` and has no runtime dependency. Direct collections, payouts and reference data are not part of the SDK. They remain available over plain HTTP: see [`docs/SEBPAY_PUBLIC_API.md`](../../docs/SEBPAY_PUBLIC_API.md).
|
|
12
|
+
|
|
13
|
+
## Install
|
|
14
|
+
|
|
15
|
+
```bash
|
|
16
|
+
npm install sebpay-node
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
Requires Node.js 18 or later. The package ships its TypeScript types.
|
|
20
|
+
|
|
21
|
+
## Usage
|
|
22
|
+
|
|
23
|
+
### 1. Create a session and redirect your customer
|
|
24
|
+
|
|
25
|
+
```ts
|
|
26
|
+
import { SebpayClient } from "sebpay-node";
|
|
27
|
+
|
|
28
|
+
// Keys from your dashboard (API développeur): pk_test_ keys go to the sandbox server, pk_live_ keys to live.
|
|
29
|
+
const sebpay = new SebpayClient(process.env.SEBPAY_PUBLIC_KEY!, process.env.SEBPAY_SECRET_KEY!);
|
|
30
|
+
|
|
31
|
+
const session = await sebpay.checkoutSessions.create({
|
|
32
|
+
amount: 5000,
|
|
33
|
+
currency: "XOF",
|
|
34
|
+
redirect_url: "https://yoursite.com/orders/return",
|
|
35
|
+
external_reference: order.id, // your order id: sending it again returns the same session
|
|
36
|
+
callback_url: "https://yoursite.com/webhook/sebpay",
|
|
37
|
+
metadata: { order_id: order.id },
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
res.redirect(session.checkout_url);
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
### 2. Confirm the payment when your customer comes back
|
|
44
|
+
|
|
45
|
+
Your `redirect_url` receives `?checkout_session_id=<id>`. Coming back proves nothing, so ask Sebpay before you deliver:
|
|
46
|
+
|
|
47
|
+
```ts
|
|
48
|
+
const session = await sebpay.checkoutSessions.retrieve(req.query.checkout_session_id);
|
|
49
|
+
|
|
50
|
+
if (session.transaction_status === "approved") {
|
|
51
|
+
// Paid: deliver the order (session.external_reference, session.metadata).
|
|
52
|
+
}
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
`retrieve()` also accepts your `external_reference`.
|
|
56
|
+
|
|
57
|
+
### 3. Verify the webhook
|
|
58
|
+
|
|
59
|
+
Sebpay POSTs the final status (`approved` or `rejected`) to your `callback_url`. For a checkout, the request also carries `checkout_session_id` and your `metadata`.
|
|
60
|
+
|
|
61
|
+
```ts
|
|
62
|
+
import express from "express";
|
|
63
|
+
import { SIGNATURE_HEADER, verifySignature, type WebhookPayload } from "sebpay-node";
|
|
64
|
+
|
|
65
|
+
app.use(express.json({ verify: (req, _res, buf) => { req.rawBody = buf; } }));
|
|
66
|
+
|
|
67
|
+
app.post("/webhook/sebpay", (req, res) => {
|
|
68
|
+
if (!verifySignature(req.rawBody, req.get(SIGNATURE_HEADER), process.env.SEBPAY_SECRET_KEY!)) {
|
|
69
|
+
return res.status(401).json({ error: "Invalid signature" });
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
const payload = req.body as WebhookPayload;
|
|
73
|
+
// payload.status: "approved" | "rejected"
|
|
74
|
+
res.status(200).json({ received: true });
|
|
75
|
+
});
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
- Always pass the **raw** request body. Re-serializing a parsed payload can reorder keys and break the signature.
|
|
79
|
+
- A missing header returns `false`; it never throws.
|
|
80
|
+
|
|
81
|
+
## Options
|
|
82
|
+
|
|
83
|
+
```ts
|
|
84
|
+
new SebpayClient(publicKey, secretKey, {
|
|
85
|
+
baseUrl: "http://localhost:8000/api/v1", // only to test against a local API; otherwise chosen from the keys
|
|
86
|
+
timeout: 10_000, // ms, default 30 000
|
|
87
|
+
});
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
`sebpay.baseUrl` tells which server the client talks to.
|
|
91
|
+
|
|
92
|
+
## Errors
|
|
93
|
+
|
|
94
|
+
Every failed call throws a `SebpayApiError`, with three fields:
|
|
95
|
+
- `status`: the HTTP status, or 0 when no response came back;
|
|
96
|
+
- `code`: the API's `errors.code`, or one of the SDK's own codes. Examples: `EXTERNAL_REFERENCE_CONFLICT`, `RATE_LIMITED`, `WRONG_ENVIRONMENT`, `NETWORK_ERROR`, `TIMEOUT`, `INVALID_RESPONSE`. It is `null` otherwise;
|
|
97
|
+
- `body`: the decoded response.
|
|
98
|
+
|
|
99
|
+
```ts
|
|
100
|
+
import { SebpayApiError } from "sebpay-node";
|
|
101
|
+
|
|
102
|
+
try {
|
|
103
|
+
await sebpay.checkoutSessions.create({ ... });
|
|
104
|
+
} catch (err) {
|
|
105
|
+
if (err instanceof SebpayApiError && err.code === "EXTERNAL_REFERENCE_CONFLICT") {
|
|
106
|
+
// This order id was already used with other parameters.
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
```
|
|
110
|
+
|
|
111
|
+
## Examples
|
|
112
|
+
|
|
113
|
+
See [`examples/`](examples/). Build first (`npm run build`), set `SEBPAY_PUBLIC_KEY` and `SEBPAY_SECRET_KEY`, then:
|
|
114
|
+
- `node examples/create-checkout.mjs` creates a session and prints its `checkout_url`;
|
|
115
|
+
- `node examples/webhook-server.mjs` starts an Express server (`npm install express`) that handles the webhook and the return page, on `PORT` (default 3001).
|
|
116
|
+
|
|
117
|
+
For local testing, also set `SEBPAY_BASE_URL`.
|
|
118
|
+
|
|
119
|
+
## Tests
|
|
120
|
+
|
|
121
|
+
`npm test` builds the SDK and runs its offline tests with `node --test`. They cover the transport, against a local HTTP server, and the shared signature vectors.
|
|
122
|
+
|
|
123
|
+
## Requirements
|
|
124
|
+
|
|
125
|
+
Node.js 18+ (for native `fetch` and `node:crypto`).
|
package/dist/client.d.ts
ADDED
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import { CheckoutSessions } from "./resources/checkoutSessions";
|
|
2
|
+
export interface SebpayClientOptions {
|
|
3
|
+
/** API URL. By default it follows the keys: pk_test_ → sandbox server, otherwise live. */
|
|
4
|
+
baseUrl?: string;
|
|
5
|
+
/** Request timeout, in milliseconds (default 30 000). */
|
|
6
|
+
timeout?: number;
|
|
7
|
+
}
|
|
8
|
+
/** How a resource reaches the API — kept internal, so the SDK only calls the endpoints it wraps. */
|
|
9
|
+
export type Requester = <T>(method: "GET" | "POST", path: string, body?: Record<string, unknown>) => Promise<T>;
|
|
10
|
+
/**
|
|
11
|
+
* Entry point for Sebpay hosted checkout. Server-side only — the secret key must never reach a
|
|
12
|
+
* browser bundle. Pass the key pair from your dashboard (API développeur): the sandbox and live
|
|
13
|
+
* are separate servers, and pk_test_ keys are sent to the sandbox, pk_live_ keys to live.
|
|
14
|
+
*/
|
|
15
|
+
export declare class SebpayClient {
|
|
16
|
+
#private;
|
|
17
|
+
readonly baseUrl: string;
|
|
18
|
+
readonly checkoutSessions: CheckoutSessions;
|
|
19
|
+
constructor(publicKey: string, secretKey: string, options?: SebpayClientOptions);
|
|
20
|
+
}
|
package/dist/client.js
ADDED
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.SebpayClient = void 0;
|
|
4
|
+
const errors_1 = require("./errors");
|
|
5
|
+
const checkoutSessions_1 = require("./resources/checkoutSessions");
|
|
6
|
+
const version_1 = require("./version");
|
|
7
|
+
const LIVE_BASE_URL = "https://newapi.sebpay.africa/api/v1";
|
|
8
|
+
const SANDBOX_BASE_URL = "https://api.sandbox.sebpay.bj/api/v1";
|
|
9
|
+
// Same 30 s default as the PHP and Python SDKs — fetch has no timeout of its own.
|
|
10
|
+
const DEFAULT_TIMEOUT_MS = 30_000;
|
|
11
|
+
/**
|
|
12
|
+
* Entry point for Sebpay hosted checkout. Server-side only — the secret key must never reach a
|
|
13
|
+
* browser bundle. Pass the key pair from your dashboard (API développeur): the sandbox and live
|
|
14
|
+
* are separate servers, and pk_test_ keys are sent to the sandbox, pk_live_ keys to live.
|
|
15
|
+
*/
|
|
16
|
+
class SebpayClient {
|
|
17
|
+
baseUrl;
|
|
18
|
+
checkoutSessions;
|
|
19
|
+
#publicKey;
|
|
20
|
+
#secretKey;
|
|
21
|
+
#timeout;
|
|
22
|
+
constructor(publicKey, secretKey, options = {}) {
|
|
23
|
+
this.#publicKey = publicKey;
|
|
24
|
+
this.#secretKey = secretKey;
|
|
25
|
+
this.#timeout = options.timeout ?? DEFAULT_TIMEOUT_MS;
|
|
26
|
+
this.baseUrl = (options.baseUrl ?? (publicKey.startsWith("pk_test_") ? SANDBOX_BASE_URL : LIVE_BASE_URL)).replace(/\/+$/, "");
|
|
27
|
+
this.checkoutSessions = new checkoutSessions_1.CheckoutSessions((method, path, body) => this.#request(method, path, body));
|
|
28
|
+
}
|
|
29
|
+
/** Returns the `data` field of a successful response; throws SebpayApiError otherwise. */
|
|
30
|
+
async #request(method, path, body) {
|
|
31
|
+
const headers = {
|
|
32
|
+
"X-Public-Key": this.#publicKey,
|
|
33
|
+
"X-Secret-Key": this.#secretKey,
|
|
34
|
+
Accept: "application/json",
|
|
35
|
+
"User-Agent": `sebpay-node/${version_1.VERSION}`,
|
|
36
|
+
};
|
|
37
|
+
if (body !== undefined)
|
|
38
|
+
headers["Content-Type"] = "application/json";
|
|
39
|
+
let res;
|
|
40
|
+
let text;
|
|
41
|
+
try {
|
|
42
|
+
res = await fetch(`${this.baseUrl}${path}`, {
|
|
43
|
+
method,
|
|
44
|
+
headers,
|
|
45
|
+
body: body === undefined ? undefined : JSON.stringify(body),
|
|
46
|
+
signal: AbortSignal.timeout(this.#timeout),
|
|
47
|
+
});
|
|
48
|
+
text = await res.text();
|
|
49
|
+
}
|
|
50
|
+
catch (err) {
|
|
51
|
+
if (err instanceof Error && err.name === "TimeoutError") {
|
|
52
|
+
throw new errors_1.SebpayApiError(`Sebpay request timed out after ${this.#timeout} ms`, 0, "TIMEOUT", null);
|
|
53
|
+
}
|
|
54
|
+
const cause = err instanceof Error ? (err.cause?.message ?? err.message) : String(err);
|
|
55
|
+
throw new errors_1.SebpayApiError(`Sebpay request failed: ${cause}`, 0, "NETWORK_ERROR", null);
|
|
56
|
+
}
|
|
57
|
+
let decoded = null;
|
|
58
|
+
try {
|
|
59
|
+
decoded = JSON.parse(text);
|
|
60
|
+
}
|
|
61
|
+
catch {
|
|
62
|
+
// not JSON: handled below
|
|
63
|
+
}
|
|
64
|
+
if (!res.ok || decoded?.success === false) {
|
|
65
|
+
const code = typeof decoded?.errors?.code === "string" ? decoded.errors.code : null;
|
|
66
|
+
throw new errors_1.SebpayApiError(decoded?.message ?? `Sebpay API error (HTTP ${res.status})`, res.status, code, decoded ?? text);
|
|
67
|
+
}
|
|
68
|
+
if (decoded === null || typeof decoded !== "object" || !("data" in decoded)) {
|
|
69
|
+
throw new errors_1.SebpayApiError("Sebpay API returned an unexpected response", res.status, "INVALID_RESPONSE", decoded ?? text);
|
|
70
|
+
}
|
|
71
|
+
return decoded.data;
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
exports.SebpayClient = SebpayClient;
|
package/dist/errors.d.ts
ADDED
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Thrown for every failed call. `status` is the HTTP status, or 0 when no response came back
|
|
3
|
+
* (`code` "NETWORK_ERROR" or "TIMEOUT"). `code` is the API's `errors.code` when it sends one
|
|
4
|
+
* (e.g. "EXTERNAL_REFERENCE_CONFLICT", "RATE_LIMITED", "WRONG_ENVIRONMENT"), else null.
|
|
5
|
+
* `body` is the decoded response.
|
|
6
|
+
*/
|
|
7
|
+
export declare class SebpayApiError extends Error {
|
|
8
|
+
readonly status: number;
|
|
9
|
+
readonly code: string | null;
|
|
10
|
+
readonly body: unknown;
|
|
11
|
+
constructor(message: string, status: number, code: string | null, body: unknown);
|
|
12
|
+
}
|
package/dist/errors.js
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.SebpayApiError = void 0;
|
|
4
|
+
/**
|
|
5
|
+
* Thrown for every failed call. `status` is the HTTP status, or 0 when no response came back
|
|
6
|
+
* (`code` "NETWORK_ERROR" or "TIMEOUT"). `code` is the API's `errors.code` when it sends one
|
|
7
|
+
* (e.g. "EXTERNAL_REFERENCE_CONFLICT", "RATE_LIMITED", "WRONG_ENVIRONMENT"), else null.
|
|
8
|
+
* `body` is the decoded response.
|
|
9
|
+
*/
|
|
10
|
+
class SebpayApiError extends Error {
|
|
11
|
+
status;
|
|
12
|
+
code;
|
|
13
|
+
body;
|
|
14
|
+
constructor(message, status, code, body) {
|
|
15
|
+
super(message);
|
|
16
|
+
this.status = status;
|
|
17
|
+
this.code = code;
|
|
18
|
+
this.body = body;
|
|
19
|
+
this.name = "SebpayApiError";
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
exports.SebpayApiError = SebpayApiError;
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
export { SebpayClient } from "./client";
|
|
2
|
+
export type { SebpayClientOptions } from "./client";
|
|
3
|
+
export { SebpayApiError } from "./errors";
|
|
4
|
+
export { SIGNATURE_HEADER, verifySignature } from "./webhooks";
|
|
5
|
+
export type { WebhookPayload } from "./webhooks";
|
|
6
|
+
export { VERSION } from "./version";
|
|
7
|
+
export type { CreateCheckoutSessionParams, CheckoutSession, CheckoutSessionDetails, CheckoutSessionStatus, } from "./resources/checkoutSessions";
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.VERSION = exports.verifySignature = exports.SIGNATURE_HEADER = exports.SebpayApiError = exports.SebpayClient = void 0;
|
|
4
|
+
var client_1 = require("./client");
|
|
5
|
+
Object.defineProperty(exports, "SebpayClient", { enumerable: true, get: function () { return client_1.SebpayClient; } });
|
|
6
|
+
var errors_1 = require("./errors");
|
|
7
|
+
Object.defineProperty(exports, "SebpayApiError", { enumerable: true, get: function () { return errors_1.SebpayApiError; } });
|
|
8
|
+
var webhooks_1 = require("./webhooks");
|
|
9
|
+
Object.defineProperty(exports, "SIGNATURE_HEADER", { enumerable: true, get: function () { return webhooks_1.SIGNATURE_HEADER; } });
|
|
10
|
+
Object.defineProperty(exports, "verifySignature", { enumerable: true, get: function () { return webhooks_1.verifySignature; } });
|
|
11
|
+
var version_1 = require("./version");
|
|
12
|
+
Object.defineProperty(exports, "VERSION", { enumerable: true, get: function () { return version_1.VERSION; } });
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import type { Requester } from "../client";
|
|
2
|
+
export interface CreateCheckoutSessionParams {
|
|
3
|
+
amount: number;
|
|
4
|
+
/** ISO code, e.g. "XOF". */
|
|
5
|
+
currency: string;
|
|
6
|
+
/** Where your customer lands after paying, with `?checkout_session_id=<id>` appended. */
|
|
7
|
+
redirect_url: string;
|
|
8
|
+
/** Your order id. Sending it again with the same parameters returns the same session (`idempotent_replay`). */
|
|
9
|
+
external_reference?: string;
|
|
10
|
+
/** Receives the signed webhook when the payment is approved or rejected (https). */
|
|
11
|
+
callback_url?: string;
|
|
12
|
+
customer_name?: string;
|
|
13
|
+
customer_email?: string;
|
|
14
|
+
customer_phone?: string;
|
|
15
|
+
/** Echoed back on the session and in its webhook. */
|
|
16
|
+
metadata?: Record<string, unknown>;
|
|
17
|
+
}
|
|
18
|
+
export type CheckoutSessionStatus = "open" | "completed" | "expired" | "canceled";
|
|
19
|
+
export interface CheckoutSession {
|
|
20
|
+
id: string;
|
|
21
|
+
status: CheckoutSessionStatus;
|
|
22
|
+
/** The Sebpay-hosted page to redirect your customer to. */
|
|
23
|
+
checkout_url: string;
|
|
24
|
+
amount: number;
|
|
25
|
+
currency: string;
|
|
26
|
+
external_reference: string | null;
|
|
27
|
+
metadata: Record<string, unknown> | null;
|
|
28
|
+
expires_at: string;
|
|
29
|
+
/** Present (true) when this external_reference had already created the session: it is returned as is. */
|
|
30
|
+
idempotent_replay?: true;
|
|
31
|
+
}
|
|
32
|
+
export interface CheckoutSessionDetails extends Omit<CheckoutSession, "idempotent_replay"> {
|
|
33
|
+
/** The latest payment attempt, if any. */
|
|
34
|
+
transaction_id: string | null;
|
|
35
|
+
/** "approved" is the only status that means paid. */
|
|
36
|
+
transaction_status: "pending" | "approved" | "rejected" | null;
|
|
37
|
+
created_at: string;
|
|
38
|
+
}
|
|
39
|
+
export declare class CheckoutSessions {
|
|
40
|
+
private readonly request;
|
|
41
|
+
constructor(request: Requester);
|
|
42
|
+
/** Create a hosted checkout session and get back a `checkout_url` to redirect your customer to. */
|
|
43
|
+
create(params: CreateCheckoutSessionParams): Promise<CheckoutSession>;
|
|
44
|
+
/**
|
|
45
|
+
* Retrieve a session by its id (the `checkout_session_id` of your redirect_url), its token or your
|
|
46
|
+
* external_reference. This is the server-to-server confirmation: when your customer comes back,
|
|
47
|
+
* deliver only if `transaction_status` is "approved" — the redirect alone proves nothing.
|
|
48
|
+
*/
|
|
49
|
+
retrieve(idOrReference: string): Promise<CheckoutSessionDetails>;
|
|
50
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.CheckoutSessions = void 0;
|
|
4
|
+
class CheckoutSessions {
|
|
5
|
+
request;
|
|
6
|
+
constructor(request) {
|
|
7
|
+
this.request = request;
|
|
8
|
+
}
|
|
9
|
+
/** Create a hosted checkout session and get back a `checkout_url` to redirect your customer to. */
|
|
10
|
+
create(params) {
|
|
11
|
+
return this.request("POST", "/checkout/sessions", { ...params });
|
|
12
|
+
}
|
|
13
|
+
/**
|
|
14
|
+
* Retrieve a session by its id (the `checkout_session_id` of your redirect_url), its token or your
|
|
15
|
+
* external_reference. This is the server-to-server confirmation: when your customer comes back,
|
|
16
|
+
* deliver only if `transaction_status` is "approved" — the redirect alone proves nothing.
|
|
17
|
+
*/
|
|
18
|
+
retrieve(idOrReference) {
|
|
19
|
+
return this.request("GET", `/checkout/sessions/${encodeURIComponent(idOrReference)}`);
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
exports.CheckoutSessions = CheckoutSessions;
|
package/dist/version.js
ADDED
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
/** The header carrying the signature of every Sebpay webhook. */
|
|
2
|
+
export declare const SIGNATURE_HEADER = "X-SebPay-Signature";
|
|
3
|
+
/** The body Sebpay POSTs to your callback_url once a payment is final. */
|
|
4
|
+
export interface WebhookPayload {
|
|
5
|
+
transaction_id: string;
|
|
6
|
+
external_reference: string | null;
|
|
7
|
+
/** Only final statuses are sent. */
|
|
8
|
+
status: "approved" | "rejected";
|
|
9
|
+
amount: number;
|
|
10
|
+
currency: string;
|
|
11
|
+
customer_phone: string | null;
|
|
12
|
+
created_at: string;
|
|
13
|
+
updated_at: string;
|
|
14
|
+
/** The checkout session this payment belongs to. */
|
|
15
|
+
checkout_session_id?: string;
|
|
16
|
+
/** The metadata you attached to that session. */
|
|
17
|
+
metadata?: Record<string, unknown> | null;
|
|
18
|
+
}
|
|
19
|
+
/**
|
|
20
|
+
* Verifies the `X-SebPay-Signature` header Sebpay sends with every webhook callback. Always pass
|
|
21
|
+
* the raw request body — re-serializing a parsed payload (e.g. via JSON.stringify after parsing)
|
|
22
|
+
* can reorder keys and break the signature. A missing signature or body returns false.
|
|
23
|
+
*/
|
|
24
|
+
export declare function verifySignature(rawBody: string | Uint8Array | null | undefined, signature: string | string[] | null | undefined, secretKey: string): boolean;
|
package/dist/webhooks.js
ADDED
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.SIGNATURE_HEADER = void 0;
|
|
4
|
+
exports.verifySignature = verifySignature;
|
|
5
|
+
const node_crypto_1 = require("node:crypto");
|
|
6
|
+
/** The header carrying the signature of every Sebpay webhook. */
|
|
7
|
+
exports.SIGNATURE_HEADER = "X-SebPay-Signature";
|
|
8
|
+
/**
|
|
9
|
+
* Verifies the `X-SebPay-Signature` header Sebpay sends with every webhook callback. Always pass
|
|
10
|
+
* the raw request body — re-serializing a parsed payload (e.g. via JSON.stringify after parsing)
|
|
11
|
+
* can reorder keys and break the signature. A missing signature or body returns false.
|
|
12
|
+
*/
|
|
13
|
+
function verifySignature(rawBody, signature, secretKey) {
|
|
14
|
+
if (typeof signature !== "string" || signature === "")
|
|
15
|
+
return false;
|
|
16
|
+
if (typeof rawBody !== "string" && !(rawBody instanceof Uint8Array))
|
|
17
|
+
return false;
|
|
18
|
+
const expected = (0, node_crypto_1.createHmac)("sha256", secretKey).update(rawBody).digest("hex");
|
|
19
|
+
const expectedBuf = Buffer.from(expected, "utf8");
|
|
20
|
+
const signatureBuf = Buffer.from(signature, "utf8");
|
|
21
|
+
if (expectedBuf.length !== signatureBuf.length) {
|
|
22
|
+
return false;
|
|
23
|
+
}
|
|
24
|
+
return (0, node_crypto_1.timingSafeEqual)(expectedBuf, signatureBuf);
|
|
25
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,42 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "sebpay-node",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"
|
|
5
|
-
"
|
|
6
|
-
|
|
3
|
+
"version": "0.2.0",
|
|
4
|
+
"description": "Official Node.js SDK for Sebpay hosted checkout — create a checkout session, redirect your customer, confirm the payment and verify webhooks. Server-side only.",
|
|
5
|
+
"license": "MIT",
|
|
6
|
+
"author": "Sebpay",
|
|
7
|
+
"homepage": "https://sebpay.africa",
|
|
8
|
+
"repository": {
|
|
9
|
+
"type": "git",
|
|
10
|
+
"url": "git+https://github.com/mjims/sebpay-sdk-js.git"
|
|
11
|
+
},
|
|
12
|
+
"bugs": {
|
|
13
|
+
"url": "https://github.com/mjims/sebpay-sdk-js/issues"
|
|
14
|
+
},
|
|
15
|
+
"keywords": [
|
|
16
|
+
"sebpay",
|
|
17
|
+
"payment",
|
|
18
|
+
"payments",
|
|
19
|
+
"checkout",
|
|
20
|
+
"mobile-money",
|
|
21
|
+
"africa",
|
|
22
|
+
"webhook",
|
|
23
|
+
"sdk"
|
|
24
|
+
],
|
|
25
|
+
"engines": {
|
|
26
|
+
"node": ">=18"
|
|
27
|
+
},
|
|
28
|
+
"main": "dist/index.js",
|
|
29
|
+
"types": "dist/index.d.ts",
|
|
30
|
+
"files": [
|
|
31
|
+
"dist"
|
|
32
|
+
],
|
|
33
|
+
"scripts": {
|
|
34
|
+
"build": "tsc",
|
|
35
|
+
"prepack": "npm run build",
|
|
36
|
+
"test": "npm run build && node --test test/*.test.mjs"
|
|
37
|
+
},
|
|
38
|
+
"devDependencies": {
|
|
39
|
+
"typescript": "^5.4.0",
|
|
40
|
+
"@types/node": "^20.0.0"
|
|
41
|
+
}
|
|
42
|
+
}
|