@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 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"]}
@@ -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 };
@@ -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
+ }