@coinomad/checkout 0.0.0-stage → 0.1.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +75 -2
- package/dist/checkouts.d.ts +3 -0
- package/dist/checkouts.js +45 -0
- package/dist/index.d.ts +12 -0
- package/dist/index.js +24 -0
- package/dist/types.d.ts +51 -0
- package/dist/types.js +12 -0
- package/dist/webhooks.d.ts +11 -0
- package/dist/webhooks.js +24 -0
- package/package.json +27 -3
package/README.md
CHANGED
|
@@ -1,3 +1,76 @@
|
|
|
1
|
-
#
|
|
1
|
+
# @coinomad/checkout
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
Server-side SDK for [Coinomad](https://coinomad.xyz)'s programmatic stablecoin checkout API.
|
|
4
|
+
|
|
5
|
+
> Requires Node.js 18+ (uses global `fetch`).
|
|
6
|
+
|
|
7
|
+
## Install
|
|
8
|
+
|
|
9
|
+
```bash
|
|
10
|
+
npm install @coinomad/checkout
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
## Create a checkout
|
|
14
|
+
|
|
15
|
+
```ts
|
|
16
|
+
import { CoinomadCheckout } from '@coinomad/checkout';
|
|
17
|
+
|
|
18
|
+
const coinomad = new CoinomadCheckout({
|
|
19
|
+
apiKey: process.env.COINOMAD_WIDGET_KEY, // ck_test_… or ck_live_…
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
const checkout = await coinomad.checkouts.create({
|
|
23
|
+
amount: '50.00',
|
|
24
|
+
currency: 'USD',
|
|
25
|
+
name: 'Conference Ticket',
|
|
26
|
+
description: 'General admission',
|
|
27
|
+
metadata: { event_id: 'evt_123', ticket_id: 't_456' },
|
|
28
|
+
merchant_reference: 'order_789',
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
// Embed this in an iframe for the customer:
|
|
32
|
+
console.log(checkout.checkout_url);
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
The `apiKey` is the widget's key from your Coinomad dashboard (Stablecoin Collections → Widget → Manage → API keys). `test_mode` checkouts are created automatically with a `ck_test_` key and never require real funds.
|
|
36
|
+
|
|
37
|
+
## Simulate a payment (test mode)
|
|
38
|
+
|
|
39
|
+
With a `ck_test_` key, drive a checkout to completion without moving real funds:
|
|
40
|
+
|
|
41
|
+
```ts
|
|
42
|
+
const result = await coinomad.checkouts.simulate(checkout.id);
|
|
43
|
+
// result.checkout.status === 'paid'
|
|
44
|
+
// result.webhook — { delivered: true, url } — the signed callback that fired
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
`simulate` only works on test-mode checkouts — a live-key checkout rejects it.
|
|
48
|
+
|
|
49
|
+
## Verify a webhook
|
|
50
|
+
|
|
51
|
+
```ts
|
|
52
|
+
import { verifyWebhook } from '@coinomad/checkout';
|
|
53
|
+
|
|
54
|
+
// In your webhook handler — pass the RAW body string, not a re-parsed object:
|
|
55
|
+
app.post('/webhook', express.raw({ type: 'application/json' }), (req, res) => {
|
|
56
|
+
const signature = req.headers['x-coinomad-signature'];
|
|
57
|
+
const ok = verifyWebhook(req.body.toString('utf8'), signature, process.env.COINOMAD_WEBHOOK_SECRET);
|
|
58
|
+
if (!ok) return res.status(401).end();
|
|
59
|
+
|
|
60
|
+
const event = JSON.parse(req.body);
|
|
61
|
+
// event.event === 'checkout.payment.confirmed' — process the payment
|
|
62
|
+
});
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
The `webhook signing secret` is the `whsec_…` value from the widget's dashboard (Stablecoin Collections → Widget → Manage → Integration).
|
|
66
|
+
|
|
67
|
+
## API
|
|
68
|
+
|
|
69
|
+
- `new CoinomadCheckout({ apiKey, baseUrl? })`
|
|
70
|
+
- `coinomad.checkouts.create(params)` → `{ id, reference_code, checkout_url, test_mode, … }`
|
|
71
|
+
- `coinomad.checkouts.simulate(id)` → `{ checkout, webhook }` (test mode only)
|
|
72
|
+
- `verifyWebhook(rawBody, signature, secret)` → `boolean`
|
|
73
|
+
|
|
74
|
+
## Errors
|
|
75
|
+
|
|
76
|
+
Non-2xx responses throw a `CoinomadError` with `.status` and `.code`. 5xx and network errors are retried (2 retries by default); 4xx errors throw immediately.
|
|
@@ -0,0 +1,3 @@
|
|
|
1
|
+
import { type Checkout, type CreateCheckoutParams, type SimulateResult } from './types';
|
|
2
|
+
export declare function createCheckout(apiKey: string, baseUrl: string, params: CreateCheckoutParams, retries?: number): Promise<Checkout>;
|
|
3
|
+
export declare function simulateCheckout(apiKey: string, baseUrl: string, id: string): Promise<SimulateResult>;
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.createCheckout = createCheckout;
|
|
4
|
+
exports.simulateCheckout = simulateCheckout;
|
|
5
|
+
const types_1 = require("./types");
|
|
6
|
+
async function createCheckout(apiKey, baseUrl, params, retries = 2) {
|
|
7
|
+
let lastErr;
|
|
8
|
+
for (let attempt = 0; attempt <= retries; attempt++) {
|
|
9
|
+
try {
|
|
10
|
+
const res = await fetch(`${baseUrl}/api/v1/checkouts`, {
|
|
11
|
+
method: 'POST',
|
|
12
|
+
headers: { 'Content-Type': 'application/json', 'x-api-key': apiKey },
|
|
13
|
+
body: JSON.stringify(params),
|
|
14
|
+
});
|
|
15
|
+
const json = (await res.json().catch(() => ({})));
|
|
16
|
+
if (!res.ok) {
|
|
17
|
+
const err = new types_1.CoinomadError(res.status, json.error?.code ?? 'UNKNOWN', json.error?.message ?? `Request failed with status ${res.status}`);
|
|
18
|
+
if (res.status < 500)
|
|
19
|
+
throw err;
|
|
20
|
+
lastErr = err;
|
|
21
|
+
continue;
|
|
22
|
+
}
|
|
23
|
+
if (!json.data)
|
|
24
|
+
throw new types_1.CoinomadError(200, 'MALFORMED_RESPONSE', 'Response was missing data');
|
|
25
|
+
return json.data;
|
|
26
|
+
}
|
|
27
|
+
catch (err) {
|
|
28
|
+
if (err instanceof types_1.CoinomadError && err.status < 500)
|
|
29
|
+
throw err;
|
|
30
|
+
lastErr = err;
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
throw lastErr instanceof Error ? lastErr : new types_1.CoinomadError(0, 'NETWORK_ERROR', 'Failed to reach the Coinomad API');
|
|
34
|
+
}
|
|
35
|
+
async function simulateCheckout(apiKey, baseUrl, id) {
|
|
36
|
+
const res = await fetch(`${baseUrl}/api/v1/checkouts/${id}/simulate`, {
|
|
37
|
+
method: 'POST',
|
|
38
|
+
headers: { 'x-api-key': apiKey },
|
|
39
|
+
});
|
|
40
|
+
const json = (await res.json().catch(() => ({})));
|
|
41
|
+
if (!res.ok || !json.data) {
|
|
42
|
+
throw new types_1.CoinomadError(res.status, json.error?.code ?? 'UNKNOWN', json.error?.message ?? `Simulate failed with status ${res.status}`);
|
|
43
|
+
}
|
|
44
|
+
return json.data;
|
|
45
|
+
}
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import type { CheckoutConfig, CreateCheckoutParams, Checkout, SimulateResult } from './types';
|
|
2
|
+
export declare class CoinomadCheckout {
|
|
3
|
+
private readonly apiKey;
|
|
4
|
+
private readonly baseUrl;
|
|
5
|
+
constructor(config: CheckoutConfig);
|
|
6
|
+
get checkouts(): {
|
|
7
|
+
create: (params: CreateCheckoutParams) => Promise<Checkout>;
|
|
8
|
+
simulate: (id: string) => Promise<SimulateResult>;
|
|
9
|
+
};
|
|
10
|
+
}
|
|
11
|
+
export { verifyWebhook } from './webhooks';
|
|
12
|
+
export { CoinomadError, type CheckoutConfig, type CreateCheckoutParams, type Checkout, type WebhookPayload, type SimulateResult, } from './types';
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.CoinomadError = exports.verifyWebhook = exports.CoinomadCheckout = void 0;
|
|
4
|
+
const checkouts_1 = require("./checkouts");
|
|
5
|
+
class CoinomadCheckout {
|
|
6
|
+
constructor(config) {
|
|
7
|
+
if (!config.apiKey || !config.apiKey.startsWith('ck_')) {
|
|
8
|
+
throw new Error("Invalid API key — expected a 'ck_test_' or 'ck_live_' key");
|
|
9
|
+
}
|
|
10
|
+
this.apiKey = config.apiKey;
|
|
11
|
+
this.baseUrl = (config.baseUrl ?? 'https://api.coinomad.xyz').replace(/\/+$/, '');
|
|
12
|
+
}
|
|
13
|
+
get checkouts() {
|
|
14
|
+
return {
|
|
15
|
+
create: (params) => (0, checkouts_1.createCheckout)(this.apiKey, this.baseUrl, params),
|
|
16
|
+
simulate: (id) => (0, checkouts_1.simulateCheckout)(this.apiKey, this.baseUrl, id),
|
|
17
|
+
};
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
exports.CoinomadCheckout = CoinomadCheckout;
|
|
21
|
+
var webhooks_1 = require("./webhooks");
|
|
22
|
+
Object.defineProperty(exports, "verifyWebhook", { enumerable: true, get: function () { return webhooks_1.verifyWebhook; } });
|
|
23
|
+
var types_1 = require("./types");
|
|
24
|
+
Object.defineProperty(exports, "CoinomadError", { enumerable: true, get: function () { return types_1.CoinomadError; } });
|
package/dist/types.d.ts
ADDED
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
export interface CheckoutConfig {
|
|
2
|
+
apiKey: string;
|
|
3
|
+
baseUrl?: string;
|
|
4
|
+
}
|
|
5
|
+
export interface CreateCheckoutParams {
|
|
6
|
+
amount: string;
|
|
7
|
+
currency: string;
|
|
8
|
+
name?: string;
|
|
9
|
+
description?: string;
|
|
10
|
+
metadata?: Record<string, unknown>;
|
|
11
|
+
merchant_reference?: string;
|
|
12
|
+
callback_url?: string;
|
|
13
|
+
redirect_url?: string;
|
|
14
|
+
}
|
|
15
|
+
export interface Checkout {
|
|
16
|
+
id: string;
|
|
17
|
+
reference_code: string;
|
|
18
|
+
checkout_url: string;
|
|
19
|
+
status: 'active' | 'expired' | 'paid';
|
|
20
|
+
test_mode: boolean;
|
|
21
|
+
amount_type: 'fixed';
|
|
22
|
+
fixed_amount: string;
|
|
23
|
+
currency_base: string;
|
|
24
|
+
merchant_reference: string | null;
|
|
25
|
+
metadata: Record<string, unknown>;
|
|
26
|
+
}
|
|
27
|
+
export interface WebhookPayload {
|
|
28
|
+
event: string;
|
|
29
|
+
checkout_id: string;
|
|
30
|
+
checkout_reference: string;
|
|
31
|
+
payment_reference: string | null;
|
|
32
|
+
merchant_reference: string | null;
|
|
33
|
+
amount: string;
|
|
34
|
+
asset: string;
|
|
35
|
+
network: string;
|
|
36
|
+
tx_hash: string;
|
|
37
|
+
sender: string;
|
|
38
|
+
test_mode: boolean;
|
|
39
|
+
}
|
|
40
|
+
export interface SimulateResult {
|
|
41
|
+
checkout: Checkout;
|
|
42
|
+
webhook: {
|
|
43
|
+
delivered: boolean;
|
|
44
|
+
url: string | null;
|
|
45
|
+
};
|
|
46
|
+
}
|
|
47
|
+
export declare class CoinomadError extends Error {
|
|
48
|
+
readonly status: number;
|
|
49
|
+
readonly code: string;
|
|
50
|
+
constructor(status: number, code: string, message: string);
|
|
51
|
+
}
|
package/dist/types.js
ADDED
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.CoinomadError = void 0;
|
|
4
|
+
class CoinomadError extends Error {
|
|
5
|
+
constructor(status, code, message) {
|
|
6
|
+
super(message);
|
|
7
|
+
this.name = 'CoinomadError';
|
|
8
|
+
this.status = status;
|
|
9
|
+
this.code = code;
|
|
10
|
+
}
|
|
11
|
+
}
|
|
12
|
+
exports.CoinomadError = CoinomadError;
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Verify a Coinomad webhook signature.
|
|
3
|
+
*
|
|
4
|
+
* The signature is an HMAC-SHA256 of the raw JSON request body, hex-encoded and
|
|
5
|
+
* prefixed with `sha256=`, sent in the `x-coinomad-signature` header.
|
|
6
|
+
*
|
|
7
|
+
* IMPORTANT: pass the RAW request body string (not a re-serialised object).
|
|
8
|
+
* If your framework's JSON body parser consumes the body, configure a raw
|
|
9
|
+
* body capture on the webhook route (e.g. Express `express.raw({ type: 'application/json' })`).
|
|
10
|
+
*/
|
|
11
|
+
export declare function verifyWebhook(rawBody: string, signature: string | undefined | null, secret: string): boolean;
|
package/dist/webhooks.js
ADDED
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.verifyWebhook = verifyWebhook;
|
|
4
|
+
const crypto_1 = require("crypto");
|
|
5
|
+
/**
|
|
6
|
+
* Verify a Coinomad webhook signature.
|
|
7
|
+
*
|
|
8
|
+
* The signature is an HMAC-SHA256 of the raw JSON request body, hex-encoded and
|
|
9
|
+
* prefixed with `sha256=`, sent in the `x-coinomad-signature` header.
|
|
10
|
+
*
|
|
11
|
+
* IMPORTANT: pass the RAW request body string (not a re-serialised object).
|
|
12
|
+
* If your framework's JSON body parser consumes the body, configure a raw
|
|
13
|
+
* body capture on the webhook route (e.g. Express `express.raw({ type: 'application/json' })`).
|
|
14
|
+
*/
|
|
15
|
+
function verifyWebhook(rawBody, signature, secret) {
|
|
16
|
+
if (!signature)
|
|
17
|
+
return false;
|
|
18
|
+
const expected = 'sha256=' + (0, crypto_1.createHmac)('sha256', secret).update(rawBody).digest('hex');
|
|
19
|
+
const a = Buffer.from(expected);
|
|
20
|
+
const b = Buffer.from(signature);
|
|
21
|
+
if (a.length !== b.length)
|
|
22
|
+
return false;
|
|
23
|
+
return (0, crypto_1.timingSafeEqual)(a, b);
|
|
24
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,30 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@coinomad/checkout",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"
|
|
5
|
-
"
|
|
3
|
+
"version": "0.1.1",
|
|
4
|
+
"description": "Server-side SDK for Coinomad's programmatic stablecoin checkout API.",
|
|
5
|
+
"main": "dist/index.js",
|
|
6
|
+
"types": "dist/index.d.ts",
|
|
7
|
+
"files": [
|
|
8
|
+
"dist"
|
|
9
|
+
],
|
|
10
|
+
"scripts": {
|
|
11
|
+
"build": "tsc",
|
|
12
|
+
"prepublishOnly": "npm run build"
|
|
13
|
+
},
|
|
14
|
+
"keywords": [
|
|
15
|
+
"coinomad",
|
|
16
|
+
"checkout",
|
|
17
|
+
"stablecoin",
|
|
18
|
+
"payments",
|
|
19
|
+
"usdc",
|
|
20
|
+
"usdt"
|
|
21
|
+
],
|
|
22
|
+
"license": "MIT",
|
|
23
|
+
"engines": {
|
|
24
|
+
"node": ">=18"
|
|
25
|
+
},
|
|
26
|
+
"devDependencies": {
|
|
27
|
+
"typescript": "^5.4.0",
|
|
28
|
+
"@types/node": "^20.0.0"
|
|
29
|
+
}
|
|
6
30
|
}
|