@porulle/adapter-stripe 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/README.md +48 -0
- package/dist/index.d.ts +9 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +121 -0
- package/package.json +50 -0
- package/src/index.ts +143 -0
package/README.md
ADDED
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
# @porulle/adapter-stripe
|
|
2
|
+
|
|
3
|
+
`PaymentAdapter` for [Stripe](https://stripe.com). The reference implementation of the payment-adapter contract.
|
|
4
|
+
|
|
5
|
+
## Usage
|
|
6
|
+
|
|
7
|
+
```ts
|
|
8
|
+
import { defineConfig } from "@porulle/core";
|
|
9
|
+
import { stripePayment } from "@porulle/adapter-stripe";
|
|
10
|
+
|
|
11
|
+
export default defineConfig({
|
|
12
|
+
payments: [
|
|
13
|
+
stripePayment({
|
|
14
|
+
secretKey: process.env.STRIPE_SECRET_KEY!,
|
|
15
|
+
webhookSecret: process.env.STRIPE_WEBHOOK_SECRET!,
|
|
16
|
+
apiVersion: "2025-08-27.basil", // optional override
|
|
17
|
+
}),
|
|
18
|
+
],
|
|
19
|
+
// …
|
|
20
|
+
});
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
## What it implements
|
|
24
|
+
|
|
25
|
+
| Adapter method | Stripe call |
|
|
26
|
+
|---|---|
|
|
27
|
+
| `createPaymentIntent({ amount, currency })` | `stripe.paymentIntents.create()` |
|
|
28
|
+
| `capturePayment(intentId, amount?)` | `stripe.paymentIntents.capture()` — returns `amountCaptured` (the framework reads this and stores it on the order; refunds are then capped at this value) |
|
|
29
|
+
| `refundPayment(paymentId, amount, reason?)` | `stripe.refunds.create()` |
|
|
30
|
+
| `cancelPaymentIntent(intentId)` | `stripe.paymentIntents.cancel()` |
|
|
31
|
+
| `verifyWebhook(payload, signature)` | `stripe.webhooks.constructEvent()` (timing-safe HMAC compare under the hood) |
|
|
32
|
+
|
|
33
|
+
## Why this is the reference
|
|
34
|
+
|
|
35
|
+
It's the canonical example of the [Payment Adapter Contract](https://github.com/asyncdotengineering/porulle/blob/main/apps/docs/src/content/docs/extending/payment-adapter-contract.mdx):
|
|
36
|
+
|
|
37
|
+
- Returns accurate `amountCaptured` (so refund cap works)
|
|
38
|
+
- Idempotency keys propagate through Stripe's `Idempotency-Key` header
|
|
39
|
+
- Webhook signature verification is timing-safe (Stripe SDK handles)
|
|
40
|
+
- Errors return `Result<T>` — never throws across module boundaries
|
|
41
|
+
- Vendor SDK (`stripe`) is isolated to this package; never leaks into core
|
|
42
|
+
|
|
43
|
+
If you build a new payment adapter, mirror this file's shape.
|
|
44
|
+
|
|
45
|
+
## See also
|
|
46
|
+
|
|
47
|
+
- [Payment Adapter Contract](https://github.com/asyncdotengineering/porulle/blob/main/apps/docs/src/content/docs/extending/payment-adapter-contract.mdx) — the contract this implements
|
|
48
|
+
- [Stripe API reference](https://stripe.com/docs/api)
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import Stripe from "stripe";
|
|
2
|
+
import { type PaymentAdapter } from "@porulle/core";
|
|
3
|
+
export interface StripeAdapterOptions {
|
|
4
|
+
secretKey: string;
|
|
5
|
+
webhookSecret?: string;
|
|
6
|
+
apiVersion?: Stripe.LatestApiVersion;
|
|
7
|
+
}
|
|
8
|
+
export declare function stripePayment(options: StripeAdapterOptions): PaymentAdapter;
|
|
9
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,MAAM,MAAM,QAAQ,CAAC;AAC5B,OAAO,EAGL,KAAK,cAAc,EAMpB,MAAM,eAAe,CAAC;AAEvB,MAAM,WAAW,oBAAoB;IACnC,SAAS,EAAE,MAAM,CAAC;IAClB,aAAa,CAAC,EAAE,MAAM,CAAC;IACvB,UAAU,CAAC,EAAE,MAAM,CAAC,gBAAgB,CAAC;CACtC;AAED,wBAAgB,aAAa,CAAC,OAAO,EAAE,oBAAoB,GAAG,cAAc,CA4H3E"}
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
import Stripe from "stripe";
|
|
2
|
+
import { Err, Ok, } from "@porulle/core";
|
|
3
|
+
export function stripePayment(options) {
|
|
4
|
+
const stripe = new Stripe(options.secretKey, {
|
|
5
|
+
apiVersion: options.apiVersion ?? "2025-08-27.basil",
|
|
6
|
+
});
|
|
7
|
+
return {
|
|
8
|
+
providerId: "stripe",
|
|
9
|
+
async createPaymentIntent(params) {
|
|
10
|
+
try {
|
|
11
|
+
const intent = await stripe.paymentIntents.create({
|
|
12
|
+
amount: params.amount,
|
|
13
|
+
currency: params.currency.toLowerCase(),
|
|
14
|
+
metadata: {
|
|
15
|
+
orderId: params.orderId,
|
|
16
|
+
customerId: params.customerId ?? "",
|
|
17
|
+
...params.metadata,
|
|
18
|
+
},
|
|
19
|
+
automatic_payment_methods: {
|
|
20
|
+
enabled: true,
|
|
21
|
+
},
|
|
22
|
+
});
|
|
23
|
+
return Ok({
|
|
24
|
+
id: intent.id,
|
|
25
|
+
status: intent.status,
|
|
26
|
+
amount: intent.amount,
|
|
27
|
+
currency: intent.currency,
|
|
28
|
+
clientSecret: intent.client_secret,
|
|
29
|
+
});
|
|
30
|
+
}
|
|
31
|
+
catch (error) {
|
|
32
|
+
return Err({
|
|
33
|
+
code: "PAYMENT_INTENT_CREATE_FAILED",
|
|
34
|
+
message: error instanceof Error ? error.message : "Stripe payment intent creation failed.",
|
|
35
|
+
});
|
|
36
|
+
}
|
|
37
|
+
},
|
|
38
|
+
async capturePayment(paymentIntentId, amount) {
|
|
39
|
+
try {
|
|
40
|
+
const captured = await stripe.paymentIntents.capture(paymentIntentId, amount ? { amount_to_capture: amount } : undefined);
|
|
41
|
+
return Ok({
|
|
42
|
+
id: captured.id,
|
|
43
|
+
status: captured.status,
|
|
44
|
+
amountCaptured: captured.amount_received,
|
|
45
|
+
});
|
|
46
|
+
}
|
|
47
|
+
catch (error) {
|
|
48
|
+
return Err({
|
|
49
|
+
code: "PAYMENT_CAPTURE_FAILED",
|
|
50
|
+
message: error instanceof Error ? error.message : "Stripe capture failed.",
|
|
51
|
+
});
|
|
52
|
+
}
|
|
53
|
+
},
|
|
54
|
+
async refundPayment(paymentId, amount, reason) {
|
|
55
|
+
try {
|
|
56
|
+
const params = {
|
|
57
|
+
payment_intent: paymentId,
|
|
58
|
+
amount,
|
|
59
|
+
};
|
|
60
|
+
if (reason != null) {
|
|
61
|
+
// Single-cast: `string` to Stripe's `Reason` enum — structurally compatible
|
|
62
|
+
params.reason = reason;
|
|
63
|
+
}
|
|
64
|
+
const refund = await stripe.refunds.create(params);
|
|
65
|
+
return Ok({
|
|
66
|
+
id: refund.id,
|
|
67
|
+
status: refund.status ?? "pending",
|
|
68
|
+
amountRefunded: refund.amount,
|
|
69
|
+
});
|
|
70
|
+
}
|
|
71
|
+
catch (error) {
|
|
72
|
+
return Err({
|
|
73
|
+
code: "PAYMENT_REFUND_FAILED",
|
|
74
|
+
message: error instanceof Error ? error.message : "Stripe refund failed.",
|
|
75
|
+
});
|
|
76
|
+
}
|
|
77
|
+
},
|
|
78
|
+
async cancelPaymentIntent(paymentIntentId) {
|
|
79
|
+
try {
|
|
80
|
+
await stripe.paymentIntents.cancel(paymentIntentId);
|
|
81
|
+
return Ok(undefined);
|
|
82
|
+
}
|
|
83
|
+
catch (error) {
|
|
84
|
+
return Err({
|
|
85
|
+
code: "PAYMENT_CANCEL_FAILED",
|
|
86
|
+
message: error instanceof Error ? error.message : "Stripe cancellation failed.",
|
|
87
|
+
});
|
|
88
|
+
}
|
|
89
|
+
},
|
|
90
|
+
async verifyWebhook(request) {
|
|
91
|
+
try {
|
|
92
|
+
if (!options.webhookSecret) {
|
|
93
|
+
return Err({
|
|
94
|
+
code: "WEBHOOK_SECRET_MISSING",
|
|
95
|
+
message: "Stripe webhook secret is not configured.",
|
|
96
|
+
});
|
|
97
|
+
}
|
|
98
|
+
const signature = request.headers.get("stripe-signature");
|
|
99
|
+
if (!signature) {
|
|
100
|
+
return Err({
|
|
101
|
+
code: "WEBHOOK_SIGNATURE_MISSING",
|
|
102
|
+
message: "Missing stripe-signature header.",
|
|
103
|
+
});
|
|
104
|
+
}
|
|
105
|
+
const body = await request.text();
|
|
106
|
+
const event = stripe.webhooks.constructEvent(body, signature, options.webhookSecret);
|
|
107
|
+
return Ok({
|
|
108
|
+
id: event.id,
|
|
109
|
+
type: event.type,
|
|
110
|
+
data: event.data.object,
|
|
111
|
+
});
|
|
112
|
+
}
|
|
113
|
+
catch (error) {
|
|
114
|
+
return Err({
|
|
115
|
+
code: "WEBHOOK_VERIFICATION_FAILED",
|
|
116
|
+
message: error instanceof Error ? error.message : "Stripe webhook verification failed.",
|
|
117
|
+
});
|
|
118
|
+
}
|
|
119
|
+
},
|
|
120
|
+
};
|
|
121
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@porulle/adapter-stripe",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"license": "MIT",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"exports": {
|
|
7
|
+
".": {
|
|
8
|
+
"bun": "./src/index.ts",
|
|
9
|
+
"import": "./dist/index.js",
|
|
10
|
+
"types": "./src/index.ts"
|
|
11
|
+
}
|
|
12
|
+
},
|
|
13
|
+
"scripts": {
|
|
14
|
+
"build": "rm -rf dist tsconfig.build.tsbuildinfo && tsc -p tsconfig.build.json",
|
|
15
|
+
"check-types": "tsc --noEmit",
|
|
16
|
+
"lint": "eslint . --max-warnings 1000",
|
|
17
|
+
"test": "vitest run"
|
|
18
|
+
},
|
|
19
|
+
"dependencies": {
|
|
20
|
+
"@porulle/core": "workspace:*",
|
|
21
|
+
"stripe": "^18.5.0"
|
|
22
|
+
},
|
|
23
|
+
"devDependencies": {
|
|
24
|
+
"@repo/eslint-config": "*",
|
|
25
|
+
"@repo/typescript-config": "*",
|
|
26
|
+
"@types/node": "^24.5.2",
|
|
27
|
+
"eslint": "^9.39.1",
|
|
28
|
+
"typescript": "5.9.2",
|
|
29
|
+
"vitest": "^3.2.4"
|
|
30
|
+
},
|
|
31
|
+
"publishConfig": {
|
|
32
|
+
"access": "public"
|
|
33
|
+
},
|
|
34
|
+
"files": [
|
|
35
|
+
"src",
|
|
36
|
+
"dist",
|
|
37
|
+
"README.md"
|
|
38
|
+
],
|
|
39
|
+
"description": "PaymentAdapter for Stripe. The reference implementation of the payment-adapter contract.",
|
|
40
|
+
"homepage": "https://porulle-docs.vercel.app",
|
|
41
|
+
"bugs": {
|
|
42
|
+
"url": "https://github.com/asyncdotengineering/porulle/issues"
|
|
43
|
+
},
|
|
44
|
+
"repository": {
|
|
45
|
+
"type": "git",
|
|
46
|
+
"url": "git+https://github.com/asyncdotengineering/porulle.git",
|
|
47
|
+
"directory": "packages/adapters/adapter-stripe"
|
|
48
|
+
},
|
|
49
|
+
"author": "Porulle contributors"
|
|
50
|
+
}
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
import Stripe from "stripe";
|
|
2
|
+
import {
|
|
3
|
+
Err,
|
|
4
|
+
Ok,
|
|
5
|
+
type PaymentAdapter,
|
|
6
|
+
type PaymentCapture,
|
|
7
|
+
type PaymentIntent,
|
|
8
|
+
type PaymentRefund,
|
|
9
|
+
type PaymentWebhookEvent,
|
|
10
|
+
type Result,
|
|
11
|
+
} from "@porulle/core";
|
|
12
|
+
|
|
13
|
+
export interface StripeAdapterOptions {
|
|
14
|
+
secretKey: string;
|
|
15
|
+
webhookSecret?: string;
|
|
16
|
+
apiVersion?: Stripe.LatestApiVersion;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export function stripePayment(options: StripeAdapterOptions): PaymentAdapter {
|
|
20
|
+
const stripe = new Stripe(options.secretKey, {
|
|
21
|
+
apiVersion: options.apiVersion ?? "2025-08-27.basil",
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
return {
|
|
25
|
+
providerId: "stripe",
|
|
26
|
+
|
|
27
|
+
async createPaymentIntent(params): Promise<Result<PaymentIntent>> {
|
|
28
|
+
try {
|
|
29
|
+
const intent = await stripe.paymentIntents.create({
|
|
30
|
+
amount: params.amount,
|
|
31
|
+
currency: params.currency.toLowerCase(),
|
|
32
|
+
metadata: {
|
|
33
|
+
orderId: params.orderId,
|
|
34
|
+
customerId: params.customerId ?? "",
|
|
35
|
+
...params.metadata,
|
|
36
|
+
},
|
|
37
|
+
automatic_payment_methods: {
|
|
38
|
+
enabled: true,
|
|
39
|
+
},
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
return Ok({
|
|
43
|
+
id: intent.id,
|
|
44
|
+
status: intent.status,
|
|
45
|
+
amount: intent.amount,
|
|
46
|
+
currency: intent.currency,
|
|
47
|
+
clientSecret: intent.client_secret,
|
|
48
|
+
});
|
|
49
|
+
} catch (error) {
|
|
50
|
+
return Err({
|
|
51
|
+
code: "PAYMENT_INTENT_CREATE_FAILED",
|
|
52
|
+
message: error instanceof Error ? error.message : "Stripe payment intent creation failed.",
|
|
53
|
+
});
|
|
54
|
+
}
|
|
55
|
+
},
|
|
56
|
+
|
|
57
|
+
async capturePayment(paymentIntentId: string, amount?: number): Promise<Result<PaymentCapture>> {
|
|
58
|
+
try {
|
|
59
|
+
const captured = await stripe.paymentIntents.capture(paymentIntentId, amount ? { amount_to_capture: amount } : undefined);
|
|
60
|
+
return Ok({
|
|
61
|
+
id: captured.id,
|
|
62
|
+
status: captured.status,
|
|
63
|
+
amountCaptured: captured.amount_received,
|
|
64
|
+
});
|
|
65
|
+
} catch (error) {
|
|
66
|
+
return Err({
|
|
67
|
+
code: "PAYMENT_CAPTURE_FAILED",
|
|
68
|
+
message: error instanceof Error ? error.message : "Stripe capture failed.",
|
|
69
|
+
});
|
|
70
|
+
}
|
|
71
|
+
},
|
|
72
|
+
|
|
73
|
+
async refundPayment(paymentId: string, amount: number, reason?: string): Promise<Result<PaymentRefund>> {
|
|
74
|
+
try {
|
|
75
|
+
const params: Stripe.RefundCreateParams = {
|
|
76
|
+
payment_intent: paymentId,
|
|
77
|
+
amount,
|
|
78
|
+
};
|
|
79
|
+
if (reason != null) {
|
|
80
|
+
// Single-cast: `string` to Stripe's `Reason` enum — structurally compatible
|
|
81
|
+
(params as Record<string, unknown>).reason = reason;
|
|
82
|
+
}
|
|
83
|
+
const refund = await stripe.refunds.create(params);
|
|
84
|
+
|
|
85
|
+
return Ok({
|
|
86
|
+
id: refund.id,
|
|
87
|
+
status: refund.status ?? "pending",
|
|
88
|
+
amountRefunded: refund.amount,
|
|
89
|
+
});
|
|
90
|
+
} catch (error) {
|
|
91
|
+
return Err({
|
|
92
|
+
code: "PAYMENT_REFUND_FAILED",
|
|
93
|
+
message: error instanceof Error ? error.message : "Stripe refund failed.",
|
|
94
|
+
});
|
|
95
|
+
}
|
|
96
|
+
},
|
|
97
|
+
|
|
98
|
+
async cancelPaymentIntent(paymentIntentId: string): Promise<Result<void>> {
|
|
99
|
+
try {
|
|
100
|
+
await stripe.paymentIntents.cancel(paymentIntentId);
|
|
101
|
+
return Ok(undefined);
|
|
102
|
+
} catch (error) {
|
|
103
|
+
return Err({
|
|
104
|
+
code: "PAYMENT_CANCEL_FAILED",
|
|
105
|
+
message: error instanceof Error ? error.message : "Stripe cancellation failed.",
|
|
106
|
+
});
|
|
107
|
+
}
|
|
108
|
+
},
|
|
109
|
+
|
|
110
|
+
async verifyWebhook(request: Request): Promise<Result<PaymentWebhookEvent>> {
|
|
111
|
+
try {
|
|
112
|
+
if (!options.webhookSecret) {
|
|
113
|
+
return Err({
|
|
114
|
+
code: "WEBHOOK_SECRET_MISSING",
|
|
115
|
+
message: "Stripe webhook secret is not configured.",
|
|
116
|
+
});
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
const signature = request.headers.get("stripe-signature");
|
|
120
|
+
if (!signature) {
|
|
121
|
+
return Err({
|
|
122
|
+
code: "WEBHOOK_SIGNATURE_MISSING",
|
|
123
|
+
message: "Missing stripe-signature header.",
|
|
124
|
+
});
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
const body = await request.text();
|
|
128
|
+
const event = stripe.webhooks.constructEvent(body, signature, options.webhookSecret);
|
|
129
|
+
|
|
130
|
+
return Ok({
|
|
131
|
+
id: event.id,
|
|
132
|
+
type: event.type,
|
|
133
|
+
data: event.data.object,
|
|
134
|
+
});
|
|
135
|
+
} catch (error) {
|
|
136
|
+
return Err({
|
|
137
|
+
code: "WEBHOOK_VERIFICATION_FAILED",
|
|
138
|
+
message: error instanceof Error ? error.message : "Stripe webhook verification failed.",
|
|
139
|
+
});
|
|
140
|
+
}
|
|
141
|
+
},
|
|
142
|
+
};
|
|
143
|
+
}
|