@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 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)
@@ -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
+ }