@murai-wallet/gateway-stripe 1.0.2
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 +39 -0
- package/dist/index.cjs +153 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +13 -0
- package/dist/index.d.ts +13 -0
- package/dist/index.js +151 -0
- package/dist/index.js.map +1 -0
- package/package.json +52 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2025-2026 murai contributors
|
|
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
ADDED
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
# @murai-wallet/gateway-stripe
|
|
2
|
+
|
|
3
|
+
Stripe Checkout payment gateway adapter for [Murai](https://github.com/ebuntario/murai).
|
|
4
|
+
|
|
5
|
+
Supports global card payments, Apple Pay, and Google Pay via Stripe Checkout.
|
|
6
|
+
|
|
7
|
+
## Install
|
|
8
|
+
|
|
9
|
+
```bash
|
|
10
|
+
npm install @murai-wallet/gateway-stripe @murai-wallet/core stripe
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
## Usage
|
|
14
|
+
|
|
15
|
+
```typescript
|
|
16
|
+
import { createStripeGateway } from '@murai-wallet/gateway-stripe';
|
|
17
|
+
|
|
18
|
+
const gateway = createStripeGateway({
|
|
19
|
+
secretKey: process.env.STRIPE_SECRET_KEY,
|
|
20
|
+
webhookSecret: process.env.STRIPE_WEBHOOK_SECRET,
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
// Use with createCheckoutManager from @murai-wallet/core
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
## Features
|
|
27
|
+
|
|
28
|
+
- Stripe Checkout Session integration
|
|
29
|
+
- Webhook signature verification via Stripe SDK
|
|
30
|
+
- Payment status polling
|
|
31
|
+
- Supports all Stripe-enabled payment methods
|
|
32
|
+
|
|
33
|
+
## Documentation
|
|
34
|
+
|
|
35
|
+
Full docs: [ebuntario.github.io/murai](https://ebuntario.github.io/murai)
|
|
36
|
+
|
|
37
|
+
## License
|
|
38
|
+
|
|
39
|
+
[MIT](../../LICENSE)
|
package/dist/index.cjs
ADDED
|
@@ -0,0 +1,153 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
var crypto = require('crypto');
|
|
4
|
+
var core = require('@murai-wallet/core');
|
|
5
|
+
|
|
6
|
+
// src/index.ts
|
|
7
|
+
var STRIPE_TIMESTAMP_TOLERANCE_SECONDS = 300;
|
|
8
|
+
function parseStripeSignature(header) {
|
|
9
|
+
const parts = header.split(",");
|
|
10
|
+
let timestamp = "";
|
|
11
|
+
const signatures = [];
|
|
12
|
+
for (const part of parts) {
|
|
13
|
+
const [key, value] = part.split("=");
|
|
14
|
+
if (!key || !value) return null;
|
|
15
|
+
if (key === "t") {
|
|
16
|
+
timestamp = value;
|
|
17
|
+
} else if (key === "v1") {
|
|
18
|
+
signatures.push(value);
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
if (!timestamp || signatures.length === 0) return null;
|
|
22
|
+
return { timestamp, signatures };
|
|
23
|
+
}
|
|
24
|
+
var statusMap = {
|
|
25
|
+
paid: "success",
|
|
26
|
+
no_payment_required: "success",
|
|
27
|
+
unpaid: "pending"
|
|
28
|
+
};
|
|
29
|
+
function createStripeGateway(config) {
|
|
30
|
+
const timeoutMs = config.timeoutMs ?? 3e4;
|
|
31
|
+
const authHeader = `Bearer ${config.secretKey}`;
|
|
32
|
+
async function createCheckout(params) {
|
|
33
|
+
const orderId = `${params.userId}-${crypto.randomUUID()}`;
|
|
34
|
+
const body = new URLSearchParams({
|
|
35
|
+
mode: "payment",
|
|
36
|
+
success_url: params.successRedirectUrl,
|
|
37
|
+
cancel_url: params.failureRedirectUrl,
|
|
38
|
+
"line_items[0][price_data][currency]": "usd",
|
|
39
|
+
"line_items[0][price_data][unit_amount]": String(params.amount),
|
|
40
|
+
"line_items[0][price_data][product_data][name]": "Token Top-Up",
|
|
41
|
+
"line_items[0][quantity]": "1",
|
|
42
|
+
"metadata[order_id]": orderId
|
|
43
|
+
});
|
|
44
|
+
const res = await fetch("https://api.stripe.com/v1/checkout/sessions", {
|
|
45
|
+
method: "POST",
|
|
46
|
+
headers: {
|
|
47
|
+
Authorization: authHeader,
|
|
48
|
+
"Content-Type": "application/x-www-form-urlencoded",
|
|
49
|
+
"Idempotency-Key": orderId
|
|
50
|
+
},
|
|
51
|
+
body: body.toString(),
|
|
52
|
+
signal: AbortSignal.timeout(timeoutMs)
|
|
53
|
+
});
|
|
54
|
+
if (!res.ok) {
|
|
55
|
+
throw new core.GatewayError("stripe", res.status, await res.text());
|
|
56
|
+
}
|
|
57
|
+
const data = await res.json();
|
|
58
|
+
return {
|
|
59
|
+
id: orderId,
|
|
60
|
+
userId: params.userId,
|
|
61
|
+
amount: params.amount,
|
|
62
|
+
redirectUrl: data.url,
|
|
63
|
+
status: "pending",
|
|
64
|
+
createdAt: /* @__PURE__ */ new Date()
|
|
65
|
+
};
|
|
66
|
+
}
|
|
67
|
+
async function verifyWebhook(_payload, signature, rawBody) {
|
|
68
|
+
if (!rawBody) {
|
|
69
|
+
throw new core.GatewayError(
|
|
70
|
+
"stripe",
|
|
71
|
+
void 0,
|
|
72
|
+
"rawBody is required for Stripe webhook verification"
|
|
73
|
+
);
|
|
74
|
+
}
|
|
75
|
+
const parsed = parseStripeSignature(signature);
|
|
76
|
+
if (!parsed) return false;
|
|
77
|
+
const timestampSeconds = Number(parsed.timestamp);
|
|
78
|
+
if (!Number.isFinite(timestampSeconds)) return false;
|
|
79
|
+
const nowSeconds = Math.floor(Date.now() / 1e3);
|
|
80
|
+
if (Math.abs(nowSeconds - timestampSeconds) > STRIPE_TIMESTAMP_TOLERANCE_SECONDS) {
|
|
81
|
+
return false;
|
|
82
|
+
}
|
|
83
|
+
const signedPayload = `${parsed.timestamp}.${typeof rawBody === "string" ? rawBody : rawBody.toString("utf8")}`;
|
|
84
|
+
const expectedSig = crypto.createHmac("sha256", config.webhookSecret).update(signedPayload).digest("hex");
|
|
85
|
+
const expectedBuf = Buffer.from(expectedSig, "hex");
|
|
86
|
+
for (const sig of parsed.signatures) {
|
|
87
|
+
try {
|
|
88
|
+
const sigBuf = Buffer.from(sig, "hex");
|
|
89
|
+
if (sigBuf.length !== expectedBuf.length) continue;
|
|
90
|
+
if (crypto.timingSafeEqual(sigBuf, expectedBuf)) return true;
|
|
91
|
+
} catch {
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
return false;
|
|
95
|
+
}
|
|
96
|
+
function parseWebhookPayload(payload) {
|
|
97
|
+
if (typeof payload !== "object" || payload === null) return null;
|
|
98
|
+
const event = payload;
|
|
99
|
+
if (typeof event.type !== "string") return null;
|
|
100
|
+
if (event.type === "checkout.session.completed") {
|
|
101
|
+
const data = event.data;
|
|
102
|
+
if (!data || typeof data.object !== "object" || data.object === null) return null;
|
|
103
|
+
const session = data.object;
|
|
104
|
+
const metadata = session.metadata;
|
|
105
|
+
if (!metadata || typeof metadata.order_id !== "string") return null;
|
|
106
|
+
const amountTotal = session.amount_total;
|
|
107
|
+
if (typeof amountTotal !== "number" || !Number.isFinite(amountTotal)) return null;
|
|
108
|
+
return {
|
|
109
|
+
orderId: metadata.order_id,
|
|
110
|
+
status: "success",
|
|
111
|
+
grossAmount: amountTotal
|
|
112
|
+
};
|
|
113
|
+
}
|
|
114
|
+
if (event.type === "checkout.session.expired") {
|
|
115
|
+
const data = event.data;
|
|
116
|
+
if (!data || typeof data.object !== "object" || data.object === null) return null;
|
|
117
|
+
const session = data.object;
|
|
118
|
+
const metadata = session.metadata;
|
|
119
|
+
if (!metadata || typeof metadata.order_id !== "string") return null;
|
|
120
|
+
const amountTotal = session.amount_total;
|
|
121
|
+
if (typeof amountTotal !== "number" || !Number.isFinite(amountTotal)) return null;
|
|
122
|
+
return {
|
|
123
|
+
orderId: metadata.order_id,
|
|
124
|
+
status: "expired",
|
|
125
|
+
grossAmount: amountTotal
|
|
126
|
+
};
|
|
127
|
+
}
|
|
128
|
+
return null;
|
|
129
|
+
}
|
|
130
|
+
async function getPaymentStatus(sessionId) {
|
|
131
|
+
const res = await fetch(
|
|
132
|
+
`https://api.stripe.com/v1/checkout/sessions/${encodeURIComponent(sessionId)}`,
|
|
133
|
+
{
|
|
134
|
+
headers: {
|
|
135
|
+
Authorization: authHeader,
|
|
136
|
+
Accept: "application/json"
|
|
137
|
+
},
|
|
138
|
+
signal: AbortSignal.timeout(timeoutMs)
|
|
139
|
+
}
|
|
140
|
+
);
|
|
141
|
+
if (!res.ok) {
|
|
142
|
+
throw new core.GatewayError("stripe", res.status, await res.text());
|
|
143
|
+
}
|
|
144
|
+
const data = await res.json();
|
|
145
|
+
const status = statusMap[data.payment_status];
|
|
146
|
+
return status ?? "failed";
|
|
147
|
+
}
|
|
148
|
+
return { createCheckout, verifyWebhook, parseWebhookPayload, getPaymentStatus };
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
exports.createStripeGateway = createStripeGateway;
|
|
152
|
+
//# sourceMappingURL=index.cjs.map
|
|
153
|
+
//# sourceMappingURL=index.cjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/index.ts"],"names":["randomUUID","GatewayError","createHmac","timingSafeEqual"],"mappings":"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","file":"index.cjs","sourcesContent":["// @murai-wallet/gateway-stripe\n// Stripe Checkout payment gateway adapter\n\nimport { createHmac, timingSafeEqual } from 'node:crypto';\nimport { randomUUID } from 'node:crypto';\nimport { GatewayError } from '@murai-wallet/core';\nimport type { CheckoutSession, PaymentGatewayAdapter, WebhookStatus } from '@murai-wallet/core';\n\nexport interface StripeConfig {\n\tsecretKey: string;\n\twebhookSecret: string;\n\t/** Fetch timeout in ms — defaults to 30000 */\n\ttimeoutMs?: number;\n}\n\ninterface StripeSignatureParts {\n\ttimestamp: string;\n\tsignatures: string[];\n}\n\nconst STRIPE_TIMESTAMP_TOLERANCE_SECONDS = 300; // 5 minutes\n\nfunction parseStripeSignature(header: string): StripeSignatureParts | null {\n\tconst parts = header.split(',');\n\tlet timestamp = '';\n\tconst signatures: string[] = [];\n\n\tfor (const part of parts) {\n\t\tconst [key, value] = part.split('=');\n\t\tif (!key || !value) return null;\n\t\tif (key === 't') {\n\t\t\ttimestamp = value;\n\t\t} else if (key === 'v1') {\n\t\t\tsignatures.push(value);\n\t\t}\n\t}\n\n\tif (!timestamp || signatures.length === 0) return null;\n\treturn { timestamp, signatures };\n}\n\nconst statusMap: Record<string, WebhookStatus> = {\n\tpaid: 'success',\n\tno_payment_required: 'success',\n\tunpaid: 'pending',\n};\n\nexport function createStripeGateway(config: StripeConfig): PaymentGatewayAdapter & {\n\tgetPaymentStatus(sessionId: string): Promise<WebhookStatus>;\n} {\n\tconst timeoutMs = config.timeoutMs ?? 30000;\n\tconst authHeader = `Bearer ${config.secretKey}`;\n\n\tasync function createCheckout(params: {\n\t\tuserId: string;\n\t\tamount: number;\n\t\tsuccessRedirectUrl: string;\n\t\tfailureRedirectUrl: string;\n\t}): Promise<CheckoutSession> {\n\t\tconst orderId = `${params.userId}-${randomUUID()}`;\n\n\t\tconst body = new URLSearchParams({\n\t\t\tmode: 'payment',\n\t\t\tsuccess_url: params.successRedirectUrl,\n\t\t\tcancel_url: params.failureRedirectUrl,\n\t\t\t'line_items[0][price_data][currency]': 'usd',\n\t\t\t'line_items[0][price_data][unit_amount]': String(params.amount),\n\t\t\t'line_items[0][price_data][product_data][name]': 'Token Top-Up',\n\t\t\t'line_items[0][quantity]': '1',\n\t\t\t'metadata[order_id]': orderId,\n\t\t});\n\n\t\tconst res = await fetch('https://api.stripe.com/v1/checkout/sessions', {\n\t\t\tmethod: 'POST',\n\t\t\theaders: {\n\t\t\t\tAuthorization: authHeader,\n\t\t\t\t'Content-Type': 'application/x-www-form-urlencoded',\n\t\t\t\t'Idempotency-Key': orderId,\n\t\t\t},\n\t\t\tbody: body.toString(),\n\t\t\tsignal: AbortSignal.timeout(timeoutMs),\n\t\t});\n\n\t\tif (!res.ok) {\n\t\t\tthrow new GatewayError('stripe', res.status, await res.text());\n\t\t}\n\n\t\tconst data = (await res.json()) as { url: string };\n\n\t\treturn {\n\t\t\tid: orderId,\n\t\t\tuserId: params.userId,\n\t\t\tamount: params.amount,\n\t\t\tredirectUrl: data.url,\n\t\t\tstatus: 'pending',\n\t\t\tcreatedAt: new Date(),\n\t\t};\n\t}\n\n\tasync function verifyWebhook(\n\t\t_payload: unknown,\n\t\tsignature: string,\n\t\trawBody?: string | Buffer,\n\t): Promise<boolean> {\n\t\tif (!rawBody) {\n\t\t\tthrow new GatewayError(\n\t\t\t\t'stripe',\n\t\t\t\tundefined,\n\t\t\t\t'rawBody is required for Stripe webhook verification',\n\t\t\t);\n\t\t}\n\n\t\tconst parsed = parseStripeSignature(signature);\n\t\tif (!parsed) return false;\n\n\t\t// Check timestamp tolerance\n\t\tconst timestampSeconds = Number(parsed.timestamp);\n\t\tif (!Number.isFinite(timestampSeconds)) return false;\n\n\t\tconst nowSeconds = Math.floor(Date.now() / 1000);\n\t\tif (Math.abs(nowSeconds - timestampSeconds) > STRIPE_TIMESTAMP_TOLERANCE_SECONDS) {\n\t\t\treturn false;\n\t\t}\n\n\t\t// Compute expected signature\n\t\tconst signedPayload = `${parsed.timestamp}.${typeof rawBody === 'string' ? rawBody : rawBody.toString('utf8')}`;\n\t\tconst expectedSig = createHmac('sha256', config.webhookSecret)\n\t\t\t.update(signedPayload)\n\t\t\t.digest('hex');\n\n\t\t// Timing-safe compare against any v1 signature\n\t\tconst expectedBuf = Buffer.from(expectedSig, 'hex');\n\t\tfor (const sig of parsed.signatures) {\n\t\t\ttry {\n\t\t\t\tconst sigBuf = Buffer.from(sig, 'hex');\n\t\t\t\tif (sigBuf.length !== expectedBuf.length) continue;\n\t\t\t\tif (timingSafeEqual(sigBuf, expectedBuf)) return true;\n\t\t\t} catch {\n\t\t\t\t// invalid hex — skip\n\t\t\t}\n\t\t}\n\n\t\treturn false;\n\t}\n\n\tfunction parseWebhookPayload(\n\t\tpayload: unknown,\n\t): { orderId: string; status: WebhookStatus; grossAmount: number } | null {\n\t\tif (typeof payload !== 'object' || payload === null) return null;\n\n\t\tconst event = payload as {\n\t\t\ttype: unknown;\n\t\t\tdata: unknown;\n\t\t};\n\t\tif (typeof event.type !== 'string') return null;\n\n\t\tif (event.type === 'checkout.session.completed') {\n\t\t\tconst data = event.data as { object: unknown } | undefined;\n\t\t\tif (!data || typeof data.object !== 'object' || data.object === null) return null;\n\n\t\t\tconst session = data.object as {\n\t\t\t\tmetadata: unknown;\n\t\t\t\tamount_total: unknown;\n\t\t\t};\n\n\t\t\tconst metadata = session.metadata as { order_id: unknown } | undefined;\n\t\t\tif (!metadata || typeof metadata.order_id !== 'string') return null;\n\n\t\t\tconst amountTotal = session.amount_total;\n\t\t\tif (typeof amountTotal !== 'number' || !Number.isFinite(amountTotal)) return null;\n\n\t\t\treturn {\n\t\t\t\torderId: metadata.order_id,\n\t\t\t\tstatus: 'success',\n\t\t\t\tgrossAmount: amountTotal,\n\t\t\t};\n\t\t}\n\n\t\tif (event.type === 'checkout.session.expired') {\n\t\t\tconst data = event.data as { object: unknown } | undefined;\n\t\t\tif (!data || typeof data.object !== 'object' || data.object === null) return null;\n\n\t\t\tconst session = data.object as {\n\t\t\t\tmetadata: unknown;\n\t\t\t\tamount_total: unknown;\n\t\t\t};\n\n\t\t\tconst metadata = session.metadata as { order_id: unknown } | undefined;\n\t\t\tif (!metadata || typeof metadata.order_id !== 'string') return null;\n\n\t\t\tconst amountTotal = session.amount_total;\n\t\t\tif (typeof amountTotal !== 'number' || !Number.isFinite(amountTotal)) return null;\n\n\t\t\treturn {\n\t\t\t\torderId: metadata.order_id,\n\t\t\t\tstatus: 'expired',\n\t\t\t\tgrossAmount: amountTotal,\n\t\t\t};\n\t\t}\n\n\t\t// Unrecognized event type\n\t\treturn null;\n\t}\n\n\tasync function getPaymentStatus(sessionId: string): Promise<WebhookStatus> {\n\t\tconst res = await fetch(\n\t\t\t`https://api.stripe.com/v1/checkout/sessions/${encodeURIComponent(sessionId)}`,\n\t\t\t{\n\t\t\t\theaders: {\n\t\t\t\t\tAuthorization: authHeader,\n\t\t\t\t\tAccept: 'application/json',\n\t\t\t\t},\n\t\t\t\tsignal: AbortSignal.timeout(timeoutMs),\n\t\t\t},\n\t\t);\n\n\t\tif (!res.ok) {\n\t\t\tthrow new GatewayError('stripe', res.status, await res.text());\n\t\t}\n\n\t\tconst data = (await res.json()) as { payment_status: string };\n\t\tconst status = statusMap[data.payment_status];\n\t\treturn status ?? 'failed';\n\t}\n\n\treturn { createCheckout, verifyWebhook, parseWebhookPayload, getPaymentStatus };\n}\n"]}
|
package/dist/index.d.cts
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { PaymentGatewayAdapter, WebhookStatus } from '@murai-wallet/core';
|
|
2
|
+
|
|
3
|
+
interface StripeConfig {
|
|
4
|
+
secretKey: string;
|
|
5
|
+
webhookSecret: string;
|
|
6
|
+
/** Fetch timeout in ms — defaults to 30000 */
|
|
7
|
+
timeoutMs?: number;
|
|
8
|
+
}
|
|
9
|
+
declare function createStripeGateway(config: StripeConfig): PaymentGatewayAdapter & {
|
|
10
|
+
getPaymentStatus(sessionId: string): Promise<WebhookStatus>;
|
|
11
|
+
};
|
|
12
|
+
|
|
13
|
+
export { type StripeConfig, createStripeGateway };
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { PaymentGatewayAdapter, WebhookStatus } from '@murai-wallet/core';
|
|
2
|
+
|
|
3
|
+
interface StripeConfig {
|
|
4
|
+
secretKey: string;
|
|
5
|
+
webhookSecret: string;
|
|
6
|
+
/** Fetch timeout in ms — defaults to 30000 */
|
|
7
|
+
timeoutMs?: number;
|
|
8
|
+
}
|
|
9
|
+
declare function createStripeGateway(config: StripeConfig): PaymentGatewayAdapter & {
|
|
10
|
+
getPaymentStatus(sessionId: string): Promise<WebhookStatus>;
|
|
11
|
+
};
|
|
12
|
+
|
|
13
|
+
export { type StripeConfig, createStripeGateway };
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,151 @@
|
|
|
1
|
+
import { randomUUID, createHmac, timingSafeEqual } from 'crypto';
|
|
2
|
+
import { GatewayError } from '@murai-wallet/core';
|
|
3
|
+
|
|
4
|
+
// src/index.ts
|
|
5
|
+
var STRIPE_TIMESTAMP_TOLERANCE_SECONDS = 300;
|
|
6
|
+
function parseStripeSignature(header) {
|
|
7
|
+
const parts = header.split(",");
|
|
8
|
+
let timestamp = "";
|
|
9
|
+
const signatures = [];
|
|
10
|
+
for (const part of parts) {
|
|
11
|
+
const [key, value] = part.split("=");
|
|
12
|
+
if (!key || !value) return null;
|
|
13
|
+
if (key === "t") {
|
|
14
|
+
timestamp = value;
|
|
15
|
+
} else if (key === "v1") {
|
|
16
|
+
signatures.push(value);
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
if (!timestamp || signatures.length === 0) return null;
|
|
20
|
+
return { timestamp, signatures };
|
|
21
|
+
}
|
|
22
|
+
var statusMap = {
|
|
23
|
+
paid: "success",
|
|
24
|
+
no_payment_required: "success",
|
|
25
|
+
unpaid: "pending"
|
|
26
|
+
};
|
|
27
|
+
function createStripeGateway(config) {
|
|
28
|
+
const timeoutMs = config.timeoutMs ?? 3e4;
|
|
29
|
+
const authHeader = `Bearer ${config.secretKey}`;
|
|
30
|
+
async function createCheckout(params) {
|
|
31
|
+
const orderId = `${params.userId}-${randomUUID()}`;
|
|
32
|
+
const body = new URLSearchParams({
|
|
33
|
+
mode: "payment",
|
|
34
|
+
success_url: params.successRedirectUrl,
|
|
35
|
+
cancel_url: params.failureRedirectUrl,
|
|
36
|
+
"line_items[0][price_data][currency]": "usd",
|
|
37
|
+
"line_items[0][price_data][unit_amount]": String(params.amount),
|
|
38
|
+
"line_items[0][price_data][product_data][name]": "Token Top-Up",
|
|
39
|
+
"line_items[0][quantity]": "1",
|
|
40
|
+
"metadata[order_id]": orderId
|
|
41
|
+
});
|
|
42
|
+
const res = await fetch("https://api.stripe.com/v1/checkout/sessions", {
|
|
43
|
+
method: "POST",
|
|
44
|
+
headers: {
|
|
45
|
+
Authorization: authHeader,
|
|
46
|
+
"Content-Type": "application/x-www-form-urlencoded",
|
|
47
|
+
"Idempotency-Key": orderId
|
|
48
|
+
},
|
|
49
|
+
body: body.toString(),
|
|
50
|
+
signal: AbortSignal.timeout(timeoutMs)
|
|
51
|
+
});
|
|
52
|
+
if (!res.ok) {
|
|
53
|
+
throw new GatewayError("stripe", res.status, await res.text());
|
|
54
|
+
}
|
|
55
|
+
const data = await res.json();
|
|
56
|
+
return {
|
|
57
|
+
id: orderId,
|
|
58
|
+
userId: params.userId,
|
|
59
|
+
amount: params.amount,
|
|
60
|
+
redirectUrl: data.url,
|
|
61
|
+
status: "pending",
|
|
62
|
+
createdAt: /* @__PURE__ */ new Date()
|
|
63
|
+
};
|
|
64
|
+
}
|
|
65
|
+
async function verifyWebhook(_payload, signature, rawBody) {
|
|
66
|
+
if (!rawBody) {
|
|
67
|
+
throw new GatewayError(
|
|
68
|
+
"stripe",
|
|
69
|
+
void 0,
|
|
70
|
+
"rawBody is required for Stripe webhook verification"
|
|
71
|
+
);
|
|
72
|
+
}
|
|
73
|
+
const parsed = parseStripeSignature(signature);
|
|
74
|
+
if (!parsed) return false;
|
|
75
|
+
const timestampSeconds = Number(parsed.timestamp);
|
|
76
|
+
if (!Number.isFinite(timestampSeconds)) return false;
|
|
77
|
+
const nowSeconds = Math.floor(Date.now() / 1e3);
|
|
78
|
+
if (Math.abs(nowSeconds - timestampSeconds) > STRIPE_TIMESTAMP_TOLERANCE_SECONDS) {
|
|
79
|
+
return false;
|
|
80
|
+
}
|
|
81
|
+
const signedPayload = `${parsed.timestamp}.${typeof rawBody === "string" ? rawBody : rawBody.toString("utf8")}`;
|
|
82
|
+
const expectedSig = createHmac("sha256", config.webhookSecret).update(signedPayload).digest("hex");
|
|
83
|
+
const expectedBuf = Buffer.from(expectedSig, "hex");
|
|
84
|
+
for (const sig of parsed.signatures) {
|
|
85
|
+
try {
|
|
86
|
+
const sigBuf = Buffer.from(sig, "hex");
|
|
87
|
+
if (sigBuf.length !== expectedBuf.length) continue;
|
|
88
|
+
if (timingSafeEqual(sigBuf, expectedBuf)) return true;
|
|
89
|
+
} catch {
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
return false;
|
|
93
|
+
}
|
|
94
|
+
function parseWebhookPayload(payload) {
|
|
95
|
+
if (typeof payload !== "object" || payload === null) return null;
|
|
96
|
+
const event = payload;
|
|
97
|
+
if (typeof event.type !== "string") return null;
|
|
98
|
+
if (event.type === "checkout.session.completed") {
|
|
99
|
+
const data = event.data;
|
|
100
|
+
if (!data || typeof data.object !== "object" || data.object === null) return null;
|
|
101
|
+
const session = data.object;
|
|
102
|
+
const metadata = session.metadata;
|
|
103
|
+
if (!metadata || typeof metadata.order_id !== "string") return null;
|
|
104
|
+
const amountTotal = session.amount_total;
|
|
105
|
+
if (typeof amountTotal !== "number" || !Number.isFinite(amountTotal)) return null;
|
|
106
|
+
return {
|
|
107
|
+
orderId: metadata.order_id,
|
|
108
|
+
status: "success",
|
|
109
|
+
grossAmount: amountTotal
|
|
110
|
+
};
|
|
111
|
+
}
|
|
112
|
+
if (event.type === "checkout.session.expired") {
|
|
113
|
+
const data = event.data;
|
|
114
|
+
if (!data || typeof data.object !== "object" || data.object === null) return null;
|
|
115
|
+
const session = data.object;
|
|
116
|
+
const metadata = session.metadata;
|
|
117
|
+
if (!metadata || typeof metadata.order_id !== "string") return null;
|
|
118
|
+
const amountTotal = session.amount_total;
|
|
119
|
+
if (typeof amountTotal !== "number" || !Number.isFinite(amountTotal)) return null;
|
|
120
|
+
return {
|
|
121
|
+
orderId: metadata.order_id,
|
|
122
|
+
status: "expired",
|
|
123
|
+
grossAmount: amountTotal
|
|
124
|
+
};
|
|
125
|
+
}
|
|
126
|
+
return null;
|
|
127
|
+
}
|
|
128
|
+
async function getPaymentStatus(sessionId) {
|
|
129
|
+
const res = await fetch(
|
|
130
|
+
`https://api.stripe.com/v1/checkout/sessions/${encodeURIComponent(sessionId)}`,
|
|
131
|
+
{
|
|
132
|
+
headers: {
|
|
133
|
+
Authorization: authHeader,
|
|
134
|
+
Accept: "application/json"
|
|
135
|
+
},
|
|
136
|
+
signal: AbortSignal.timeout(timeoutMs)
|
|
137
|
+
}
|
|
138
|
+
);
|
|
139
|
+
if (!res.ok) {
|
|
140
|
+
throw new GatewayError("stripe", res.status, await res.text());
|
|
141
|
+
}
|
|
142
|
+
const data = await res.json();
|
|
143
|
+
const status = statusMap[data.payment_status];
|
|
144
|
+
return status ?? "failed";
|
|
145
|
+
}
|
|
146
|
+
return { createCheckout, verifyWebhook, parseWebhookPayload, getPaymentStatus };
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
export { createStripeGateway };
|
|
150
|
+
//# sourceMappingURL=index.js.map
|
|
151
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/index.ts"],"names":[],"mappings":"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","file":"index.js","sourcesContent":["// @murai-wallet/gateway-stripe\n// Stripe Checkout payment gateway adapter\n\nimport { createHmac, timingSafeEqual } from 'node:crypto';\nimport { randomUUID } from 'node:crypto';\nimport { GatewayError } from '@murai-wallet/core';\nimport type { CheckoutSession, PaymentGatewayAdapter, WebhookStatus } from '@murai-wallet/core';\n\nexport interface StripeConfig {\n\tsecretKey: string;\n\twebhookSecret: string;\n\t/** Fetch timeout in ms — defaults to 30000 */\n\ttimeoutMs?: number;\n}\n\ninterface StripeSignatureParts {\n\ttimestamp: string;\n\tsignatures: string[];\n}\n\nconst STRIPE_TIMESTAMP_TOLERANCE_SECONDS = 300; // 5 minutes\n\nfunction parseStripeSignature(header: string): StripeSignatureParts | null {\n\tconst parts = header.split(',');\n\tlet timestamp = '';\n\tconst signatures: string[] = [];\n\n\tfor (const part of parts) {\n\t\tconst [key, value] = part.split('=');\n\t\tif (!key || !value) return null;\n\t\tif (key === 't') {\n\t\t\ttimestamp = value;\n\t\t} else if (key === 'v1') {\n\t\t\tsignatures.push(value);\n\t\t}\n\t}\n\n\tif (!timestamp || signatures.length === 0) return null;\n\treturn { timestamp, signatures };\n}\n\nconst statusMap: Record<string, WebhookStatus> = {\n\tpaid: 'success',\n\tno_payment_required: 'success',\n\tunpaid: 'pending',\n};\n\nexport function createStripeGateway(config: StripeConfig): PaymentGatewayAdapter & {\n\tgetPaymentStatus(sessionId: string): Promise<WebhookStatus>;\n} {\n\tconst timeoutMs = config.timeoutMs ?? 30000;\n\tconst authHeader = `Bearer ${config.secretKey}`;\n\n\tasync function createCheckout(params: {\n\t\tuserId: string;\n\t\tamount: number;\n\t\tsuccessRedirectUrl: string;\n\t\tfailureRedirectUrl: string;\n\t}): Promise<CheckoutSession> {\n\t\tconst orderId = `${params.userId}-${randomUUID()}`;\n\n\t\tconst body = new URLSearchParams({\n\t\t\tmode: 'payment',\n\t\t\tsuccess_url: params.successRedirectUrl,\n\t\t\tcancel_url: params.failureRedirectUrl,\n\t\t\t'line_items[0][price_data][currency]': 'usd',\n\t\t\t'line_items[0][price_data][unit_amount]': String(params.amount),\n\t\t\t'line_items[0][price_data][product_data][name]': 'Token Top-Up',\n\t\t\t'line_items[0][quantity]': '1',\n\t\t\t'metadata[order_id]': orderId,\n\t\t});\n\n\t\tconst res = await fetch('https://api.stripe.com/v1/checkout/sessions', {\n\t\t\tmethod: 'POST',\n\t\t\theaders: {\n\t\t\t\tAuthorization: authHeader,\n\t\t\t\t'Content-Type': 'application/x-www-form-urlencoded',\n\t\t\t\t'Idempotency-Key': orderId,\n\t\t\t},\n\t\t\tbody: body.toString(),\n\t\t\tsignal: AbortSignal.timeout(timeoutMs),\n\t\t});\n\n\t\tif (!res.ok) {\n\t\t\tthrow new GatewayError('stripe', res.status, await res.text());\n\t\t}\n\n\t\tconst data = (await res.json()) as { url: string };\n\n\t\treturn {\n\t\t\tid: orderId,\n\t\t\tuserId: params.userId,\n\t\t\tamount: params.amount,\n\t\t\tredirectUrl: data.url,\n\t\t\tstatus: 'pending',\n\t\t\tcreatedAt: new Date(),\n\t\t};\n\t}\n\n\tasync function verifyWebhook(\n\t\t_payload: unknown,\n\t\tsignature: string,\n\t\trawBody?: string | Buffer,\n\t): Promise<boolean> {\n\t\tif (!rawBody) {\n\t\t\tthrow new GatewayError(\n\t\t\t\t'stripe',\n\t\t\t\tundefined,\n\t\t\t\t'rawBody is required for Stripe webhook verification',\n\t\t\t);\n\t\t}\n\n\t\tconst parsed = parseStripeSignature(signature);\n\t\tif (!parsed) return false;\n\n\t\t// Check timestamp tolerance\n\t\tconst timestampSeconds = Number(parsed.timestamp);\n\t\tif (!Number.isFinite(timestampSeconds)) return false;\n\n\t\tconst nowSeconds = Math.floor(Date.now() / 1000);\n\t\tif (Math.abs(nowSeconds - timestampSeconds) > STRIPE_TIMESTAMP_TOLERANCE_SECONDS) {\n\t\t\treturn false;\n\t\t}\n\n\t\t// Compute expected signature\n\t\tconst signedPayload = `${parsed.timestamp}.${typeof rawBody === 'string' ? rawBody : rawBody.toString('utf8')}`;\n\t\tconst expectedSig = createHmac('sha256', config.webhookSecret)\n\t\t\t.update(signedPayload)\n\t\t\t.digest('hex');\n\n\t\t// Timing-safe compare against any v1 signature\n\t\tconst expectedBuf = Buffer.from(expectedSig, 'hex');\n\t\tfor (const sig of parsed.signatures) {\n\t\t\ttry {\n\t\t\t\tconst sigBuf = Buffer.from(sig, 'hex');\n\t\t\t\tif (sigBuf.length !== expectedBuf.length) continue;\n\t\t\t\tif (timingSafeEqual(sigBuf, expectedBuf)) return true;\n\t\t\t} catch {\n\t\t\t\t// invalid hex — skip\n\t\t\t}\n\t\t}\n\n\t\treturn false;\n\t}\n\n\tfunction parseWebhookPayload(\n\t\tpayload: unknown,\n\t): { orderId: string; status: WebhookStatus; grossAmount: number } | null {\n\t\tif (typeof payload !== 'object' || payload === null) return null;\n\n\t\tconst event = payload as {\n\t\t\ttype: unknown;\n\t\t\tdata: unknown;\n\t\t};\n\t\tif (typeof event.type !== 'string') return null;\n\n\t\tif (event.type === 'checkout.session.completed') {\n\t\t\tconst data = event.data as { object: unknown } | undefined;\n\t\t\tif (!data || typeof data.object !== 'object' || data.object === null) return null;\n\n\t\t\tconst session = data.object as {\n\t\t\t\tmetadata: unknown;\n\t\t\t\tamount_total: unknown;\n\t\t\t};\n\n\t\t\tconst metadata = session.metadata as { order_id: unknown } | undefined;\n\t\t\tif (!metadata || typeof metadata.order_id !== 'string') return null;\n\n\t\t\tconst amountTotal = session.amount_total;\n\t\t\tif (typeof amountTotal !== 'number' || !Number.isFinite(amountTotal)) return null;\n\n\t\t\treturn {\n\t\t\t\torderId: metadata.order_id,\n\t\t\t\tstatus: 'success',\n\t\t\t\tgrossAmount: amountTotal,\n\t\t\t};\n\t\t}\n\n\t\tif (event.type === 'checkout.session.expired') {\n\t\t\tconst data = event.data as { object: unknown } | undefined;\n\t\t\tif (!data || typeof data.object !== 'object' || data.object === null) return null;\n\n\t\t\tconst session = data.object as {\n\t\t\t\tmetadata: unknown;\n\t\t\t\tamount_total: unknown;\n\t\t\t};\n\n\t\t\tconst metadata = session.metadata as { order_id: unknown } | undefined;\n\t\t\tif (!metadata || typeof metadata.order_id !== 'string') return null;\n\n\t\t\tconst amountTotal = session.amount_total;\n\t\t\tif (typeof amountTotal !== 'number' || !Number.isFinite(amountTotal)) return null;\n\n\t\t\treturn {\n\t\t\t\torderId: metadata.order_id,\n\t\t\t\tstatus: 'expired',\n\t\t\t\tgrossAmount: amountTotal,\n\t\t\t};\n\t\t}\n\n\t\t// Unrecognized event type\n\t\treturn null;\n\t}\n\n\tasync function getPaymentStatus(sessionId: string): Promise<WebhookStatus> {\n\t\tconst res = await fetch(\n\t\t\t`https://api.stripe.com/v1/checkout/sessions/${encodeURIComponent(sessionId)}`,\n\t\t\t{\n\t\t\t\theaders: {\n\t\t\t\t\tAuthorization: authHeader,\n\t\t\t\t\tAccept: 'application/json',\n\t\t\t\t},\n\t\t\t\tsignal: AbortSignal.timeout(timeoutMs),\n\t\t\t},\n\t\t);\n\n\t\tif (!res.ok) {\n\t\t\tthrow new GatewayError('stripe', res.status, await res.text());\n\t\t}\n\n\t\tconst data = (await res.json()) as { payment_status: string };\n\t\tconst status = statusMap[data.payment_status];\n\t\treturn status ?? 'failed';\n\t}\n\n\treturn { createCheckout, verifyWebhook, parseWebhookPayload, getPaymentStatus };\n}\n"]}
|
package/package.json
ADDED
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@murai-wallet/gateway-stripe",
|
|
3
|
+
"version": "1.0.2",
|
|
4
|
+
"type": "module",
|
|
5
|
+
"description": "Stripe Checkout payment gateway adapter for murai",
|
|
6
|
+
"license": "MIT",
|
|
7
|
+
"main": "./dist/index.cjs",
|
|
8
|
+
"module": "./dist/index.js",
|
|
9
|
+
"types": "./dist/index.d.ts",
|
|
10
|
+
"exports": {
|
|
11
|
+
".": {
|
|
12
|
+
"import": {
|
|
13
|
+
"types": "./dist/index.d.ts",
|
|
14
|
+
"default": "./dist/index.js"
|
|
15
|
+
},
|
|
16
|
+
"require": {
|
|
17
|
+
"types": "./dist/index.d.cts",
|
|
18
|
+
"default": "./dist/index.cjs"
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
},
|
|
22
|
+
"files": [
|
|
23
|
+
"dist"
|
|
24
|
+
],
|
|
25
|
+
"sideEffects": false,
|
|
26
|
+
"repository": {
|
|
27
|
+
"type": "git",
|
|
28
|
+
"url": "git+https://github.com/ebuntario/murai.git",
|
|
29
|
+
"directory": "packages/gateway-stripe"
|
|
30
|
+
},
|
|
31
|
+
"homepage": "https://ebuntario.github.io/murai",
|
|
32
|
+
"bugs": "https://github.com/ebuntario/murai/issues",
|
|
33
|
+
"keywords": [
|
|
34
|
+
"stripe",
|
|
35
|
+
"payment",
|
|
36
|
+
"gateway",
|
|
37
|
+
"checkout"
|
|
38
|
+
],
|
|
39
|
+
"dependencies": {
|
|
40
|
+
"@murai-wallet/core": "1.0.2"
|
|
41
|
+
},
|
|
42
|
+
"engines": {
|
|
43
|
+
"node": ">=22"
|
|
44
|
+
},
|
|
45
|
+
"scripts": {
|
|
46
|
+
"build": "tsup",
|
|
47
|
+
"dev": "tsup --watch",
|
|
48
|
+
"typecheck": "tsc --noEmit",
|
|
49
|
+
"test": "vitest run --passWithNoTests",
|
|
50
|
+
"clean": "rm -rf dist"
|
|
51
|
+
}
|
|
52
|
+
}
|