@dineway-ai/plugin-payment 0.2.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.
@@ -0,0 +1,144 @@
1
+ import Stripe from "stripe";
2
+
3
+ import type {
4
+ StripeRefundInput,
5
+ StripeRefundProvider,
6
+ StripeRefundResult,
7
+ StripeRefundStatus,
8
+ } from "../types.js";
9
+ import { stripeProviderFailure } from "./errors.js";
10
+
11
+ const SAFE_FAILURE_REASON = /^[a-z][a-z0-9_]{0,63}$/;
12
+ const CURRENCY_PATTERN = /^[A-Z]{3}$/i;
13
+
14
+ export interface StripeRefundResponse {
15
+ id: string;
16
+ apiVersion: string;
17
+ paymentIntentId: string | null;
18
+ status: string | null;
19
+ amountMinor: number;
20
+ currency: string;
21
+ failureReason: string | null;
22
+ }
23
+
24
+ export interface StripeRefundClient {
25
+ createRefund(
26
+ params: Stripe.RefundCreateParams,
27
+ options: Pick<Stripe.RequestOptions, "idempotencyKey">,
28
+ ): Promise<StripeRefundResponse>;
29
+ }
30
+
31
+ export type StripeRefundClientFactory = (secretKey: string) => StripeRefundClient;
32
+
33
+ export class StripeNodeRefundProvider implements StripeRefundProvider {
34
+ constructor(
35
+ private readonly createClient: StripeRefundClientFactory = createStripeRefundClient,
36
+ ) {}
37
+
38
+ async createRefund(input: StripeRefundInput): Promise<StripeRefundResult> {
39
+ try {
40
+ assertRefundInput(input);
41
+ const response = await this.createClient(input.secretKey).createRefund(
42
+ {
43
+ payment_intent: input.paymentIntentId,
44
+ amount: input.amountMinor,
45
+ reason: input.reason,
46
+ ...(input.metadata ? { metadata: input.metadata } : {}),
47
+ },
48
+ { idempotencyKey: input.idempotencyKey },
49
+ );
50
+ const status = refundStatus(response.status);
51
+ if (
52
+ !response.id ||
53
+ !response.apiVersion ||
54
+ response.paymentIntentId !== input.paymentIntentId ||
55
+ response.amountMinor !== input.amountMinor ||
56
+ response.currency.toUpperCase() !== input.currency.toUpperCase()
57
+ ) {
58
+ throw new Error("Stripe Refund response does not match the request");
59
+ }
60
+ return {
61
+ ok: true,
62
+ refundId: response.id,
63
+ paymentIntentId: response.paymentIntentId,
64
+ apiVersion: response.apiVersion,
65
+ livemode: input.secretKey.startsWith("sk_live_"),
66
+ status,
67
+ amountMinor: response.amountMinor,
68
+ currency: response.currency.toUpperCase(),
69
+ failureReason:
70
+ response.failureReason && SAFE_FAILURE_REASON.test(response.failureReason)
71
+ ? response.failureReason
72
+ : response.failureReason
73
+ ? "provider_error"
74
+ : null,
75
+ };
76
+ } catch (error) {
77
+ return stripeProviderFailure(error);
78
+ }
79
+ }
80
+ }
81
+
82
+ function createStripeRefundClient(secretKey: string): StripeRefundClient {
83
+ const stripe = new Stripe(secretKey, {
84
+ appInfo: { name: "Dineway Payments" },
85
+ maxNetworkRetries: 1,
86
+ timeout: 15_000,
87
+ typescript: true,
88
+ });
89
+ return {
90
+ async createRefund(params, options) {
91
+ const refund = await stripe.refunds.create(params, options);
92
+ return {
93
+ id: refund.id,
94
+ apiVersion: refund.lastResponse.apiVersion ?? Stripe.API_VERSION,
95
+ paymentIntentId:
96
+ typeof refund.payment_intent === "string"
97
+ ? refund.payment_intent
98
+ : (refund.payment_intent?.id ?? null),
99
+ status: refund.status,
100
+ amountMinor: refund.amount,
101
+ currency: refund.currency,
102
+ failureReason: refund.failure_reason ?? null,
103
+ };
104
+ },
105
+ };
106
+ }
107
+
108
+ function assertRefundInput(input: StripeRefundInput): void {
109
+ if (
110
+ (!input.secretKey.startsWith("sk_test_") && !input.secretKey.startsWith("sk_live_")) ||
111
+ !input.idempotencyKey.trim() ||
112
+ !input.paymentIntentId.startsWith("pi_") ||
113
+ !Number.isSafeInteger(input.amountMinor) ||
114
+ input.amountMinor <= 0 ||
115
+ !CURRENCY_PATTERN.test(input.currency) ||
116
+ !validMetadata(input.metadata)
117
+ ) {
118
+ throw new Error("Stripe Refund input is invalid");
119
+ }
120
+ }
121
+
122
+ function validMetadata(metadata: Record<string, string> | undefined): boolean {
123
+ if (!metadata) return true;
124
+ const entries = Object.entries(metadata);
125
+ return (
126
+ entries.length <= 50 &&
127
+ entries.every(
128
+ ([key, value]) => key.length > 0 && key.length <= 40 && value.length > 0 && value.length <= 500,
129
+ )
130
+ );
131
+ }
132
+
133
+ function refundStatus(value: string | null): StripeRefundStatus {
134
+ if (
135
+ value === "pending" ||
136
+ value === "requires_action" ||
137
+ value === "succeeded" ||
138
+ value === "failed" ||
139
+ value === "canceled"
140
+ ) {
141
+ return value;
142
+ }
143
+ throw new Error("Stripe Refund status is invalid");
144
+ }
@@ -0,0 +1,223 @@
1
+ import { createHash } from "node:crypto";
2
+
3
+ import Stripe from "stripe";
4
+
5
+ import type {
6
+ StripePaymentIntentStatus,
7
+ StripeRefundStatus,
8
+ StripeWebhookEvent,
9
+ StripeWebhookPaymentIntent,
10
+ StripeWebhookRefund,
11
+ StripeWebhookVerificationInput,
12
+ StripeWebhookVerificationResult,
13
+ StripeWebhookVerifier,
14
+ } from "../types.js";
15
+
16
+ export const STRIPE_WEBHOOK_MAX_BYTES = 256 * 1024;
17
+ const WEBHOOK_TOLERANCE_SECONDS = 300;
18
+ const CURRENCY_PATTERN = /^[A-Za-z]{3}$/;
19
+ const PAYMENT_INTENT_STATUSES = new Set<StripePaymentIntentStatus>([
20
+ "requires_payment_method",
21
+ "requires_confirmation",
22
+ "requires_action",
23
+ "processing",
24
+ "requires_capture",
25
+ "canceled",
26
+ "succeeded",
27
+ ]);
28
+ const REFUND_STATUSES = new Set<StripeRefundStatus>([
29
+ "pending",
30
+ "requires_action",
31
+ "succeeded",
32
+ "failed",
33
+ "canceled",
34
+ ]);
35
+ const SAFE_FAILURE_REASON = /^[a-z][a-z0-9_]{0,63}$/;
36
+
37
+ export class StripeNodeWebhookVerifier implements StripeWebhookVerifier {
38
+ verify(input: StripeWebhookVerificationInput): StripeWebhookVerificationResult {
39
+ if (input.rawBody.byteLength === 0 || input.rawBody.byteLength > STRIPE_WEBHOOK_MAX_BYTES) {
40
+ return { ok: false, errorCode: "payload_invalid" };
41
+ }
42
+ if (!input.signature?.trim()) return { ok: false, errorCode: "signature_missing" };
43
+
44
+ let event: Stripe.Event;
45
+ try {
46
+ const stripe = new Stripe(input.secretKey, {
47
+ appInfo: { name: "Dineway Payments" },
48
+ maxNetworkRetries: 0,
49
+ timeout: 15_000,
50
+ typescript: true,
51
+ });
52
+ event = stripe.webhooks.constructEvent(
53
+ input.rawBody,
54
+ input.signature,
55
+ input.webhookSecret,
56
+ WEBHOOK_TOLERANCE_SECONDS,
57
+ undefined,
58
+ input.receivedAt,
59
+ );
60
+ } catch (error) {
61
+ return {
62
+ ok: false,
63
+ errorCode:
64
+ error instanceof Stripe.errors.StripeSignatureVerificationError
65
+ ? "signature_invalid"
66
+ : "payload_invalid",
67
+ };
68
+ }
69
+
70
+ try {
71
+ return { ok: true, event: projectEvent(event, input.rawBody) };
72
+ } catch {
73
+ return { ok: false, errorCode: "event_invalid" };
74
+ }
75
+ }
76
+ }
77
+
78
+ function projectEvent(event: Stripe.Event, rawBody: Uint8Array): StripeWebhookEvent {
79
+ assertSafeString(event.id, 255);
80
+ assertSafeString(event.type, 255);
81
+ assertSafeInteger(event.created);
82
+ if (typeof event.livemode !== "boolean") throw new Error("Invalid Stripe event mode");
83
+ if (event.api_version !== null) assertSafeString(event.api_version, 64);
84
+
85
+ const objectRecord = asRecord(event.data.object as unknown);
86
+ const objectType = optionalSafeString(objectRecord?.object, 128);
87
+ const objectId = optionalSafeString(objectRecord?.id, 255);
88
+ if ((objectType === null) !== (objectId === null)) throw new Error("Incomplete Stripe object");
89
+
90
+ return {
91
+ eventId: event.id,
92
+ eventType: event.type,
93
+ eventCreated: event.created,
94
+ livemode: event.livemode,
95
+ apiVersion: event.api_version,
96
+ payloadSha256: `sha256:${createHash("sha256").update(rawBody).digest("hex")}`,
97
+ objectType,
98
+ objectId,
99
+ paymentIntent:
100
+ objectType === "payment_intent" ? projectPaymentIntent(objectRecord) : null,
101
+ refund: objectType === "refund" ? projectRefund(objectRecord) : null,
102
+ };
103
+ }
104
+
105
+ function projectPaymentIntent(value: Record<string, unknown> | null): StripeWebhookPaymentIntent {
106
+ if (!value) throw new Error("Stripe PaymentIntent is missing");
107
+ const status = requiredSafeString(value.status, 64);
108
+ if (!PAYMENT_INTENT_STATUSES.has(status as StripePaymentIntentStatus)) {
109
+ throw new Error("Stripe PaymentIntent status is invalid");
110
+ }
111
+ const incrementalAuthorizationSupported = readIncrementalAuthorizationSupported(value);
112
+ return {
113
+ id: requiredSafeString(value.id, 255),
114
+ status: status as StripePaymentIntentStatus,
115
+ amountMinor: requiredSafeInteger(value.amount),
116
+ amountReceivedMinor: requiredSafeInteger(value.amount_received),
117
+ amountCapturableMinor: requiredSafeInteger(value.amount_capturable),
118
+ currency: requiredCurrency(value.currency),
119
+ ...(incrementalAuthorizationSupported === undefined
120
+ ? {}
121
+ : { incrementalAuthorizationSupported }),
122
+ metadata: projectMetadata(value.metadata),
123
+ };
124
+ }
125
+
126
+ function projectRefund(value: Record<string, unknown> | null): StripeWebhookRefund {
127
+ if (!value) throw new Error("Stripe Refund is missing");
128
+ const status = requiredSafeString(value.status, 64);
129
+ if (!REFUND_STATUSES.has(status as StripeRefundStatus)) {
130
+ throw new Error("Stripe Refund status is invalid");
131
+ }
132
+ const failureReason = optionalSafeString(value.failure_reason, 64);
133
+ return {
134
+ id: requiredSafeString(value.id, 255),
135
+ status: status as StripeRefundStatus,
136
+ paymentIntentId: requiredRelationId(value.payment_intent, "pi_"),
137
+ amountMinor: requiredSafeInteger(value.amount),
138
+ currency: requiredCurrency(value.currency),
139
+ failureReason:
140
+ failureReason && SAFE_FAILURE_REASON.test(failureReason)
141
+ ? failureReason
142
+ : failureReason
143
+ ? "provider_error"
144
+ : null,
145
+ metadata: projectMetadata(value.metadata),
146
+ };
147
+ }
148
+
149
+ function projectMetadata(value: unknown): Record<string, string> {
150
+ const record = asRecord(value);
151
+ if (!record) return {};
152
+ const entries = Object.entries(record);
153
+ if (entries.length > 50) throw new Error("Stripe metadata exceeds the supported limit");
154
+ return Object.fromEntries(
155
+ entries.map(([key, item]) => {
156
+ assertSafeString(key, 40);
157
+ assertSafeString(item, 500);
158
+ return [key, item];
159
+ }),
160
+ );
161
+ }
162
+
163
+ function readIncrementalAuthorizationSupported(
164
+ value: Record<string, unknown>,
165
+ ): boolean | undefined {
166
+ const latestCharge = asRecord(value.latest_charge);
167
+ const paymentMethodDetails = asRecord(latestCharge?.payment_method_details);
168
+ const cardPresent = asRecord(paymentMethodDetails?.card_present);
169
+ if (!cardPresent || !("incremental_authorization_supported" in cardPresent)) return undefined;
170
+ const supported = cardPresent.incremental_authorization_supported;
171
+ if (typeof supported !== "boolean") {
172
+ throw new Error("Invalid Stripe incremental authorization capability");
173
+ }
174
+ return supported;
175
+ }
176
+
177
+ function asRecord(value: unknown): Record<string, unknown> | null {
178
+ return typeof value === "object" && value !== null && !Array.isArray(value)
179
+ ? (value as Record<string, unknown>)
180
+ : null;
181
+ }
182
+
183
+ function requiredSafeString(value: unknown, maxLength: number): string {
184
+ assertSafeString(value, maxLength);
185
+ return value;
186
+ }
187
+
188
+ function optionalSafeString(value: unknown, maxLength: number): string | null {
189
+ if (value === undefined || value === null || value === "") return null;
190
+ assertSafeString(value, maxLength);
191
+ return value;
192
+ }
193
+
194
+ function assertSafeString(value: unknown, maxLength: number): asserts value is string {
195
+ if (typeof value !== "string" || value.length === 0 || value.length > maxLength) {
196
+ throw new Error("Invalid Stripe string");
197
+ }
198
+ }
199
+
200
+ function requiredSafeInteger(value: unknown): number {
201
+ assertSafeInteger(value);
202
+ return value;
203
+ }
204
+
205
+ function assertSafeInteger(value: unknown): asserts value is number {
206
+ if (!Number.isSafeInteger(value) || (value as number) < 0) {
207
+ throw new Error("Invalid Stripe integer");
208
+ }
209
+ }
210
+
211
+ function requiredCurrency(value: unknown): string {
212
+ const currency = requiredSafeString(value, 3);
213
+ if (!CURRENCY_PATTERN.test(currency)) throw new Error("Invalid Stripe currency");
214
+ return currency.toUpperCase();
215
+ }
216
+
217
+ function requiredRelationId(value: unknown, prefix: string): string {
218
+ const record = asRecord(value);
219
+ const id = typeof value === "string" ? value : record?.id;
220
+ const normalized = requiredSafeString(id, 255);
221
+ if (!normalized.startsWith(prefix)) throw new Error("Invalid Stripe relation");
222
+ return normalized;
223
+ }
package/src/types.ts ADDED
@@ -0,0 +1,290 @@
1
+ export type StripeMode = "test" | "live";
2
+ export type StripeTaxStatus = "unknown" | "active" | "pending" | "unavailable";
3
+
4
+ export type StripeProviderErrorCode =
5
+ | "authentication_failed"
6
+ | "permission_denied"
7
+ | "rate_limited"
8
+ | "connection_error"
9
+ | "invalid_request"
10
+ | "provider_error";
11
+
12
+ export interface StripeProviderFailure {
13
+ ok: false;
14
+ errorCode: StripeProviderErrorCode;
15
+ retryable: boolean;
16
+ }
17
+
18
+ export interface StripeAccountProbeInput {
19
+ mode: StripeMode;
20
+ secretKey: string;
21
+ }
22
+
23
+ export type StripeAccountProbeResult =
24
+ | {
25
+ ok: true;
26
+ accountId: string;
27
+ livemode: boolean;
28
+ chargesEnabled: boolean;
29
+ detailsSubmitted: boolean;
30
+ taxStatus: Exclude<StripeTaxStatus, "unknown">;
31
+ }
32
+ | {
33
+ ok: false;
34
+ errorCode: string;
35
+ };
36
+
37
+ export interface StripeAccountProbe {
38
+ probe(input: StripeAccountProbeInput): Promise<StripeAccountProbeResult>;
39
+ }
40
+
41
+ export type StripePaymentIntentStatus =
42
+ | "requires_payment_method"
43
+ | "requires_confirmation"
44
+ | "requires_action"
45
+ | "processing"
46
+ | "requires_capture"
47
+ | "canceled"
48
+ | "succeeded";
49
+
50
+ export type StripeRefundStatus =
51
+ | "pending"
52
+ | "requires_action"
53
+ | "succeeded"
54
+ | "failed"
55
+ | "canceled";
56
+
57
+ export type StripeRefundReason = "duplicate" | "fraudulent" | "requested_by_customer";
58
+
59
+ export interface StripeRefundInput {
60
+ secretKey: string;
61
+ idempotencyKey: string;
62
+ paymentIntentId: string;
63
+ amountMinor: number;
64
+ currency: string;
65
+ reason: StripeRefundReason;
66
+ metadata?: Record<string, string>;
67
+ }
68
+
69
+ export interface StripeRefundSuccess {
70
+ ok: true;
71
+ refundId: string;
72
+ paymentIntentId: string;
73
+ apiVersion: string;
74
+ livemode: boolean;
75
+ status: StripeRefundStatus;
76
+ amountMinor: number;
77
+ currency: string;
78
+ failureReason: string | null;
79
+ }
80
+
81
+ export type StripeRefundResult = StripeRefundSuccess | StripeProviderFailure;
82
+
83
+ export interface StripeRefundProvider {
84
+ createRefund(input: StripeRefundInput): Promise<StripeRefundResult>;
85
+ }
86
+
87
+ export interface StripeAddress {
88
+ countryCode: string;
89
+ line1?: string;
90
+ line2?: string;
91
+ city?: string;
92
+ region?: string;
93
+ postalCode?: string;
94
+ }
95
+
96
+ export interface StripeTaxLineInput {
97
+ reference: string;
98
+ amountMinor: number;
99
+ quantity: number;
100
+ taxCode: string | null;
101
+ taxBehavior: "exclusive" | "inclusive";
102
+ }
103
+
104
+ export interface StripeTaxCalculationInput {
105
+ secretKey: string;
106
+ idempotencyKey: string;
107
+ currency: string;
108
+ customerAddress: StripeAddress;
109
+ customerAddressSource: "billing" | "shipping";
110
+ shipFromAddress: StripeAddress;
111
+ lines: StripeTaxLineInput[];
112
+ taxDate?: number;
113
+ }
114
+
115
+ export interface StripeTaxLineBreakdown {
116
+ amountMinor: number;
117
+ taxableAmountMinor: number;
118
+ taxabilityReason: string;
119
+ sourcing: "destination" | "origin";
120
+ jurisdiction: {
121
+ countryCode: string;
122
+ regionCode: string | null;
123
+ displayName: string;
124
+ level: "city" | "country" | "county" | "district" | "state";
125
+ };
126
+ rate: {
127
+ displayName: string;
128
+ percentage: string;
129
+ taxType: string;
130
+ } | null;
131
+ }
132
+
133
+ export interface StripeTaxCalculationSuccess {
134
+ ok: true;
135
+ calculationId: string;
136
+ apiVersion: string;
137
+ livemode: boolean;
138
+ currency: string;
139
+ expiresAt: number | null;
140
+ amountTotalMinor: number;
141
+ taxAmountExclusiveMinor: number;
142
+ taxAmountInclusiveMinor: number;
143
+ lines: Array<{
144
+ reference: string;
145
+ amountMinor: number;
146
+ taxMinor: number;
147
+ quantity: number;
148
+ taxBehavior: "exclusive" | "inclusive";
149
+ taxCode: string;
150
+ breakdown: StripeTaxLineBreakdown[];
151
+ }>;
152
+ }
153
+
154
+ export type StripeTaxCalculationResult = StripeTaxCalculationSuccess | StripeProviderFailure;
155
+
156
+ export interface StripePaymentIntentInput {
157
+ secretKey: string;
158
+ idempotencyKey: string;
159
+ amountMinor: number;
160
+ currency: string;
161
+ description: string;
162
+ metadata?: Record<string, string>;
163
+ receiptEmail?: string | null;
164
+ captureMethod?: "automatic" | "manual";
165
+ requestIncrementalAuthorizationSupport?: boolean;
166
+ requestOvercapture?: boolean;
167
+ }
168
+
169
+ export interface StripePaymentIntentSuccess {
170
+ ok: true;
171
+ paymentIntentId: string;
172
+ apiVersion: string;
173
+ clientSecret: string;
174
+ livemode: boolean;
175
+ status: StripePaymentIntentStatus;
176
+ amountMinor: number;
177
+ amountReceivedMinor: number;
178
+ amountCapturableMinor: number;
179
+ currency: string;
180
+ incrementalAuthorizationSupported?: boolean | null;
181
+ }
182
+
183
+ export type StripePaymentIntentResult = StripePaymentIntentSuccess | StripeProviderFailure;
184
+
185
+ export interface StripePaymentIntentOperationInput {
186
+ secretKey: string;
187
+ idempotencyKey: string;
188
+ paymentIntentId: string;
189
+ currency: string;
190
+ metadata?: Record<string, string>;
191
+ }
192
+
193
+ export interface StripeCapturePaymentIntentInput extends StripePaymentIntentOperationInput {
194
+ amountMinor: number;
195
+ }
196
+
197
+ export interface StripeIncrementAuthorizationInput extends StripePaymentIntentOperationInput {
198
+ amountMinor: number;
199
+ }
200
+
201
+ export interface StripeCancelPaymentIntentInput extends StripePaymentIntentOperationInput {
202
+ reason?: "duplicate" | "fraudulent" | "requested_by_customer" | "abandoned";
203
+ }
204
+
205
+ export interface StripePaymentIntentOperationSuccess {
206
+ ok: true;
207
+ paymentIntentId: string;
208
+ apiVersion: string;
209
+ livemode: boolean;
210
+ status: StripePaymentIntentStatus;
211
+ amountMinor: number;
212
+ amountReceivedMinor: number;
213
+ amountCapturableMinor: number;
214
+ currency: string;
215
+ incrementalAuthorizationSupported?: boolean | null;
216
+ }
217
+
218
+ export type StripePaymentIntentOperationResult =
219
+ | StripePaymentIntentOperationSuccess
220
+ | StripeProviderFailure;
221
+
222
+ export interface StripeCheckoutProvider {
223
+ calculateTax(input: StripeTaxCalculationInput): Promise<StripeTaxCalculationResult>;
224
+ createPaymentIntent(input: StripePaymentIntentInput): Promise<StripePaymentIntentResult>;
225
+ capturePaymentIntent(
226
+ input: StripeCapturePaymentIntentInput,
227
+ ): Promise<StripePaymentIntentOperationResult>;
228
+ incrementAuthorization(
229
+ input: StripeIncrementAuthorizationInput,
230
+ ): Promise<StripePaymentIntentOperationResult>;
231
+ cancelPaymentIntent(
232
+ input: StripeCancelPaymentIntentInput,
233
+ ): Promise<StripePaymentIntentOperationResult>;
234
+ }
235
+
236
+ export type StripeWebhookErrorCode =
237
+ | "signature_missing"
238
+ | "signature_invalid"
239
+ | "payload_invalid"
240
+ | "event_invalid";
241
+
242
+ export interface StripeWebhookPaymentIntent {
243
+ id: string;
244
+ status: StripePaymentIntentStatus;
245
+ amountMinor: number;
246
+ amountReceivedMinor: number;
247
+ amountCapturableMinor: number;
248
+ currency: string;
249
+ incrementalAuthorizationSupported?: boolean | null;
250
+ metadata: Record<string, string>;
251
+ }
252
+
253
+ export interface StripeWebhookRefund {
254
+ id: string;
255
+ status: StripeRefundStatus;
256
+ paymentIntentId: string;
257
+ amountMinor: number;
258
+ currency: string;
259
+ failureReason: string | null;
260
+ metadata: Record<string, string>;
261
+ }
262
+
263
+ export interface StripeWebhookEvent {
264
+ eventId: string;
265
+ eventType: string;
266
+ eventCreated: number;
267
+ livemode: boolean;
268
+ apiVersion: string | null;
269
+ payloadSha256: string;
270
+ objectType: string | null;
271
+ objectId: string | null;
272
+ paymentIntent: StripeWebhookPaymentIntent | null;
273
+ refund: StripeWebhookRefund | null;
274
+ }
275
+
276
+ export type StripeWebhookVerificationResult =
277
+ | { ok: true; event: StripeWebhookEvent }
278
+ | { ok: false; errorCode: StripeWebhookErrorCode };
279
+
280
+ export interface StripeWebhookVerificationInput {
281
+ secretKey: string;
282
+ webhookSecret: string;
283
+ signature: string | null;
284
+ rawBody: Uint8Array;
285
+ receivedAt?: number;
286
+ }
287
+
288
+ export interface StripeWebhookVerifier {
289
+ verify(input: StripeWebhookVerificationInput): StripeWebhookVerificationResult;
290
+ }