gardeshpay-sdk 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 +211 -2
- package/dist/index.cjs +307 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +177 -0
- package/dist/index.d.ts +177 -0
- package/dist/index.js +270 -0
- package/dist/index.js.map +1 -0
- package/package.json +57 -4
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 GardeshPay / PayGate365
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
CHANGED
|
@@ -1,3 +1,212 @@
|
|
|
1
|
-
#
|
|
1
|
+
# GardeshPay Node SDK (`gardeshpay-sdk`)
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
Official **Node.js SDK** for the **GardeshPay Merchant API**.
|
|
4
|
+
|
|
5
|
+
It wraps the payment APIs merchants call from their backend: create → redirect buyer → verify before fulfill, with **API key + HMAC-SHA256** request signing (and webhook signature helpers).
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
npm install gardeshpay-sdk
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
> The unscoped npm name `gardeshpay` is already taken by an unrelated 2020 package (`1.0.2`). This official SDK publishes as **`gardeshpay-sdk`**. Import class name is still `GardeshPay`.
|
|
12
|
+
|
|
13
|
+
> **Server-side only.** Never put `apiKey` / `hmacSecret` in browser code.
|
|
14
|
+
|
|
15
|
+
---
|
|
16
|
+
|
|
17
|
+
## What this package does
|
|
18
|
+
|
|
19
|
+
| You call | SDK does |
|
|
20
|
+
|----------|----------|
|
|
21
|
+
| `createPayment(...)` | `POST /v1/payments` + HMAC headers + idempotency |
|
|
22
|
+
| `verifyPayment(id)` | `POST /v1/payments/{id}/verify` (fulfillment gate) |
|
|
23
|
+
| `getPayment(id)` | `GET /v1/payments/{id}` |
|
|
24
|
+
| `createRefund(...)` | `POST /v1/payments/{id}/refunds` |
|
|
25
|
+
| `verifyWebhook(...)` | Check inbound webhook HMAC (notification only) |
|
|
26
|
+
|
|
27
|
+
Same HTTP API as cURL / OpenAPI — just less boilerplate.
|
|
28
|
+
|
|
29
|
+
---
|
|
30
|
+
|
|
31
|
+
## Quick start
|
|
32
|
+
|
|
33
|
+
```ts
|
|
34
|
+
import { GardeshPay } from "gardeshpay-sdk";
|
|
35
|
+
|
|
36
|
+
const gp = new GardeshPay({
|
|
37
|
+
apiKey: process.env.GARDESHPAY_API_KEY!,
|
|
38
|
+
hmacSecret: process.env.GARDESHPAY_HMAC_SECRET!,
|
|
39
|
+
baseUrl: process.env.GARDESHPAY_BASE_URL!, // e.g. https://api.example.com/v1
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
// 1) Create payment
|
|
43
|
+
const payment = await gp.createPayment({
|
|
44
|
+
orderRef: "ORD-1001",
|
|
45
|
+
amountRial: 250_000,
|
|
46
|
+
returnUrl: "https://shop.example.com/return",
|
|
47
|
+
description: "Order 1001",
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
// 2) Redirect the buyer
|
|
51
|
+
const redirectTo = payment.startPayUrl ?? payment.checkoutUrl;
|
|
52
|
+
// res.redirect(redirectTo)
|
|
53
|
+
|
|
54
|
+
// 3) On returnUrl — ALWAYS verify before fulfill
|
|
55
|
+
const verified = await gp.verifyPayment(payment.paymentId);
|
|
56
|
+
if (verified.status === "SUCCEEDED") {
|
|
57
|
+
// ship / unlock order
|
|
58
|
+
}
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
---
|
|
62
|
+
|
|
63
|
+
## Auth (HMAC)
|
|
64
|
+
|
|
65
|
+
Every request is signed as:
|
|
66
|
+
|
|
67
|
+
```text
|
|
68
|
+
METHOD\nPATH?QUERY\nTIMESTAMP\nNONCE\nSHA256_HEX(body)
|
|
69
|
+
X-Signature = Base64(HMAC-SHA256(hmacSecret, canonical))
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
Headers sent automatically:
|
|
73
|
+
|
|
74
|
+
- `X-Api-Key`
|
|
75
|
+
- `X-Timestamp` (unix seconds)
|
|
76
|
+
- `X-Nonce`
|
|
77
|
+
- `X-Signature`
|
|
78
|
+
- `Idempotency-Key` on create / verify / refund
|
|
79
|
+
|
|
80
|
+
Get keys from the merchant portal after your gateway is **ACTIVE** (API keys page).
|
|
81
|
+
|
|
82
|
+
---
|
|
83
|
+
|
|
84
|
+
## Webhooks (optional notifications)
|
|
85
|
+
|
|
86
|
+
Webhooks are **not** proof of payment. Always `verifyPayment` (or GET status) before fulfill.
|
|
87
|
+
|
|
88
|
+
```ts
|
|
89
|
+
import { verifyWebhook } from "gardeshpay-sdk";
|
|
90
|
+
import express from "express";
|
|
91
|
+
|
|
92
|
+
const app = express();
|
|
93
|
+
app.post(
|
|
94
|
+
"/webhooks/gardeshpay",
|
|
95
|
+
express.raw({ type: "application/json" }),
|
|
96
|
+
(req, res) => {
|
|
97
|
+
const result = verifyWebhook({
|
|
98
|
+
secret: process.env.GARDESHPAY_WEBHOOK_SECRET!,
|
|
99
|
+
rawBody: req.body as Buffer,
|
|
100
|
+
headers: req.headers,
|
|
101
|
+
});
|
|
102
|
+
if (!result.ok) return res.status(401).send(result.reason);
|
|
103
|
+
|
|
104
|
+
// enqueue background verifyPayment(paymentId) — then fulfill
|
|
105
|
+
res.sendStatus(200);
|
|
106
|
+
},
|
|
107
|
+
);
|
|
108
|
+
```
|
|
109
|
+
|
|
110
|
+
Configure the webhook URL + secret on the gateway settings page in the portal.
|
|
111
|
+
|
|
112
|
+
`verifyWebhook` validates the signed delivery headers for you. You still must verify the payment; delivery success ≠ money settled.
|
|
113
|
+
|
|
114
|
+
---
|
|
115
|
+
|
|
116
|
+
## API reference
|
|
117
|
+
|
|
118
|
+
### `new GardeshPay(options)`
|
|
119
|
+
|
|
120
|
+
| Option | Required | Description |
|
|
121
|
+
|--------|----------|-------------|
|
|
122
|
+
| `apiKey` | yes | Portal API key (`pk_…`) |
|
|
123
|
+
| `hmacSecret` | yes | HMAC secret for that key |
|
|
124
|
+
| `baseUrl` | yes | Base URL **including** `/v1` |
|
|
125
|
+
| `timeoutMs` | no | Default `30000` |
|
|
126
|
+
| `fetch` | no | Custom fetch (tests) |
|
|
127
|
+
|
|
128
|
+
### `createPayment(input)`
|
|
129
|
+
|
|
130
|
+
| Field | Required | Notes |
|
|
131
|
+
|-------|----------|-------|
|
|
132
|
+
| `orderRef` | yes | Your order id |
|
|
133
|
+
| `amountRial` | yes | Integer Rials |
|
|
134
|
+
| `returnUrl` | yes | Buyer browser return (not proof) |
|
|
135
|
+
| `callbackUrl` | no | Optional webhook override if allowed |
|
|
136
|
+
| `description` | no | |
|
|
137
|
+
| `expiresInSec` | no | |
|
|
138
|
+
| `metadata` | no | string map |
|
|
139
|
+
| `idempotencyKey` | no | Auto UUID if omitted |
|
|
140
|
+
|
|
141
|
+
Returns `paymentId`, `authority`, `checkoutUrl`, `startPayUrl` (when present), `status`, …
|
|
142
|
+
|
|
143
|
+
### `verifyPayment(paymentId)`
|
|
144
|
+
|
|
145
|
+
Fulfill only when `status === "SUCCEEDED"`. Safe to retry (idempotent).
|
|
146
|
+
|
|
147
|
+
### `getPayment(paymentId)` / `createRefund({ paymentId, amountRial })`
|
|
148
|
+
|
|
149
|
+
Status inquiry and refunds.
|
|
150
|
+
|
|
151
|
+
### Errors
|
|
152
|
+
|
|
153
|
+
Failed HTTP calls throw `GardeshPayError` with `status`, optional `code` / `correlationId`, and problem-details body when present.
|
|
154
|
+
|
|
155
|
+
---
|
|
156
|
+
|
|
157
|
+
## Payment flow
|
|
158
|
+
|
|
159
|
+
```text
|
|
160
|
+
Merchant backend GardeshPay API Buyer
|
|
161
|
+
|-- createPayment ---------->| |
|
|
162
|
+
|<-- paymentId + URLs -------| |
|
|
163
|
+
|--------- redirect buyer ------------------------------>|
|
|
164
|
+
| |<-- pay at bank/checkout ---|
|
|
165
|
+
|<-- returnUrl?paymentId ----| |
|
|
166
|
+
|-- verifyPayment ---------->| |
|
|
167
|
+
|<-- SUCCEEDED / … ----------| |
|
|
168
|
+
|-- fulfill order -----------| |
|
|
169
|
+
```
|
|
170
|
+
|
|
171
|
+
Optional: webhook `payment.succeeded` may arrive in parallel — still verify.
|
|
172
|
+
|
|
173
|
+
---
|
|
174
|
+
|
|
175
|
+
## TypeScript
|
|
176
|
+
|
|
177
|
+
Published with full `.d.ts`. Works with ESM and CommonJS:
|
|
178
|
+
|
|
179
|
+
```ts
|
|
180
|
+
import { GardeshPay } from "gardeshpay-sdk";
|
|
181
|
+
// or
|
|
182
|
+
const { GardeshPay } = require("gardeshpay-sdk");
|
|
183
|
+
```
|
|
184
|
+
|
|
185
|
+
Node **18+** (native `fetch` + `node:crypto`).
|
|
186
|
+
|
|
187
|
+
---
|
|
188
|
+
|
|
189
|
+
## Development
|
|
190
|
+
|
|
191
|
+
```bash
|
|
192
|
+
npm install
|
|
193
|
+
npm test
|
|
194
|
+
npm run build
|
|
195
|
+
```
|
|
196
|
+
|
|
197
|
+
---
|
|
198
|
+
|
|
199
|
+
## Security checklist
|
|
200
|
+
|
|
201
|
+
- [ ] Store secrets in env / secret manager — never in git or frontend
|
|
202
|
+
- [ ] Use HTTPS `returnUrl` / webhook URL
|
|
203
|
+
- [ ] Treat `returnUrl` query as a hint only
|
|
204
|
+
- [ ] Treat webhook as a hint only
|
|
205
|
+
- [ ] Fulfill only after `verifyPayment` → `SUCCEEDED`
|
|
206
|
+
- [ ] Use unique `orderRef` / idempotency keys
|
|
207
|
+
|
|
208
|
+
---
|
|
209
|
+
|
|
210
|
+
## License
|
|
211
|
+
|
|
212
|
+
MIT © GardeshPay
|
package/dist/index.cjs
ADDED
|
@@ -0,0 +1,307 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __defProp = Object.defineProperty;
|
|
3
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
4
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
5
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
6
|
+
var __export = (target, all) => {
|
|
7
|
+
for (var name in all)
|
|
8
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
9
|
+
};
|
|
10
|
+
var __copyProps = (to, from, except, desc) => {
|
|
11
|
+
if (from && typeof from === "object" || typeof from === "function") {
|
|
12
|
+
for (let key of __getOwnPropNames(from))
|
|
13
|
+
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
14
|
+
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
15
|
+
}
|
|
16
|
+
return to;
|
|
17
|
+
};
|
|
18
|
+
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
19
|
+
|
|
20
|
+
// src/index.ts
|
|
21
|
+
var index_exports = {};
|
|
22
|
+
__export(index_exports, {
|
|
23
|
+
GardeshPay: () => GardeshPay,
|
|
24
|
+
GardeshPayError: () => GardeshPayError,
|
|
25
|
+
hmacSha256Base64: () => hmacSha256Base64,
|
|
26
|
+
hmacSha256Hex: () => hmacSha256Hex,
|
|
27
|
+
merchantCanonical: () => merchantCanonical,
|
|
28
|
+
newIdempotencyKey: () => newIdempotencyKey,
|
|
29
|
+
newNonce: () => newNonce,
|
|
30
|
+
sha256Hex: () => sha256Hex,
|
|
31
|
+
signMerchantRequest: () => signMerchantRequest,
|
|
32
|
+
unixSeconds: () => unixSeconds,
|
|
33
|
+
verifyWebhook: () => verifyWebhook
|
|
34
|
+
});
|
|
35
|
+
module.exports = __toCommonJS(index_exports);
|
|
36
|
+
|
|
37
|
+
// src/errors.ts
|
|
38
|
+
var GardeshPayError = class extends Error {
|
|
39
|
+
status;
|
|
40
|
+
code;
|
|
41
|
+
correlationId;
|
|
42
|
+
problem;
|
|
43
|
+
bodyText;
|
|
44
|
+
constructor(message, opts) {
|
|
45
|
+
super(message);
|
|
46
|
+
this.name = "GardeshPayError";
|
|
47
|
+
this.status = opts.status;
|
|
48
|
+
this.code = opts.code;
|
|
49
|
+
this.correlationId = opts.correlationId;
|
|
50
|
+
this.problem = opts.problem;
|
|
51
|
+
this.bodyText = opts.bodyText;
|
|
52
|
+
}
|
|
53
|
+
};
|
|
54
|
+
|
|
55
|
+
// src/hmac.ts
|
|
56
|
+
var import_node_crypto = require("crypto");
|
|
57
|
+
function sha256Hex(body) {
|
|
58
|
+
return (0, import_node_crypto.createHash)("sha256").update(body ?? "").digest("hex");
|
|
59
|
+
}
|
|
60
|
+
function merchantCanonical(method, pathWithQuery, timestamp, nonce, bodySha256Hex) {
|
|
61
|
+
return [method, pathWithQuery, timestamp, nonce, bodySha256Hex].join("\n");
|
|
62
|
+
}
|
|
63
|
+
function hmacSha256Base64(secret, canonical) {
|
|
64
|
+
return (0, import_node_crypto.createHmac)("sha256", secret).update(canonical, "utf8").digest("base64");
|
|
65
|
+
}
|
|
66
|
+
function hmacSha256Hex(secret, message) {
|
|
67
|
+
return (0, import_node_crypto.createHmac)("sha256", secret).update(message, "utf8").digest("hex");
|
|
68
|
+
}
|
|
69
|
+
function signMerchantRequest(opts) {
|
|
70
|
+
const bodyHash = sha256Hex(opts.body);
|
|
71
|
+
const canonical = merchantCanonical(
|
|
72
|
+
opts.method.toUpperCase(),
|
|
73
|
+
opts.pathWithQuery,
|
|
74
|
+
opts.timestamp,
|
|
75
|
+
opts.nonce,
|
|
76
|
+
bodyHash
|
|
77
|
+
);
|
|
78
|
+
return hmacSha256Base64(opts.secret, canonical);
|
|
79
|
+
}
|
|
80
|
+
function newNonce() {
|
|
81
|
+
return `n-${(0, import_node_crypto.randomUUID)()}`;
|
|
82
|
+
}
|
|
83
|
+
function newIdempotencyKey() {
|
|
84
|
+
return (0, import_node_crypto.randomUUID)();
|
|
85
|
+
}
|
|
86
|
+
function unixSeconds(now = Date.now()) {
|
|
87
|
+
return String(Math.floor(now / 1e3));
|
|
88
|
+
}
|
|
89
|
+
function safeEqualHex(a, b) {
|
|
90
|
+
try {
|
|
91
|
+
const left = Buffer.from(a, "utf8");
|
|
92
|
+
const right = Buffer.from(b, "utf8");
|
|
93
|
+
if (left.length !== right.length) return false;
|
|
94
|
+
return (0, import_node_crypto.timingSafeEqual)(left, right);
|
|
95
|
+
} catch {
|
|
96
|
+
return false;
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
// src/client.ts
|
|
101
|
+
var GardeshPay = class {
|
|
102
|
+
apiKey;
|
|
103
|
+
hmacSecret;
|
|
104
|
+
baseUrl;
|
|
105
|
+
fetchImpl;
|
|
106
|
+
timeoutMs;
|
|
107
|
+
constructor(options) {
|
|
108
|
+
if (!options?.apiKey?.trim()) throw new Error("apiKey is required");
|
|
109
|
+
if (!options?.hmacSecret?.trim()) throw new Error("hmacSecret is required");
|
|
110
|
+
if (!options?.baseUrl?.trim()) throw new Error("baseUrl is required (include /v1)");
|
|
111
|
+
this.apiKey = options.apiKey.trim();
|
|
112
|
+
this.hmacSecret = options.hmacSecret.trim();
|
|
113
|
+
this.baseUrl = options.baseUrl.replace(/\/+$/, "");
|
|
114
|
+
this.fetchImpl = options.fetch ?? fetch;
|
|
115
|
+
this.timeoutMs = options.timeoutMs ?? 3e4;
|
|
116
|
+
}
|
|
117
|
+
/** Create a payment and get checkout / start-pay URLs. */
|
|
118
|
+
async createPayment(input) {
|
|
119
|
+
const data = await this.request({
|
|
120
|
+
method: "POST",
|
|
121
|
+
path: "/payments",
|
|
122
|
+
body: {
|
|
123
|
+
orderRef: input.orderRef,
|
|
124
|
+
amountRial: input.amountRial,
|
|
125
|
+
returnUrl: input.returnUrl,
|
|
126
|
+
callbackUrl: input.callbackUrl,
|
|
127
|
+
description: input.description,
|
|
128
|
+
expiresInSec: input.expiresInSec,
|
|
129
|
+
metadata: input.metadata,
|
|
130
|
+
gatewayId: input.gatewayId
|
|
131
|
+
},
|
|
132
|
+
idempotencyKey: input.idempotencyKey ?? newIdempotencyKey(),
|
|
133
|
+
correlationId: input.correlationId
|
|
134
|
+
});
|
|
135
|
+
return data;
|
|
136
|
+
}
|
|
137
|
+
/** GET payment status (masked). */
|
|
138
|
+
async getPayment(paymentId) {
|
|
139
|
+
return this.request({
|
|
140
|
+
method: "GET",
|
|
141
|
+
path: `/payments/${encodeURIComponent(paymentId)}`
|
|
142
|
+
});
|
|
143
|
+
}
|
|
144
|
+
/**
|
|
145
|
+
* Authoritative verify before fulfillment (idempotent).
|
|
146
|
+
* Fulfill only when `status === "SUCCEEDED"`.
|
|
147
|
+
*/
|
|
148
|
+
async verifyPayment(paymentId, opts) {
|
|
149
|
+
return this.request({
|
|
150
|
+
method: "POST",
|
|
151
|
+
path: `/payments/${encodeURIComponent(paymentId)}/verify`,
|
|
152
|
+
// Empty body — matches typical verify clients; HMAC hashes empty string.
|
|
153
|
+
idempotencyKey: opts?.idempotencyKey ?? newIdempotencyKey()
|
|
154
|
+
});
|
|
155
|
+
}
|
|
156
|
+
/** Create a refund against a succeeded payment. */
|
|
157
|
+
async createRefund(input) {
|
|
158
|
+
return this.request({
|
|
159
|
+
method: "POST",
|
|
160
|
+
path: `/payments/${encodeURIComponent(input.paymentId)}/refunds`,
|
|
161
|
+
body: {
|
|
162
|
+
amountRial: input.amountRial,
|
|
163
|
+
reason: input.reason
|
|
164
|
+
},
|
|
165
|
+
idempotencyKey: input.idempotencyKey ?? newIdempotencyKey()
|
|
166
|
+
});
|
|
167
|
+
}
|
|
168
|
+
async request(opts) {
|
|
169
|
+
const pathWithQuery = opts.path.startsWith("/") ? opts.path : `/${opts.path}`;
|
|
170
|
+
const url = `${this.baseUrl}${pathWithQuery}`;
|
|
171
|
+
const bodyText = opts.method === "GET" || opts.body === void 0 ? "" : JSON.stringify(opts.body ?? {});
|
|
172
|
+
const timestamp = unixSeconds();
|
|
173
|
+
const nonce = newNonce();
|
|
174
|
+
const signPath = `/v1${pathWithQuery}`;
|
|
175
|
+
const signature = signMerchantRequest({
|
|
176
|
+
secret: this.hmacSecret,
|
|
177
|
+
method: opts.method,
|
|
178
|
+
pathWithQuery: signPath,
|
|
179
|
+
timestamp,
|
|
180
|
+
nonce,
|
|
181
|
+
body: bodyText
|
|
182
|
+
});
|
|
183
|
+
const headers = {
|
|
184
|
+
Accept: "application/json",
|
|
185
|
+
"X-Api-Key": this.apiKey,
|
|
186
|
+
"X-Timestamp": timestamp,
|
|
187
|
+
"X-Nonce": nonce,
|
|
188
|
+
"X-Signature": signature
|
|
189
|
+
};
|
|
190
|
+
if (bodyText) headers["Content-Type"] = "application/json";
|
|
191
|
+
if (opts.idempotencyKey) headers["Idempotency-Key"] = opts.idempotencyKey;
|
|
192
|
+
if (opts.correlationId) headers["X-Correlation-Id"] = opts.correlationId;
|
|
193
|
+
const controller = new AbortController();
|
|
194
|
+
const timer = setTimeout(() => controller.abort(), this.timeoutMs);
|
|
195
|
+
let res;
|
|
196
|
+
try {
|
|
197
|
+
res = await this.fetchImpl(url, {
|
|
198
|
+
method: opts.method,
|
|
199
|
+
headers,
|
|
200
|
+
body: bodyText || void 0,
|
|
201
|
+
signal: controller.signal
|
|
202
|
+
});
|
|
203
|
+
} catch (err) {
|
|
204
|
+
clearTimeout(timer);
|
|
205
|
+
throw new GardeshPayError(
|
|
206
|
+
err instanceof Error ? err.message : "network error",
|
|
207
|
+
{ status: 0 }
|
|
208
|
+
);
|
|
209
|
+
} finally {
|
|
210
|
+
clearTimeout(timer);
|
|
211
|
+
}
|
|
212
|
+
const text = await res.text();
|
|
213
|
+
let json = void 0;
|
|
214
|
+
if (text) {
|
|
215
|
+
try {
|
|
216
|
+
json = JSON.parse(text);
|
|
217
|
+
} catch {
|
|
218
|
+
json = void 0;
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
if (!res.ok) {
|
|
222
|
+
const problem = json && typeof json === "object" ? json : void 0;
|
|
223
|
+
throw new GardeshPayError(problem?.detail || problem?.title || `HTTP ${res.status}`, {
|
|
224
|
+
status: res.status,
|
|
225
|
+
code: problem?.code,
|
|
226
|
+
correlationId: problem?.correlationId,
|
|
227
|
+
problem,
|
|
228
|
+
bodyText: text
|
|
229
|
+
});
|
|
230
|
+
}
|
|
231
|
+
if (json && typeof json === "object" && "data" in json) {
|
|
232
|
+
return json.data;
|
|
233
|
+
}
|
|
234
|
+
return json;
|
|
235
|
+
}
|
|
236
|
+
};
|
|
237
|
+
|
|
238
|
+
// src/webhooks.ts
|
|
239
|
+
function header(headers, name) {
|
|
240
|
+
const direct = headers[name] ?? headers[name.toLowerCase()];
|
|
241
|
+
if (Array.isArray(direct)) return direct[0];
|
|
242
|
+
if (typeof direct === "string") return direct;
|
|
243
|
+
const key = Object.keys(headers).find((k) => k.toLowerCase() === name.toLowerCase());
|
|
244
|
+
if (!key) return void 0;
|
|
245
|
+
const v = headers[key];
|
|
246
|
+
return Array.isArray(v) ? v[0] : v;
|
|
247
|
+
}
|
|
248
|
+
function verifyWebhook(opts) {
|
|
249
|
+
const skew = opts.skewSeconds ?? 300;
|
|
250
|
+
const now = opts.nowSeconds ?? Math.floor(Date.now() / 1e3);
|
|
251
|
+
const raw = typeof opts.rawBody === "string" ? opts.rawBody : opts.rawBody.toString("utf8");
|
|
252
|
+
const paygateSig = header(opts.headers, "X-PayGate-Signature");
|
|
253
|
+
const paygateTs = header(opts.headers, "X-PayGate-Timestamp");
|
|
254
|
+
if (paygateSig && paygateTs) {
|
|
255
|
+
const ts = Number(paygateTs);
|
|
256
|
+
if (!Number.isFinite(ts) || Math.abs(now - ts) > skew) {
|
|
257
|
+
return { ok: false, reason: "timestamp skew" };
|
|
258
|
+
}
|
|
259
|
+
const expectedHex = hmacSha256Hex(opts.secret, `${paygateTs}.${raw}`);
|
|
260
|
+
const presented = paygateSig.replace(/^sha256=/i, "").trim();
|
|
261
|
+
if (!safeEqualHex(expectedHex, presented)) {
|
|
262
|
+
return { ok: false, reason: "invalid signature" };
|
|
263
|
+
}
|
|
264
|
+
return {
|
|
265
|
+
ok: true,
|
|
266
|
+
timestamp: paygateTs,
|
|
267
|
+
eventType: header(opts.headers, "X-PayGate-Event")
|
|
268
|
+
};
|
|
269
|
+
}
|
|
270
|
+
const pg365 = header(opts.headers, "X-PG365-Signature");
|
|
271
|
+
if (pg365) {
|
|
272
|
+
const parts = Object.fromEntries(
|
|
273
|
+
pg365.split(",").map((p) => {
|
|
274
|
+
const [k, ...rest] = p.trim().split("=");
|
|
275
|
+
return [k, rest.join("=")];
|
|
276
|
+
})
|
|
277
|
+
);
|
|
278
|
+
const t = parts.t;
|
|
279
|
+
const v1 = parts.v1;
|
|
280
|
+
if (!t || !v1) return { ok: false, reason: "malformed X-PG365-Signature" };
|
|
281
|
+
const ts = Number(t);
|
|
282
|
+
if (!Number.isFinite(ts) || Math.abs(now - ts) > skew) {
|
|
283
|
+
return { ok: false, reason: "timestamp skew" };
|
|
284
|
+
}
|
|
285
|
+
const expected = hmacSha256Hex(opts.secret, `${t}.${raw}`);
|
|
286
|
+
if (!safeEqualHex(expected, v1)) {
|
|
287
|
+
return { ok: false, reason: "invalid signature" };
|
|
288
|
+
}
|
|
289
|
+
return { ok: true, timestamp: t };
|
|
290
|
+
}
|
|
291
|
+
return { ok: false, reason: "missing webhook signature headers" };
|
|
292
|
+
}
|
|
293
|
+
// Annotate the CommonJS export names for ESM import in node:
|
|
294
|
+
0 && (module.exports = {
|
|
295
|
+
GardeshPay,
|
|
296
|
+
GardeshPayError,
|
|
297
|
+
hmacSha256Base64,
|
|
298
|
+
hmacSha256Hex,
|
|
299
|
+
merchantCanonical,
|
|
300
|
+
newIdempotencyKey,
|
|
301
|
+
newNonce,
|
|
302
|
+
sha256Hex,
|
|
303
|
+
signMerchantRequest,
|
|
304
|
+
unixSeconds,
|
|
305
|
+
verifyWebhook
|
|
306
|
+
});
|
|
307
|
+
//# sourceMappingURL=index.cjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/index.ts","../src/errors.ts","../src/hmac.ts","../src/client.ts","../src/webhooks.ts"],"sourcesContent":["export { GardeshPay } from \"./client.js\";\nexport { GardeshPayError } from \"./errors.js\";\nexport {\n hmacSha256Base64,\n hmacSha256Hex,\n merchantCanonical,\n newIdempotencyKey,\n newNonce,\n sha256Hex,\n signMerchantRequest,\n unixSeconds,\n} from \"./hmac.js\";\nexport { verifyWebhook } from \"./webhooks.js\";\nexport type {\n CreatePaymentInput,\n CreatePaymentResult,\n CreateRefundInput,\n GardeshPayOptions,\n PaymentResult,\n PaymentStatus,\n ProblemDetails,\n RefundResult,\n VerifyPaymentResult,\n} from \"./types.js\";\n","import type { ProblemDetails } from \"./types.js\";\n\nexport class GardeshPayError extends Error {\n readonly status: number;\n readonly code?: string;\n readonly correlationId?: string;\n readonly problem?: ProblemDetails;\n readonly bodyText?: string;\n\n constructor(\n message: string,\n opts: {\n status: number;\n code?: string;\n correlationId?: string;\n problem?: ProblemDetails;\n bodyText?: string;\n },\n ) {\n super(message);\n this.name = \"GardeshPayError\";\n this.status = opts.status;\n this.code = opts.code;\n this.correlationId = opts.correlationId;\n this.problem = opts.problem;\n this.bodyText = opts.bodyText;\n }\n}\n","import { createHash, createHmac, randomUUID, timingSafeEqual } from \"node:crypto\";\n\n/** SHA-256 hex of UTF-8 body (empty string when body is empty). */\nexport function sha256Hex(body: string | Buffer | Uint8Array): string {\n return createHash(\"sha256\").update(body ?? \"\").digest(\"hex\");\n}\n\n/**\n * Merchant API canonical string (must match Java `HmacSigner.merchantCanonical`):\n * `METHOD\\nPATH?QUERY\\nTIMESTAMP\\nNONCE\\nSHA256_HEX(body)`\n */\nexport function merchantCanonical(\n method: string,\n pathWithQuery: string,\n timestamp: string,\n nonce: string,\n bodySha256Hex: string,\n): string {\n return [method, pathWithQuery, timestamp, nonce, bodySha256Hex].join(\"\\n\");\n}\n\n/** Base64(HMAC-SHA256(secret, canonical)) — value for `X-Signature`. */\nexport function hmacSha256Base64(secret: string, canonical: string): string {\n return createHmac(\"sha256\", secret).update(canonical, \"utf8\").digest(\"base64\");\n}\n\n/** Hex(HMAC-SHA256(secret, message)) — used for live webhook signatures. */\nexport function hmacSha256Hex(secret: string, message: string): string {\n return createHmac(\"sha256\", secret).update(message, \"utf8\").digest(\"hex\");\n}\n\nexport function signMerchantRequest(opts: {\n secret: string;\n method: string;\n pathWithQuery: string;\n timestamp: string;\n nonce: string;\n body: string;\n}): string {\n const bodyHash = sha256Hex(opts.body);\n const canonical = merchantCanonical(\n opts.method.toUpperCase(),\n opts.pathWithQuery,\n opts.timestamp,\n opts.nonce,\n bodyHash,\n );\n return hmacSha256Base64(opts.secret, canonical);\n}\n\nexport function newNonce(): string {\n return `n-${randomUUID()}`;\n}\n\nexport function newIdempotencyKey(): string {\n return randomUUID();\n}\n\nexport function unixSeconds(now = Date.now()): string {\n return String(Math.floor(now / 1000));\n}\n\nexport function safeEqualHex(a: string, b: string): boolean {\n try {\n const left = Buffer.from(a, \"utf8\");\n const right = Buffer.from(b, \"utf8\");\n if (left.length !== right.length) return false;\n return timingSafeEqual(left, right);\n } catch {\n return false;\n }\n}\n","import { GardeshPayError } from \"./errors.js\";\nimport { newIdempotencyKey, newNonce, signMerchantRequest, unixSeconds } from \"./hmac.js\";\nimport type {\n CreatePaymentInput,\n CreatePaymentResult,\n CreateRefundInput,\n GardeshPayOptions,\n PaymentResult,\n ProblemDetails,\n RefundResult,\n VerifyPaymentResult,\n} from \"./types.js\";\n\ntype RequestOpts = {\n method: \"GET\" | \"POST\";\n path: string;\n body?: unknown;\n idempotencyKey?: string;\n correlationId?: string;\n};\n\nexport class GardeshPay {\n readonly apiKey: string;\n readonly hmacSecret: string;\n readonly baseUrl: string;\n private readonly fetchImpl: typeof fetch;\n private readonly timeoutMs: number;\n\n constructor(options: GardeshPayOptions) {\n if (!options?.apiKey?.trim()) throw new Error(\"apiKey is required\");\n if (!options?.hmacSecret?.trim()) throw new Error(\"hmacSecret is required\");\n if (!options?.baseUrl?.trim()) throw new Error(\"baseUrl is required (include /v1)\");\n this.apiKey = options.apiKey.trim();\n this.hmacSecret = options.hmacSecret.trim();\n this.baseUrl = options.baseUrl.replace(/\\/+$/, \"\");\n this.fetchImpl = options.fetch ?? fetch;\n this.timeoutMs = options.timeoutMs ?? 30_000;\n }\n\n /** Create a payment and get checkout / start-pay URLs. */\n async createPayment(input: CreatePaymentInput): Promise<CreatePaymentResult> {\n const data = await this.request<CreatePaymentResult>({\n method: \"POST\",\n path: \"/payments\",\n body: {\n orderRef: input.orderRef,\n amountRial: input.amountRial,\n returnUrl: input.returnUrl,\n callbackUrl: input.callbackUrl,\n description: input.description,\n expiresInSec: input.expiresInSec,\n metadata: input.metadata,\n gatewayId: input.gatewayId,\n },\n idempotencyKey: input.idempotencyKey ?? newIdempotencyKey(),\n correlationId: input.correlationId,\n });\n return data;\n }\n\n /** GET payment status (masked). */\n async getPayment(paymentId: string): Promise<PaymentResult> {\n return this.request<PaymentResult>({\n method: \"GET\",\n path: `/payments/${encodeURIComponent(paymentId)}`,\n });\n }\n\n /**\n * Authoritative verify before fulfillment (idempotent).\n * Fulfill only when `status === \"SUCCEEDED\"`.\n */\n async verifyPayment(\n paymentId: string,\n opts?: { idempotencyKey?: string },\n ): Promise<VerifyPaymentResult> {\n return this.request<VerifyPaymentResult>({\n method: \"POST\",\n path: `/payments/${encodeURIComponent(paymentId)}/verify`,\n // Empty body — matches typical verify clients; HMAC hashes empty string.\n idempotencyKey: opts?.idempotencyKey ?? newIdempotencyKey(),\n });\n }\n\n /** Create a refund against a succeeded payment. */\n async createRefund(input: CreateRefundInput): Promise<RefundResult> {\n return this.request<RefundResult>({\n method: \"POST\",\n path: `/payments/${encodeURIComponent(input.paymentId)}/refunds`,\n body: {\n amountRial: input.amountRial,\n reason: input.reason,\n },\n idempotencyKey: input.idempotencyKey ?? newIdempotencyKey(),\n });\n }\n\n private async request<T>(opts: RequestOpts): Promise<T> {\n const pathWithQuery = opts.path.startsWith(\"/\") ? opts.path : `/${opts.path}`;\n const url = `${this.baseUrl}${pathWithQuery}`;\n const bodyText =\n opts.method === \"GET\" || opts.body === undefined ? \"\" : JSON.stringify(opts.body ?? {});\n const timestamp = unixSeconds();\n const nonce = newNonce();\n // Sign against the public path as exposed under /v1 (matches Java filter).\n const signPath = `/v1${pathWithQuery}`;\n const signature = signMerchantRequest({\n secret: this.hmacSecret,\n method: opts.method,\n pathWithQuery: signPath,\n timestamp,\n nonce,\n body: bodyText,\n });\n\n const headers: Record<string, string> = {\n Accept: \"application/json\",\n \"X-Api-Key\": this.apiKey,\n \"X-Timestamp\": timestamp,\n \"X-Nonce\": nonce,\n \"X-Signature\": signature,\n };\n if (bodyText) headers[\"Content-Type\"] = \"application/json\";\n if (opts.idempotencyKey) headers[\"Idempotency-Key\"] = opts.idempotencyKey;\n if (opts.correlationId) headers[\"X-Correlation-Id\"] = opts.correlationId;\n\n const controller = new AbortController();\n const timer = setTimeout(() => controller.abort(), this.timeoutMs);\n let res: Response;\n try {\n res = await this.fetchImpl(url, {\n method: opts.method,\n headers,\n body: bodyText || undefined,\n signal: controller.signal,\n });\n } catch (err) {\n clearTimeout(timer);\n throw new GardeshPayError(\n err instanceof Error ? err.message : \"network error\",\n { status: 0 },\n );\n } finally {\n clearTimeout(timer);\n }\n\n const text = await res.text();\n let json: unknown = undefined;\n if (text) {\n try {\n json = JSON.parse(text);\n } catch {\n json = undefined;\n }\n }\n\n if (!res.ok) {\n const problem = (json && typeof json === \"object\" ? json : undefined) as\n | ProblemDetails\n | undefined;\n throw new GardeshPayError(problem?.detail || problem?.title || `HTTP ${res.status}`, {\n status: res.status,\n code: problem?.code,\n correlationId: problem?.correlationId,\n problem,\n bodyText: text,\n });\n }\n\n if (json && typeof json === \"object\" && \"data\" in json) {\n return (json as { data: T }).data;\n }\n return json as T;\n }\n}\n","import { hmacSha256Hex, safeEqualHex } from \"./hmac.js\";\n\nexport type WebhookHeaders = Record<string, string | string[] | undefined>;\n\nfunction header(headers: WebhookHeaders, name: string): string | undefined {\n const direct = headers[name] ?? headers[name.toLowerCase()];\n if (Array.isArray(direct)) return direct[0];\n if (typeof direct === \"string\") return direct;\n // case-insensitive fallback\n const key = Object.keys(headers).find((k) => k.toLowerCase() === name.toLowerCase());\n if (!key) return undefined;\n const v = headers[key];\n return Array.isArray(v) ? v[0] : v;\n}\n\n/**\n * Verify an inbound GardeshPay / PayGate365 webhook.\n *\n * Live notification-service signs with:\n * - `X-PayGate-Signature: sha256=<hex(HMAC-SHA256(secret, `${t}.${rawBody}`))>`\n * - `X-PayGate-Timestamp: <unix seconds>`\n *\n * Docs also mention `X-PG365-Signature: t=…,v1=…` — both are accepted.\n *\n * Webhooks are **notifications only**. Always call `payments.verify` (or GET)\n * before fulfilling an order.\n */\nexport function verifyWebhook(opts: {\n secret: string;\n rawBody: string | Buffer;\n headers: WebhookHeaders;\n /** Max clock skew in seconds (default 300). */\n skewSeconds?: number;\n nowSeconds?: number;\n}): { ok: true; timestamp: string; eventType?: string } | { ok: false; reason: string } {\n const skew = opts.skewSeconds ?? 300;\n const now = opts.nowSeconds ?? Math.floor(Date.now() / 1000);\n const raw = typeof opts.rawBody === \"string\" ? opts.rawBody : opts.rawBody.toString(\"utf8\");\n\n const paygateSig = header(opts.headers, \"X-PayGate-Signature\");\n const paygateTs = header(opts.headers, \"X-PayGate-Timestamp\");\n if (paygateSig && paygateTs) {\n const ts = Number(paygateTs);\n if (!Number.isFinite(ts) || Math.abs(now - ts) > skew) {\n return { ok: false, reason: \"timestamp skew\" };\n }\n const expectedHex = hmacSha256Hex(opts.secret, `${paygateTs}.${raw}`);\n const presented = paygateSig.replace(/^sha256=/i, \"\").trim();\n if (!safeEqualHex(expectedHex, presented)) {\n return { ok: false, reason: \"invalid signature\" };\n }\n return {\n ok: true,\n timestamp: paygateTs,\n eventType: header(opts.headers, \"X-PayGate-Event\"),\n };\n }\n\n const pg365 = header(opts.headers, \"X-PG365-Signature\");\n if (pg365) {\n // Format: t=<unix>,v1=<hex>\n const parts = Object.fromEntries(\n pg365.split(\",\").map((p) => {\n const [k, ...rest] = p.trim().split(\"=\");\n return [k, rest.join(\"=\")];\n }),\n ) as Record<string, string>;\n const t = parts.t;\n const v1 = parts.v1;\n if (!t || !v1) return { ok: false, reason: \"malformed X-PG365-Signature\" };\n const ts = Number(t);\n if (!Number.isFinite(ts) || Math.abs(now - ts) > skew) {\n return { ok: false, reason: \"timestamp skew\" };\n }\n const expected = hmacSha256Hex(opts.secret, `${t}.${raw}`);\n if (!safeEqualHex(expected, v1)) {\n return { ok: false, reason: \"invalid signature\" };\n }\n return { ok: true, timestamp: t };\n }\n\n return { ok: false, reason: \"missing webhook signature headers\" };\n}\n"],"mappings":"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","names":[]}
|
package/dist/index.d.cts
ADDED
|
@@ -0,0 +1,177 @@
|
|
|
1
|
+
type PaymentStatus = "CREATED" | "AWAITING_PAYMENT" | "SUCCEEDED" | "FAILED" | "EXPIRED" | "REVIEW" | "REFUNDED" | "PARTIALLY_REFUNDED" | "CANCELLED";
|
|
2
|
+
type GardeshPayOptions = {
|
|
3
|
+
/** API key from the merchant portal (per gateway). */
|
|
4
|
+
apiKey: string;
|
|
5
|
+
/** HMAC secret shown with the API key — used to sign every request. */
|
|
6
|
+
hmacSecret: string;
|
|
7
|
+
/**
|
|
8
|
+
* Public Merchant API base including `/v1`, e.g.
|
|
9
|
+
* `https://api.example.com/v1`
|
|
10
|
+
*/
|
|
11
|
+
baseUrl: string;
|
|
12
|
+
/** Optional fetch override (tests / custom agents). */
|
|
13
|
+
fetch?: typeof fetch;
|
|
14
|
+
/** Default timeout in ms (default 30_000). */
|
|
15
|
+
timeoutMs?: number;
|
|
16
|
+
};
|
|
17
|
+
type CreatePaymentInput = {
|
|
18
|
+
orderRef: string;
|
|
19
|
+
/** Integer amount in Iranian Rials. */
|
|
20
|
+
amountRial: number;
|
|
21
|
+
/** Buyer return URL after bank/checkout. Not proof of payment. */
|
|
22
|
+
returnUrl: string;
|
|
23
|
+
callbackUrl?: string;
|
|
24
|
+
description?: string;
|
|
25
|
+
expiresInSec?: number;
|
|
26
|
+
metadata?: Record<string, string>;
|
|
27
|
+
gatewayId?: string;
|
|
28
|
+
/** Optional idempotency key; auto-generated UUID when omitted. */
|
|
29
|
+
idempotencyKey?: string;
|
|
30
|
+
correlationId?: string;
|
|
31
|
+
};
|
|
32
|
+
type CreatePaymentResult = {
|
|
33
|
+
paymentId: string;
|
|
34
|
+
authority: string;
|
|
35
|
+
checkoutUrl: string;
|
|
36
|
+
/** Prefer this for redirect when present. */
|
|
37
|
+
startPayUrl?: string;
|
|
38
|
+
redirectUrl?: string;
|
|
39
|
+
status: PaymentStatus;
|
|
40
|
+
amountRial: number;
|
|
41
|
+
organizationId?: string;
|
|
42
|
+
expiresAt?: string;
|
|
43
|
+
};
|
|
44
|
+
type PaymentResult = {
|
|
45
|
+
paymentId: string;
|
|
46
|
+
authority?: string;
|
|
47
|
+
status: PaymentStatus;
|
|
48
|
+
amountRial: number;
|
|
49
|
+
orderRef?: string;
|
|
50
|
+
checkoutUrl?: string;
|
|
51
|
+
startPayUrl?: string;
|
|
52
|
+
expiresAt?: string;
|
|
53
|
+
verifiedAt?: string | null;
|
|
54
|
+
reviewReason?: string | null;
|
|
55
|
+
[key: string]: unknown;
|
|
56
|
+
};
|
|
57
|
+
type VerifyPaymentResult = {
|
|
58
|
+
paymentId: string;
|
|
59
|
+
status: PaymentStatus;
|
|
60
|
+
amountRial: number;
|
|
61
|
+
verifiedAt?: string | null;
|
|
62
|
+
reviewReason?: string | null;
|
|
63
|
+
};
|
|
64
|
+
type CreateRefundInput = {
|
|
65
|
+
paymentId: string;
|
|
66
|
+
amountRial: number;
|
|
67
|
+
reason?: string;
|
|
68
|
+
idempotencyKey?: string;
|
|
69
|
+
};
|
|
70
|
+
type RefundResult = {
|
|
71
|
+
refundId?: string;
|
|
72
|
+
paymentId?: string;
|
|
73
|
+
status?: string;
|
|
74
|
+
amountRial?: number;
|
|
75
|
+
[key: string]: unknown;
|
|
76
|
+
};
|
|
77
|
+
type ProblemDetails = {
|
|
78
|
+
type?: string;
|
|
79
|
+
title?: string;
|
|
80
|
+
status?: number;
|
|
81
|
+
detail?: string;
|
|
82
|
+
code?: string;
|
|
83
|
+
correlationId?: string;
|
|
84
|
+
[key: string]: unknown;
|
|
85
|
+
};
|
|
86
|
+
|
|
87
|
+
declare class GardeshPay {
|
|
88
|
+
readonly apiKey: string;
|
|
89
|
+
readonly hmacSecret: string;
|
|
90
|
+
readonly baseUrl: string;
|
|
91
|
+
private readonly fetchImpl;
|
|
92
|
+
private readonly timeoutMs;
|
|
93
|
+
constructor(options: GardeshPayOptions);
|
|
94
|
+
/** Create a payment and get checkout / start-pay URLs. */
|
|
95
|
+
createPayment(input: CreatePaymentInput): Promise<CreatePaymentResult>;
|
|
96
|
+
/** GET payment status (masked). */
|
|
97
|
+
getPayment(paymentId: string): Promise<PaymentResult>;
|
|
98
|
+
/**
|
|
99
|
+
* Authoritative verify before fulfillment (idempotent).
|
|
100
|
+
* Fulfill only when `status === "SUCCEEDED"`.
|
|
101
|
+
*/
|
|
102
|
+
verifyPayment(paymentId: string, opts?: {
|
|
103
|
+
idempotencyKey?: string;
|
|
104
|
+
}): Promise<VerifyPaymentResult>;
|
|
105
|
+
/** Create a refund against a succeeded payment. */
|
|
106
|
+
createRefund(input: CreateRefundInput): Promise<RefundResult>;
|
|
107
|
+
private request;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
declare class GardeshPayError extends Error {
|
|
111
|
+
readonly status: number;
|
|
112
|
+
readonly code?: string;
|
|
113
|
+
readonly correlationId?: string;
|
|
114
|
+
readonly problem?: ProblemDetails;
|
|
115
|
+
readonly bodyText?: string;
|
|
116
|
+
constructor(message: string, opts: {
|
|
117
|
+
status: number;
|
|
118
|
+
code?: string;
|
|
119
|
+
correlationId?: string;
|
|
120
|
+
problem?: ProblemDetails;
|
|
121
|
+
bodyText?: string;
|
|
122
|
+
});
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
/** SHA-256 hex of UTF-8 body (empty string when body is empty). */
|
|
126
|
+
declare function sha256Hex(body: string | Buffer | Uint8Array): string;
|
|
127
|
+
/**
|
|
128
|
+
* Merchant API canonical string (must match Java `HmacSigner.merchantCanonical`):
|
|
129
|
+
* `METHOD\nPATH?QUERY\nTIMESTAMP\nNONCE\nSHA256_HEX(body)`
|
|
130
|
+
*/
|
|
131
|
+
declare function merchantCanonical(method: string, pathWithQuery: string, timestamp: string, nonce: string, bodySha256Hex: string): string;
|
|
132
|
+
/** Base64(HMAC-SHA256(secret, canonical)) — value for `X-Signature`. */
|
|
133
|
+
declare function hmacSha256Base64(secret: string, canonical: string): string;
|
|
134
|
+
/** Hex(HMAC-SHA256(secret, message)) — used for live webhook signatures. */
|
|
135
|
+
declare function hmacSha256Hex(secret: string, message: string): string;
|
|
136
|
+
declare function signMerchantRequest(opts: {
|
|
137
|
+
secret: string;
|
|
138
|
+
method: string;
|
|
139
|
+
pathWithQuery: string;
|
|
140
|
+
timestamp: string;
|
|
141
|
+
nonce: string;
|
|
142
|
+
body: string;
|
|
143
|
+
}): string;
|
|
144
|
+
declare function newNonce(): string;
|
|
145
|
+
declare function newIdempotencyKey(): string;
|
|
146
|
+
declare function unixSeconds(now?: number): string;
|
|
147
|
+
|
|
148
|
+
type WebhookHeaders = Record<string, string | string[] | undefined>;
|
|
149
|
+
/**
|
|
150
|
+
* Verify an inbound GardeshPay / PayGate365 webhook.
|
|
151
|
+
*
|
|
152
|
+
* Live notification-service signs with:
|
|
153
|
+
* - `X-PayGate-Signature: sha256=<hex(HMAC-SHA256(secret, `${t}.${rawBody}`))>`
|
|
154
|
+
* - `X-PayGate-Timestamp: <unix seconds>`
|
|
155
|
+
*
|
|
156
|
+
* Docs also mention `X-PG365-Signature: t=…,v1=…` — both are accepted.
|
|
157
|
+
*
|
|
158
|
+
* Webhooks are **notifications only**. Always call `payments.verify` (or GET)
|
|
159
|
+
* before fulfilling an order.
|
|
160
|
+
*/
|
|
161
|
+
declare function verifyWebhook(opts: {
|
|
162
|
+
secret: string;
|
|
163
|
+
rawBody: string | Buffer;
|
|
164
|
+
headers: WebhookHeaders;
|
|
165
|
+
/** Max clock skew in seconds (default 300). */
|
|
166
|
+
skewSeconds?: number;
|
|
167
|
+
nowSeconds?: number;
|
|
168
|
+
}): {
|
|
169
|
+
ok: true;
|
|
170
|
+
timestamp: string;
|
|
171
|
+
eventType?: string;
|
|
172
|
+
} | {
|
|
173
|
+
ok: false;
|
|
174
|
+
reason: string;
|
|
175
|
+
};
|
|
176
|
+
|
|
177
|
+
export { type CreatePaymentInput, type CreatePaymentResult, type CreateRefundInput, GardeshPay, GardeshPayError, type GardeshPayOptions, type PaymentResult, type PaymentStatus, type ProblemDetails, type RefundResult, type VerifyPaymentResult, hmacSha256Base64, hmacSha256Hex, merchantCanonical, newIdempotencyKey, newNonce, sha256Hex, signMerchantRequest, unixSeconds, verifyWebhook };
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,177 @@
|
|
|
1
|
+
type PaymentStatus = "CREATED" | "AWAITING_PAYMENT" | "SUCCEEDED" | "FAILED" | "EXPIRED" | "REVIEW" | "REFUNDED" | "PARTIALLY_REFUNDED" | "CANCELLED";
|
|
2
|
+
type GardeshPayOptions = {
|
|
3
|
+
/** API key from the merchant portal (per gateway). */
|
|
4
|
+
apiKey: string;
|
|
5
|
+
/** HMAC secret shown with the API key — used to sign every request. */
|
|
6
|
+
hmacSecret: string;
|
|
7
|
+
/**
|
|
8
|
+
* Public Merchant API base including `/v1`, e.g.
|
|
9
|
+
* `https://api.example.com/v1`
|
|
10
|
+
*/
|
|
11
|
+
baseUrl: string;
|
|
12
|
+
/** Optional fetch override (tests / custom agents). */
|
|
13
|
+
fetch?: typeof fetch;
|
|
14
|
+
/** Default timeout in ms (default 30_000). */
|
|
15
|
+
timeoutMs?: number;
|
|
16
|
+
};
|
|
17
|
+
type CreatePaymentInput = {
|
|
18
|
+
orderRef: string;
|
|
19
|
+
/** Integer amount in Iranian Rials. */
|
|
20
|
+
amountRial: number;
|
|
21
|
+
/** Buyer return URL after bank/checkout. Not proof of payment. */
|
|
22
|
+
returnUrl: string;
|
|
23
|
+
callbackUrl?: string;
|
|
24
|
+
description?: string;
|
|
25
|
+
expiresInSec?: number;
|
|
26
|
+
metadata?: Record<string, string>;
|
|
27
|
+
gatewayId?: string;
|
|
28
|
+
/** Optional idempotency key; auto-generated UUID when omitted. */
|
|
29
|
+
idempotencyKey?: string;
|
|
30
|
+
correlationId?: string;
|
|
31
|
+
};
|
|
32
|
+
type CreatePaymentResult = {
|
|
33
|
+
paymentId: string;
|
|
34
|
+
authority: string;
|
|
35
|
+
checkoutUrl: string;
|
|
36
|
+
/** Prefer this for redirect when present. */
|
|
37
|
+
startPayUrl?: string;
|
|
38
|
+
redirectUrl?: string;
|
|
39
|
+
status: PaymentStatus;
|
|
40
|
+
amountRial: number;
|
|
41
|
+
organizationId?: string;
|
|
42
|
+
expiresAt?: string;
|
|
43
|
+
};
|
|
44
|
+
type PaymentResult = {
|
|
45
|
+
paymentId: string;
|
|
46
|
+
authority?: string;
|
|
47
|
+
status: PaymentStatus;
|
|
48
|
+
amountRial: number;
|
|
49
|
+
orderRef?: string;
|
|
50
|
+
checkoutUrl?: string;
|
|
51
|
+
startPayUrl?: string;
|
|
52
|
+
expiresAt?: string;
|
|
53
|
+
verifiedAt?: string | null;
|
|
54
|
+
reviewReason?: string | null;
|
|
55
|
+
[key: string]: unknown;
|
|
56
|
+
};
|
|
57
|
+
type VerifyPaymentResult = {
|
|
58
|
+
paymentId: string;
|
|
59
|
+
status: PaymentStatus;
|
|
60
|
+
amountRial: number;
|
|
61
|
+
verifiedAt?: string | null;
|
|
62
|
+
reviewReason?: string | null;
|
|
63
|
+
};
|
|
64
|
+
type CreateRefundInput = {
|
|
65
|
+
paymentId: string;
|
|
66
|
+
amountRial: number;
|
|
67
|
+
reason?: string;
|
|
68
|
+
idempotencyKey?: string;
|
|
69
|
+
};
|
|
70
|
+
type RefundResult = {
|
|
71
|
+
refundId?: string;
|
|
72
|
+
paymentId?: string;
|
|
73
|
+
status?: string;
|
|
74
|
+
amountRial?: number;
|
|
75
|
+
[key: string]: unknown;
|
|
76
|
+
};
|
|
77
|
+
type ProblemDetails = {
|
|
78
|
+
type?: string;
|
|
79
|
+
title?: string;
|
|
80
|
+
status?: number;
|
|
81
|
+
detail?: string;
|
|
82
|
+
code?: string;
|
|
83
|
+
correlationId?: string;
|
|
84
|
+
[key: string]: unknown;
|
|
85
|
+
};
|
|
86
|
+
|
|
87
|
+
declare class GardeshPay {
|
|
88
|
+
readonly apiKey: string;
|
|
89
|
+
readonly hmacSecret: string;
|
|
90
|
+
readonly baseUrl: string;
|
|
91
|
+
private readonly fetchImpl;
|
|
92
|
+
private readonly timeoutMs;
|
|
93
|
+
constructor(options: GardeshPayOptions);
|
|
94
|
+
/** Create a payment and get checkout / start-pay URLs. */
|
|
95
|
+
createPayment(input: CreatePaymentInput): Promise<CreatePaymentResult>;
|
|
96
|
+
/** GET payment status (masked). */
|
|
97
|
+
getPayment(paymentId: string): Promise<PaymentResult>;
|
|
98
|
+
/**
|
|
99
|
+
* Authoritative verify before fulfillment (idempotent).
|
|
100
|
+
* Fulfill only when `status === "SUCCEEDED"`.
|
|
101
|
+
*/
|
|
102
|
+
verifyPayment(paymentId: string, opts?: {
|
|
103
|
+
idempotencyKey?: string;
|
|
104
|
+
}): Promise<VerifyPaymentResult>;
|
|
105
|
+
/** Create a refund against a succeeded payment. */
|
|
106
|
+
createRefund(input: CreateRefundInput): Promise<RefundResult>;
|
|
107
|
+
private request;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
declare class GardeshPayError extends Error {
|
|
111
|
+
readonly status: number;
|
|
112
|
+
readonly code?: string;
|
|
113
|
+
readonly correlationId?: string;
|
|
114
|
+
readonly problem?: ProblemDetails;
|
|
115
|
+
readonly bodyText?: string;
|
|
116
|
+
constructor(message: string, opts: {
|
|
117
|
+
status: number;
|
|
118
|
+
code?: string;
|
|
119
|
+
correlationId?: string;
|
|
120
|
+
problem?: ProblemDetails;
|
|
121
|
+
bodyText?: string;
|
|
122
|
+
});
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
/** SHA-256 hex of UTF-8 body (empty string when body is empty). */
|
|
126
|
+
declare function sha256Hex(body: string | Buffer | Uint8Array): string;
|
|
127
|
+
/**
|
|
128
|
+
* Merchant API canonical string (must match Java `HmacSigner.merchantCanonical`):
|
|
129
|
+
* `METHOD\nPATH?QUERY\nTIMESTAMP\nNONCE\nSHA256_HEX(body)`
|
|
130
|
+
*/
|
|
131
|
+
declare function merchantCanonical(method: string, pathWithQuery: string, timestamp: string, nonce: string, bodySha256Hex: string): string;
|
|
132
|
+
/** Base64(HMAC-SHA256(secret, canonical)) — value for `X-Signature`. */
|
|
133
|
+
declare function hmacSha256Base64(secret: string, canonical: string): string;
|
|
134
|
+
/** Hex(HMAC-SHA256(secret, message)) — used for live webhook signatures. */
|
|
135
|
+
declare function hmacSha256Hex(secret: string, message: string): string;
|
|
136
|
+
declare function signMerchantRequest(opts: {
|
|
137
|
+
secret: string;
|
|
138
|
+
method: string;
|
|
139
|
+
pathWithQuery: string;
|
|
140
|
+
timestamp: string;
|
|
141
|
+
nonce: string;
|
|
142
|
+
body: string;
|
|
143
|
+
}): string;
|
|
144
|
+
declare function newNonce(): string;
|
|
145
|
+
declare function newIdempotencyKey(): string;
|
|
146
|
+
declare function unixSeconds(now?: number): string;
|
|
147
|
+
|
|
148
|
+
type WebhookHeaders = Record<string, string | string[] | undefined>;
|
|
149
|
+
/**
|
|
150
|
+
* Verify an inbound GardeshPay / PayGate365 webhook.
|
|
151
|
+
*
|
|
152
|
+
* Live notification-service signs with:
|
|
153
|
+
* - `X-PayGate-Signature: sha256=<hex(HMAC-SHA256(secret, `${t}.${rawBody}`))>`
|
|
154
|
+
* - `X-PayGate-Timestamp: <unix seconds>`
|
|
155
|
+
*
|
|
156
|
+
* Docs also mention `X-PG365-Signature: t=…,v1=…` — both are accepted.
|
|
157
|
+
*
|
|
158
|
+
* Webhooks are **notifications only**. Always call `payments.verify` (or GET)
|
|
159
|
+
* before fulfilling an order.
|
|
160
|
+
*/
|
|
161
|
+
declare function verifyWebhook(opts: {
|
|
162
|
+
secret: string;
|
|
163
|
+
rawBody: string | Buffer;
|
|
164
|
+
headers: WebhookHeaders;
|
|
165
|
+
/** Max clock skew in seconds (default 300). */
|
|
166
|
+
skewSeconds?: number;
|
|
167
|
+
nowSeconds?: number;
|
|
168
|
+
}): {
|
|
169
|
+
ok: true;
|
|
170
|
+
timestamp: string;
|
|
171
|
+
eventType?: string;
|
|
172
|
+
} | {
|
|
173
|
+
ok: false;
|
|
174
|
+
reason: string;
|
|
175
|
+
};
|
|
176
|
+
|
|
177
|
+
export { type CreatePaymentInput, type CreatePaymentResult, type CreateRefundInput, GardeshPay, GardeshPayError, type GardeshPayOptions, type PaymentResult, type PaymentStatus, type ProblemDetails, type RefundResult, type VerifyPaymentResult, hmacSha256Base64, hmacSha256Hex, merchantCanonical, newIdempotencyKey, newNonce, sha256Hex, signMerchantRequest, unixSeconds, verifyWebhook };
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,270 @@
|
|
|
1
|
+
// src/errors.ts
|
|
2
|
+
var GardeshPayError = class extends Error {
|
|
3
|
+
status;
|
|
4
|
+
code;
|
|
5
|
+
correlationId;
|
|
6
|
+
problem;
|
|
7
|
+
bodyText;
|
|
8
|
+
constructor(message, opts) {
|
|
9
|
+
super(message);
|
|
10
|
+
this.name = "GardeshPayError";
|
|
11
|
+
this.status = opts.status;
|
|
12
|
+
this.code = opts.code;
|
|
13
|
+
this.correlationId = opts.correlationId;
|
|
14
|
+
this.problem = opts.problem;
|
|
15
|
+
this.bodyText = opts.bodyText;
|
|
16
|
+
}
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
// src/hmac.ts
|
|
20
|
+
import { createHash, createHmac, randomUUID, timingSafeEqual } from "crypto";
|
|
21
|
+
function sha256Hex(body) {
|
|
22
|
+
return createHash("sha256").update(body ?? "").digest("hex");
|
|
23
|
+
}
|
|
24
|
+
function merchantCanonical(method, pathWithQuery, timestamp, nonce, bodySha256Hex) {
|
|
25
|
+
return [method, pathWithQuery, timestamp, nonce, bodySha256Hex].join("\n");
|
|
26
|
+
}
|
|
27
|
+
function hmacSha256Base64(secret, canonical) {
|
|
28
|
+
return createHmac("sha256", secret).update(canonical, "utf8").digest("base64");
|
|
29
|
+
}
|
|
30
|
+
function hmacSha256Hex(secret, message) {
|
|
31
|
+
return createHmac("sha256", secret).update(message, "utf8").digest("hex");
|
|
32
|
+
}
|
|
33
|
+
function signMerchantRequest(opts) {
|
|
34
|
+
const bodyHash = sha256Hex(opts.body);
|
|
35
|
+
const canonical = merchantCanonical(
|
|
36
|
+
opts.method.toUpperCase(),
|
|
37
|
+
opts.pathWithQuery,
|
|
38
|
+
opts.timestamp,
|
|
39
|
+
opts.nonce,
|
|
40
|
+
bodyHash
|
|
41
|
+
);
|
|
42
|
+
return hmacSha256Base64(opts.secret, canonical);
|
|
43
|
+
}
|
|
44
|
+
function newNonce() {
|
|
45
|
+
return `n-${randomUUID()}`;
|
|
46
|
+
}
|
|
47
|
+
function newIdempotencyKey() {
|
|
48
|
+
return randomUUID();
|
|
49
|
+
}
|
|
50
|
+
function unixSeconds(now = Date.now()) {
|
|
51
|
+
return String(Math.floor(now / 1e3));
|
|
52
|
+
}
|
|
53
|
+
function safeEqualHex(a, b) {
|
|
54
|
+
try {
|
|
55
|
+
const left = Buffer.from(a, "utf8");
|
|
56
|
+
const right = Buffer.from(b, "utf8");
|
|
57
|
+
if (left.length !== right.length) return false;
|
|
58
|
+
return timingSafeEqual(left, right);
|
|
59
|
+
} catch {
|
|
60
|
+
return false;
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
// src/client.ts
|
|
65
|
+
var GardeshPay = class {
|
|
66
|
+
apiKey;
|
|
67
|
+
hmacSecret;
|
|
68
|
+
baseUrl;
|
|
69
|
+
fetchImpl;
|
|
70
|
+
timeoutMs;
|
|
71
|
+
constructor(options) {
|
|
72
|
+
if (!options?.apiKey?.trim()) throw new Error("apiKey is required");
|
|
73
|
+
if (!options?.hmacSecret?.trim()) throw new Error("hmacSecret is required");
|
|
74
|
+
if (!options?.baseUrl?.trim()) throw new Error("baseUrl is required (include /v1)");
|
|
75
|
+
this.apiKey = options.apiKey.trim();
|
|
76
|
+
this.hmacSecret = options.hmacSecret.trim();
|
|
77
|
+
this.baseUrl = options.baseUrl.replace(/\/+$/, "");
|
|
78
|
+
this.fetchImpl = options.fetch ?? fetch;
|
|
79
|
+
this.timeoutMs = options.timeoutMs ?? 3e4;
|
|
80
|
+
}
|
|
81
|
+
/** Create a payment and get checkout / start-pay URLs. */
|
|
82
|
+
async createPayment(input) {
|
|
83
|
+
const data = await this.request({
|
|
84
|
+
method: "POST",
|
|
85
|
+
path: "/payments",
|
|
86
|
+
body: {
|
|
87
|
+
orderRef: input.orderRef,
|
|
88
|
+
amountRial: input.amountRial,
|
|
89
|
+
returnUrl: input.returnUrl,
|
|
90
|
+
callbackUrl: input.callbackUrl,
|
|
91
|
+
description: input.description,
|
|
92
|
+
expiresInSec: input.expiresInSec,
|
|
93
|
+
metadata: input.metadata,
|
|
94
|
+
gatewayId: input.gatewayId
|
|
95
|
+
},
|
|
96
|
+
idempotencyKey: input.idempotencyKey ?? newIdempotencyKey(),
|
|
97
|
+
correlationId: input.correlationId
|
|
98
|
+
});
|
|
99
|
+
return data;
|
|
100
|
+
}
|
|
101
|
+
/** GET payment status (masked). */
|
|
102
|
+
async getPayment(paymentId) {
|
|
103
|
+
return this.request({
|
|
104
|
+
method: "GET",
|
|
105
|
+
path: `/payments/${encodeURIComponent(paymentId)}`
|
|
106
|
+
});
|
|
107
|
+
}
|
|
108
|
+
/**
|
|
109
|
+
* Authoritative verify before fulfillment (idempotent).
|
|
110
|
+
* Fulfill only when `status === "SUCCEEDED"`.
|
|
111
|
+
*/
|
|
112
|
+
async verifyPayment(paymentId, opts) {
|
|
113
|
+
return this.request({
|
|
114
|
+
method: "POST",
|
|
115
|
+
path: `/payments/${encodeURIComponent(paymentId)}/verify`,
|
|
116
|
+
// Empty body — matches typical verify clients; HMAC hashes empty string.
|
|
117
|
+
idempotencyKey: opts?.idempotencyKey ?? newIdempotencyKey()
|
|
118
|
+
});
|
|
119
|
+
}
|
|
120
|
+
/** Create a refund against a succeeded payment. */
|
|
121
|
+
async createRefund(input) {
|
|
122
|
+
return this.request({
|
|
123
|
+
method: "POST",
|
|
124
|
+
path: `/payments/${encodeURIComponent(input.paymentId)}/refunds`,
|
|
125
|
+
body: {
|
|
126
|
+
amountRial: input.amountRial,
|
|
127
|
+
reason: input.reason
|
|
128
|
+
},
|
|
129
|
+
idempotencyKey: input.idempotencyKey ?? newIdempotencyKey()
|
|
130
|
+
});
|
|
131
|
+
}
|
|
132
|
+
async request(opts) {
|
|
133
|
+
const pathWithQuery = opts.path.startsWith("/") ? opts.path : `/${opts.path}`;
|
|
134
|
+
const url = `${this.baseUrl}${pathWithQuery}`;
|
|
135
|
+
const bodyText = opts.method === "GET" || opts.body === void 0 ? "" : JSON.stringify(opts.body ?? {});
|
|
136
|
+
const timestamp = unixSeconds();
|
|
137
|
+
const nonce = newNonce();
|
|
138
|
+
const signPath = `/v1${pathWithQuery}`;
|
|
139
|
+
const signature = signMerchantRequest({
|
|
140
|
+
secret: this.hmacSecret,
|
|
141
|
+
method: opts.method,
|
|
142
|
+
pathWithQuery: signPath,
|
|
143
|
+
timestamp,
|
|
144
|
+
nonce,
|
|
145
|
+
body: bodyText
|
|
146
|
+
});
|
|
147
|
+
const headers = {
|
|
148
|
+
Accept: "application/json",
|
|
149
|
+
"X-Api-Key": this.apiKey,
|
|
150
|
+
"X-Timestamp": timestamp,
|
|
151
|
+
"X-Nonce": nonce,
|
|
152
|
+
"X-Signature": signature
|
|
153
|
+
};
|
|
154
|
+
if (bodyText) headers["Content-Type"] = "application/json";
|
|
155
|
+
if (opts.idempotencyKey) headers["Idempotency-Key"] = opts.idempotencyKey;
|
|
156
|
+
if (opts.correlationId) headers["X-Correlation-Id"] = opts.correlationId;
|
|
157
|
+
const controller = new AbortController();
|
|
158
|
+
const timer = setTimeout(() => controller.abort(), this.timeoutMs);
|
|
159
|
+
let res;
|
|
160
|
+
try {
|
|
161
|
+
res = await this.fetchImpl(url, {
|
|
162
|
+
method: opts.method,
|
|
163
|
+
headers,
|
|
164
|
+
body: bodyText || void 0,
|
|
165
|
+
signal: controller.signal
|
|
166
|
+
});
|
|
167
|
+
} catch (err) {
|
|
168
|
+
clearTimeout(timer);
|
|
169
|
+
throw new GardeshPayError(
|
|
170
|
+
err instanceof Error ? err.message : "network error",
|
|
171
|
+
{ status: 0 }
|
|
172
|
+
);
|
|
173
|
+
} finally {
|
|
174
|
+
clearTimeout(timer);
|
|
175
|
+
}
|
|
176
|
+
const text = await res.text();
|
|
177
|
+
let json = void 0;
|
|
178
|
+
if (text) {
|
|
179
|
+
try {
|
|
180
|
+
json = JSON.parse(text);
|
|
181
|
+
} catch {
|
|
182
|
+
json = void 0;
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
if (!res.ok) {
|
|
186
|
+
const problem = json && typeof json === "object" ? json : void 0;
|
|
187
|
+
throw new GardeshPayError(problem?.detail || problem?.title || `HTTP ${res.status}`, {
|
|
188
|
+
status: res.status,
|
|
189
|
+
code: problem?.code,
|
|
190
|
+
correlationId: problem?.correlationId,
|
|
191
|
+
problem,
|
|
192
|
+
bodyText: text
|
|
193
|
+
});
|
|
194
|
+
}
|
|
195
|
+
if (json && typeof json === "object" && "data" in json) {
|
|
196
|
+
return json.data;
|
|
197
|
+
}
|
|
198
|
+
return json;
|
|
199
|
+
}
|
|
200
|
+
};
|
|
201
|
+
|
|
202
|
+
// src/webhooks.ts
|
|
203
|
+
function header(headers, name) {
|
|
204
|
+
const direct = headers[name] ?? headers[name.toLowerCase()];
|
|
205
|
+
if (Array.isArray(direct)) return direct[0];
|
|
206
|
+
if (typeof direct === "string") return direct;
|
|
207
|
+
const key = Object.keys(headers).find((k) => k.toLowerCase() === name.toLowerCase());
|
|
208
|
+
if (!key) return void 0;
|
|
209
|
+
const v = headers[key];
|
|
210
|
+
return Array.isArray(v) ? v[0] : v;
|
|
211
|
+
}
|
|
212
|
+
function verifyWebhook(opts) {
|
|
213
|
+
const skew = opts.skewSeconds ?? 300;
|
|
214
|
+
const now = opts.nowSeconds ?? Math.floor(Date.now() / 1e3);
|
|
215
|
+
const raw = typeof opts.rawBody === "string" ? opts.rawBody : opts.rawBody.toString("utf8");
|
|
216
|
+
const paygateSig = header(opts.headers, "X-PayGate-Signature");
|
|
217
|
+
const paygateTs = header(opts.headers, "X-PayGate-Timestamp");
|
|
218
|
+
if (paygateSig && paygateTs) {
|
|
219
|
+
const ts = Number(paygateTs);
|
|
220
|
+
if (!Number.isFinite(ts) || Math.abs(now - ts) > skew) {
|
|
221
|
+
return { ok: false, reason: "timestamp skew" };
|
|
222
|
+
}
|
|
223
|
+
const expectedHex = hmacSha256Hex(opts.secret, `${paygateTs}.${raw}`);
|
|
224
|
+
const presented = paygateSig.replace(/^sha256=/i, "").trim();
|
|
225
|
+
if (!safeEqualHex(expectedHex, presented)) {
|
|
226
|
+
return { ok: false, reason: "invalid signature" };
|
|
227
|
+
}
|
|
228
|
+
return {
|
|
229
|
+
ok: true,
|
|
230
|
+
timestamp: paygateTs,
|
|
231
|
+
eventType: header(opts.headers, "X-PayGate-Event")
|
|
232
|
+
};
|
|
233
|
+
}
|
|
234
|
+
const pg365 = header(opts.headers, "X-PG365-Signature");
|
|
235
|
+
if (pg365) {
|
|
236
|
+
const parts = Object.fromEntries(
|
|
237
|
+
pg365.split(",").map((p) => {
|
|
238
|
+
const [k, ...rest] = p.trim().split("=");
|
|
239
|
+
return [k, rest.join("=")];
|
|
240
|
+
})
|
|
241
|
+
);
|
|
242
|
+
const t = parts.t;
|
|
243
|
+
const v1 = parts.v1;
|
|
244
|
+
if (!t || !v1) return { ok: false, reason: "malformed X-PG365-Signature" };
|
|
245
|
+
const ts = Number(t);
|
|
246
|
+
if (!Number.isFinite(ts) || Math.abs(now - ts) > skew) {
|
|
247
|
+
return { ok: false, reason: "timestamp skew" };
|
|
248
|
+
}
|
|
249
|
+
const expected = hmacSha256Hex(opts.secret, `${t}.${raw}`);
|
|
250
|
+
if (!safeEqualHex(expected, v1)) {
|
|
251
|
+
return { ok: false, reason: "invalid signature" };
|
|
252
|
+
}
|
|
253
|
+
return { ok: true, timestamp: t };
|
|
254
|
+
}
|
|
255
|
+
return { ok: false, reason: "missing webhook signature headers" };
|
|
256
|
+
}
|
|
257
|
+
export {
|
|
258
|
+
GardeshPay,
|
|
259
|
+
GardeshPayError,
|
|
260
|
+
hmacSha256Base64,
|
|
261
|
+
hmacSha256Hex,
|
|
262
|
+
merchantCanonical,
|
|
263
|
+
newIdempotencyKey,
|
|
264
|
+
newNonce,
|
|
265
|
+
sha256Hex,
|
|
266
|
+
signMerchantRequest,
|
|
267
|
+
unixSeconds,
|
|
268
|
+
verifyWebhook
|
|
269
|
+
};
|
|
270
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/errors.ts","../src/hmac.ts","../src/client.ts","../src/webhooks.ts"],"sourcesContent":["import type { ProblemDetails } from \"./types.js\";\n\nexport class GardeshPayError extends Error {\n readonly status: number;\n readonly code?: string;\n readonly correlationId?: string;\n readonly problem?: ProblemDetails;\n readonly bodyText?: string;\n\n constructor(\n message: string,\n opts: {\n status: number;\n code?: string;\n correlationId?: string;\n problem?: ProblemDetails;\n bodyText?: string;\n },\n ) {\n super(message);\n this.name = \"GardeshPayError\";\n this.status = opts.status;\n this.code = opts.code;\n this.correlationId = opts.correlationId;\n this.problem = opts.problem;\n this.bodyText = opts.bodyText;\n }\n}\n","import { createHash, createHmac, randomUUID, timingSafeEqual } from \"node:crypto\";\n\n/** SHA-256 hex of UTF-8 body (empty string when body is empty). */\nexport function sha256Hex(body: string | Buffer | Uint8Array): string {\n return createHash(\"sha256\").update(body ?? \"\").digest(\"hex\");\n}\n\n/**\n * Merchant API canonical string (must match Java `HmacSigner.merchantCanonical`):\n * `METHOD\\nPATH?QUERY\\nTIMESTAMP\\nNONCE\\nSHA256_HEX(body)`\n */\nexport function merchantCanonical(\n method: string,\n pathWithQuery: string,\n timestamp: string,\n nonce: string,\n bodySha256Hex: string,\n): string {\n return [method, pathWithQuery, timestamp, nonce, bodySha256Hex].join(\"\\n\");\n}\n\n/** Base64(HMAC-SHA256(secret, canonical)) — value for `X-Signature`. */\nexport function hmacSha256Base64(secret: string, canonical: string): string {\n return createHmac(\"sha256\", secret).update(canonical, \"utf8\").digest(\"base64\");\n}\n\n/** Hex(HMAC-SHA256(secret, message)) — used for live webhook signatures. */\nexport function hmacSha256Hex(secret: string, message: string): string {\n return createHmac(\"sha256\", secret).update(message, \"utf8\").digest(\"hex\");\n}\n\nexport function signMerchantRequest(opts: {\n secret: string;\n method: string;\n pathWithQuery: string;\n timestamp: string;\n nonce: string;\n body: string;\n}): string {\n const bodyHash = sha256Hex(opts.body);\n const canonical = merchantCanonical(\n opts.method.toUpperCase(),\n opts.pathWithQuery,\n opts.timestamp,\n opts.nonce,\n bodyHash,\n );\n return hmacSha256Base64(opts.secret, canonical);\n}\n\nexport function newNonce(): string {\n return `n-${randomUUID()}`;\n}\n\nexport function newIdempotencyKey(): string {\n return randomUUID();\n}\n\nexport function unixSeconds(now = Date.now()): string {\n return String(Math.floor(now / 1000));\n}\n\nexport function safeEqualHex(a: string, b: string): boolean {\n try {\n const left = Buffer.from(a, \"utf8\");\n const right = Buffer.from(b, \"utf8\");\n if (left.length !== right.length) return false;\n return timingSafeEqual(left, right);\n } catch {\n return false;\n }\n}\n","import { GardeshPayError } from \"./errors.js\";\nimport { newIdempotencyKey, newNonce, signMerchantRequest, unixSeconds } from \"./hmac.js\";\nimport type {\n CreatePaymentInput,\n CreatePaymentResult,\n CreateRefundInput,\n GardeshPayOptions,\n PaymentResult,\n ProblemDetails,\n RefundResult,\n VerifyPaymentResult,\n} from \"./types.js\";\n\ntype RequestOpts = {\n method: \"GET\" | \"POST\";\n path: string;\n body?: unknown;\n idempotencyKey?: string;\n correlationId?: string;\n};\n\nexport class GardeshPay {\n readonly apiKey: string;\n readonly hmacSecret: string;\n readonly baseUrl: string;\n private readonly fetchImpl: typeof fetch;\n private readonly timeoutMs: number;\n\n constructor(options: GardeshPayOptions) {\n if (!options?.apiKey?.trim()) throw new Error(\"apiKey is required\");\n if (!options?.hmacSecret?.trim()) throw new Error(\"hmacSecret is required\");\n if (!options?.baseUrl?.trim()) throw new Error(\"baseUrl is required (include /v1)\");\n this.apiKey = options.apiKey.trim();\n this.hmacSecret = options.hmacSecret.trim();\n this.baseUrl = options.baseUrl.replace(/\\/+$/, \"\");\n this.fetchImpl = options.fetch ?? fetch;\n this.timeoutMs = options.timeoutMs ?? 30_000;\n }\n\n /** Create a payment and get checkout / start-pay URLs. */\n async createPayment(input: CreatePaymentInput): Promise<CreatePaymentResult> {\n const data = await this.request<CreatePaymentResult>({\n method: \"POST\",\n path: \"/payments\",\n body: {\n orderRef: input.orderRef,\n amountRial: input.amountRial,\n returnUrl: input.returnUrl,\n callbackUrl: input.callbackUrl,\n description: input.description,\n expiresInSec: input.expiresInSec,\n metadata: input.metadata,\n gatewayId: input.gatewayId,\n },\n idempotencyKey: input.idempotencyKey ?? newIdempotencyKey(),\n correlationId: input.correlationId,\n });\n return data;\n }\n\n /** GET payment status (masked). */\n async getPayment(paymentId: string): Promise<PaymentResult> {\n return this.request<PaymentResult>({\n method: \"GET\",\n path: `/payments/${encodeURIComponent(paymentId)}`,\n });\n }\n\n /**\n * Authoritative verify before fulfillment (idempotent).\n * Fulfill only when `status === \"SUCCEEDED\"`.\n */\n async verifyPayment(\n paymentId: string,\n opts?: { idempotencyKey?: string },\n ): Promise<VerifyPaymentResult> {\n return this.request<VerifyPaymentResult>({\n method: \"POST\",\n path: `/payments/${encodeURIComponent(paymentId)}/verify`,\n // Empty body — matches typical verify clients; HMAC hashes empty string.\n idempotencyKey: opts?.idempotencyKey ?? newIdempotencyKey(),\n });\n }\n\n /** Create a refund against a succeeded payment. */\n async createRefund(input: CreateRefundInput): Promise<RefundResult> {\n return this.request<RefundResult>({\n method: \"POST\",\n path: `/payments/${encodeURIComponent(input.paymentId)}/refunds`,\n body: {\n amountRial: input.amountRial,\n reason: input.reason,\n },\n idempotencyKey: input.idempotencyKey ?? newIdempotencyKey(),\n });\n }\n\n private async request<T>(opts: RequestOpts): Promise<T> {\n const pathWithQuery = opts.path.startsWith(\"/\") ? opts.path : `/${opts.path}`;\n const url = `${this.baseUrl}${pathWithQuery}`;\n const bodyText =\n opts.method === \"GET\" || opts.body === undefined ? \"\" : JSON.stringify(opts.body ?? {});\n const timestamp = unixSeconds();\n const nonce = newNonce();\n // Sign against the public path as exposed under /v1 (matches Java filter).\n const signPath = `/v1${pathWithQuery}`;\n const signature = signMerchantRequest({\n secret: this.hmacSecret,\n method: opts.method,\n pathWithQuery: signPath,\n timestamp,\n nonce,\n body: bodyText,\n });\n\n const headers: Record<string, string> = {\n Accept: \"application/json\",\n \"X-Api-Key\": this.apiKey,\n \"X-Timestamp\": timestamp,\n \"X-Nonce\": nonce,\n \"X-Signature\": signature,\n };\n if (bodyText) headers[\"Content-Type\"] = \"application/json\";\n if (opts.idempotencyKey) headers[\"Idempotency-Key\"] = opts.idempotencyKey;\n if (opts.correlationId) headers[\"X-Correlation-Id\"] = opts.correlationId;\n\n const controller = new AbortController();\n const timer = setTimeout(() => controller.abort(), this.timeoutMs);\n let res: Response;\n try {\n res = await this.fetchImpl(url, {\n method: opts.method,\n headers,\n body: bodyText || undefined,\n signal: controller.signal,\n });\n } catch (err) {\n clearTimeout(timer);\n throw new GardeshPayError(\n err instanceof Error ? err.message : \"network error\",\n { status: 0 },\n );\n } finally {\n clearTimeout(timer);\n }\n\n const text = await res.text();\n let json: unknown = undefined;\n if (text) {\n try {\n json = JSON.parse(text);\n } catch {\n json = undefined;\n }\n }\n\n if (!res.ok) {\n const problem = (json && typeof json === \"object\" ? json : undefined) as\n | ProblemDetails\n | undefined;\n throw new GardeshPayError(problem?.detail || problem?.title || `HTTP ${res.status}`, {\n status: res.status,\n code: problem?.code,\n correlationId: problem?.correlationId,\n problem,\n bodyText: text,\n });\n }\n\n if (json && typeof json === \"object\" && \"data\" in json) {\n return (json as { data: T }).data;\n }\n return json as T;\n }\n}\n","import { hmacSha256Hex, safeEqualHex } from \"./hmac.js\";\n\nexport type WebhookHeaders = Record<string, string | string[] | undefined>;\n\nfunction header(headers: WebhookHeaders, name: string): string | undefined {\n const direct = headers[name] ?? headers[name.toLowerCase()];\n if (Array.isArray(direct)) return direct[0];\n if (typeof direct === \"string\") return direct;\n // case-insensitive fallback\n const key = Object.keys(headers).find((k) => k.toLowerCase() === name.toLowerCase());\n if (!key) return undefined;\n const v = headers[key];\n return Array.isArray(v) ? v[0] : v;\n}\n\n/**\n * Verify an inbound GardeshPay / PayGate365 webhook.\n *\n * Live notification-service signs with:\n * - `X-PayGate-Signature: sha256=<hex(HMAC-SHA256(secret, `${t}.${rawBody}`))>`\n * - `X-PayGate-Timestamp: <unix seconds>`\n *\n * Docs also mention `X-PG365-Signature: t=…,v1=…` — both are accepted.\n *\n * Webhooks are **notifications only**. Always call `payments.verify` (or GET)\n * before fulfilling an order.\n */\nexport function verifyWebhook(opts: {\n secret: string;\n rawBody: string | Buffer;\n headers: WebhookHeaders;\n /** Max clock skew in seconds (default 300). */\n skewSeconds?: number;\n nowSeconds?: number;\n}): { ok: true; timestamp: string; eventType?: string } | { ok: false; reason: string } {\n const skew = opts.skewSeconds ?? 300;\n const now = opts.nowSeconds ?? Math.floor(Date.now() / 1000);\n const raw = typeof opts.rawBody === \"string\" ? opts.rawBody : opts.rawBody.toString(\"utf8\");\n\n const paygateSig = header(opts.headers, \"X-PayGate-Signature\");\n const paygateTs = header(opts.headers, \"X-PayGate-Timestamp\");\n if (paygateSig && paygateTs) {\n const ts = Number(paygateTs);\n if (!Number.isFinite(ts) || Math.abs(now - ts) > skew) {\n return { ok: false, reason: \"timestamp skew\" };\n }\n const expectedHex = hmacSha256Hex(opts.secret, `${paygateTs}.${raw}`);\n const presented = paygateSig.replace(/^sha256=/i, \"\").trim();\n if (!safeEqualHex(expectedHex, presented)) {\n return { ok: false, reason: \"invalid signature\" };\n }\n return {\n ok: true,\n timestamp: paygateTs,\n eventType: header(opts.headers, \"X-PayGate-Event\"),\n };\n }\n\n const pg365 = header(opts.headers, \"X-PG365-Signature\");\n if (pg365) {\n // Format: t=<unix>,v1=<hex>\n const parts = Object.fromEntries(\n pg365.split(\",\").map((p) => {\n const [k, ...rest] = p.trim().split(\"=\");\n return [k, rest.join(\"=\")];\n }),\n ) as Record<string, string>;\n const t = parts.t;\n const v1 = parts.v1;\n if (!t || !v1) return { ok: false, reason: \"malformed X-PG365-Signature\" };\n const ts = Number(t);\n if (!Number.isFinite(ts) || Math.abs(now - ts) > skew) {\n return { ok: false, reason: \"timestamp skew\" };\n }\n const expected = hmacSha256Hex(opts.secret, `${t}.${raw}`);\n if (!safeEqualHex(expected, v1)) {\n return { ok: false, reason: \"invalid signature\" };\n }\n return { ok: true, timestamp: t };\n }\n\n return { ok: false, reason: \"missing webhook signature headers\" };\n}\n"],"mappings":"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","names":[]}
|
package/package.json
CHANGED
|
@@ -1,6 +1,59 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "gardeshpay-sdk",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"
|
|
5
|
-
"
|
|
6
|
-
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Official Node.js SDK for GardeshPay / PayGate365 Merchant API — payments, verify, refunds, HMAC auth, webhook signature helpers. (npm name gardeshpay was already taken by an unrelated 2020 package.)",
|
|
5
|
+
"license": "MIT",
|
|
6
|
+
"author": "GardeshPay / PayGate365",
|
|
7
|
+
"keywords": [
|
|
8
|
+
"gardeshpay",
|
|
9
|
+
"paygate365",
|
|
10
|
+
"payment",
|
|
11
|
+
"iran",
|
|
12
|
+
"ipg",
|
|
13
|
+
"hmac",
|
|
14
|
+
"merchant-api",
|
|
15
|
+
"psp"
|
|
16
|
+
],
|
|
17
|
+
"homepage": "https://source365.ir/paygate365",
|
|
18
|
+
"repository": {
|
|
19
|
+
"type": "git",
|
|
20
|
+
"url": "git+https://source365.ir/paygate365/gardeshpay.git"
|
|
21
|
+
},
|
|
22
|
+
"bugs": {
|
|
23
|
+
"url": "https://source365.ir/paygate365/gardeshpay/issues"
|
|
24
|
+
},
|
|
25
|
+
"type": "module",
|
|
26
|
+
"main": "./dist/index.cjs",
|
|
27
|
+
"module": "./dist/index.js",
|
|
28
|
+
"types": "./dist/index.d.ts",
|
|
29
|
+
"exports": {
|
|
30
|
+
".": {
|
|
31
|
+
"types": "./dist/index.d.ts",
|
|
32
|
+
"import": "./dist/index.js",
|
|
33
|
+
"require": "./dist/index.cjs"
|
|
34
|
+
}
|
|
35
|
+
},
|
|
36
|
+
"files": [
|
|
37
|
+
"dist",
|
|
38
|
+
"README.md",
|
|
39
|
+
"LICENSE"
|
|
40
|
+
],
|
|
41
|
+
"engines": {
|
|
42
|
+
"node": ">=18"
|
|
43
|
+
},
|
|
44
|
+
"scripts": {
|
|
45
|
+
"build": "tsup src/index.ts --format esm,cjs --dts --clean --sourcemap",
|
|
46
|
+
"test": "node --import tsx --test test/**/*.test.ts",
|
|
47
|
+
"prepublishOnly": "npm run build && npm test",
|
|
48
|
+
"typecheck": "tsc --noEmit"
|
|
49
|
+
},
|
|
50
|
+
"devDependencies": {
|
|
51
|
+
"@types/node": "^22.13.10",
|
|
52
|
+
"tsup": "^8.4.0",
|
|
53
|
+
"tsx": "^4.19.3",
|
|
54
|
+
"typescript": "^5.8.2"
|
|
55
|
+
},
|
|
56
|
+
"publishConfig": {
|
|
57
|
+
"access": "public"
|
|
58
|
+
}
|
|
59
|
+
}
|