@alphawebai/hs-prism 0.1.0-rc.27
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 +201 -0
- package/README.md +493 -0
- package/dist/src/alphaweb/contracts.d.ts +6 -0
- package/dist/src/alphaweb/contracts.js +14 -0
- package/dist/src/alphaweb/error.d.ts +13 -0
- package/dist/src/alphaweb/error.js +100 -0
- package/dist/src/alphaweb/index.d.ts +3 -0
- package/dist/src/alphaweb/index.js +22 -0
- package/dist/src/alphaweb/manifest.d.ts +28 -0
- package/dist/src/alphaweb/manifest.js +55 -0
- package/dist/src/alphaweb/money.d.ts +19 -0
- package/dist/src/alphaweb/money.js +128 -0
- package/dist/src/alphaweb/stripe-connector.d.ts +58 -0
- package/dist/src/alphaweb/stripe-connector.js +1402 -0
- package/dist/src/alphaweb/types.d.ts +255 -0
- package/dist/src/alphaweb/types.js +3 -0
- package/dist/src/http_client.d.ts +58 -0
- package/dist/src/http_client.js +179 -0
- package/dist/src/index.d.ts +10 -0
- package/dist/src/index.js +41 -0
- package/dist/src/payments/_generated_connector_client_flows.d.ts +112 -0
- package/dist/src/payments/_generated_connector_client_flows.js +215 -0
- package/dist/src/payments/_generated_flows.js +141 -0
- package/dist/src/payments/_generated_grpc_client.d.ts +194 -0
- package/dist/src/payments/_generated_grpc_client.js +905 -0
- package/dist/src/payments/_generated_uniffi_client_flows.d.ts +169 -0
- package/dist/src/payments/_generated_uniffi_client_flows.js +342 -0
- package/dist/src/payments/connector_client.d.ts +51 -0
- package/dist/src/payments/connector_client.js +152 -0
- package/dist/src/payments/errors.d.ts +27 -0
- package/dist/src/payments/errors.js +39 -0
- package/dist/src/payments/generated/libconnector_service_ffi.so +0 -0
- package/dist/src/payments/generated/proto.d.ts +49087 -0
- package/dist/src/payments/generated/proto.js +154163 -0
- package/dist/src/payments/grpc_client.d.ts +1 -0
- package/dist/src/payments/grpc_client.js +20 -0
- package/dist/src/payments/uniffi_client.d.ts +40 -0
- package/dist/src/payments/uniffi_client.js +249 -0
- package/package.json +79 -0
|
@@ -0,0 +1,1402 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.PrismStripeConnector = void 0;
|
|
4
|
+
exports.verifyPrismStripeRuntimeCredential = verifyPrismStripeRuntimeCredential;
|
|
5
|
+
const node_crypto_1 = require("node:crypto");
|
|
6
|
+
const _generated_connector_client_flows_1 = require("../payments/_generated_connector_client_flows");
|
|
7
|
+
const errors_1 = require("../payments/errors");
|
|
8
|
+
const proto_1 = require("../payments/generated/proto");
|
|
9
|
+
const error_1 = require("./error");
|
|
10
|
+
const manifest_1 = require("./manifest");
|
|
11
|
+
const money_1 = require("./money");
|
|
12
|
+
const STRIPE_API_BASE = 'https://api.stripe.com/v1';
|
|
13
|
+
const OPERATION_METADATA_KEYS = new Set(['medusa_payment_session_id', 'alphaweb_owner_id']);
|
|
14
|
+
function recordOf(value) {
|
|
15
|
+
return typeof value === 'object' && value !== null ? value : {};
|
|
16
|
+
}
|
|
17
|
+
function stringOf(value) {
|
|
18
|
+
return typeof value === 'string' && value.length > 0 ? value : null;
|
|
19
|
+
}
|
|
20
|
+
function integerOf(value) {
|
|
21
|
+
return typeof value === 'number' && Number.isSafeInteger(value) ? value : null;
|
|
22
|
+
}
|
|
23
|
+
function secret(value) {
|
|
24
|
+
return { value };
|
|
25
|
+
}
|
|
26
|
+
function configurationError(safeMessage, code) {
|
|
27
|
+
return new error_1.PrismOperationError({
|
|
28
|
+
category: 'configuration',
|
|
29
|
+
providerCode: code,
|
|
30
|
+
retryable: false,
|
|
31
|
+
outcome: 'definitely_not_applied',
|
|
32
|
+
safeMessage,
|
|
33
|
+
});
|
|
34
|
+
}
|
|
35
|
+
function validationError(safeMessage, code) {
|
|
36
|
+
return new error_1.PrismOperationError({
|
|
37
|
+
category: 'validation',
|
|
38
|
+
providerCode: code,
|
|
39
|
+
retryable: false,
|
|
40
|
+
outcome: 'definitely_not_applied',
|
|
41
|
+
safeMessage,
|
|
42
|
+
});
|
|
43
|
+
}
|
|
44
|
+
function assertIdentifier(value, name) {
|
|
45
|
+
if (!/^[A-Za-z0-9:_-]{1,255}$/.test(value)) {
|
|
46
|
+
throw validationError(`${name} is invalid.`, 'PRISM_INVALID_IDENTIFIER');
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
function currencyEnum(currency) {
|
|
50
|
+
const normalized = currency.trim().toUpperCase();
|
|
51
|
+
const value = proto_1.types.Currency[normalized];
|
|
52
|
+
if (typeof value !== 'number') {
|
|
53
|
+
throw validationError('Currency is not supported by the native Prism contract.', 'PRISM_CURRENCY');
|
|
54
|
+
}
|
|
55
|
+
return value;
|
|
56
|
+
}
|
|
57
|
+
function moneyMessage(money) {
|
|
58
|
+
const normalized = (0, money_1.assertSafeProviderMoney)(money);
|
|
59
|
+
return { minorAmount: normalized.amountMinor, currency: currencyEnum(normalized.currency) };
|
|
60
|
+
}
|
|
61
|
+
function paymentStatus(status) {
|
|
62
|
+
switch (status) {
|
|
63
|
+
case proto_1.types.PaymentStatus.PAYMENT_METHOD_AWAITED:
|
|
64
|
+
case proto_1.types.PaymentStatus.CONFIRMATION_AWAITED:
|
|
65
|
+
return 'requires_payment_method';
|
|
66
|
+
case proto_1.types.PaymentStatus.AUTHENTICATION_PENDING:
|
|
67
|
+
return 'requires_customer_action';
|
|
68
|
+
case proto_1.types.PaymentStatus.AUTHORIZING:
|
|
69
|
+
case proto_1.types.PaymentStatus.STARTED:
|
|
70
|
+
case proto_1.types.PaymentStatus.PENDING:
|
|
71
|
+
case proto_1.types.PaymentStatus.CAPTURE_INITIATED:
|
|
72
|
+
return 'pending_authorization';
|
|
73
|
+
case proto_1.types.PaymentStatus.AUTHORIZED:
|
|
74
|
+
case proto_1.types.PaymentStatus.PARTIALLY_AUTHORIZED:
|
|
75
|
+
return 'authorized';
|
|
76
|
+
case proto_1.types.PaymentStatus.CHARGED:
|
|
77
|
+
case proto_1.types.PaymentStatus.PARTIAL_CHARGED:
|
|
78
|
+
case proto_1.types.PaymentStatus.PARTIAL_CHARGED_AND_CHARGEABLE:
|
|
79
|
+
return 'captured';
|
|
80
|
+
case proto_1.types.PaymentStatus.VOIDED:
|
|
81
|
+
case proto_1.types.PaymentStatus.VOIDED_POST_CAPTURE:
|
|
82
|
+
case proto_1.types.PaymentStatus.EXPIRED:
|
|
83
|
+
return 'canceled';
|
|
84
|
+
case proto_1.types.PaymentStatus.AUTHENTICATION_FAILED:
|
|
85
|
+
case proto_1.types.PaymentStatus.AUTHORIZATION_FAILED:
|
|
86
|
+
case proto_1.types.PaymentStatus.CAPTURE_FAILED:
|
|
87
|
+
case proto_1.types.PaymentStatus.VOID_FAILED:
|
|
88
|
+
case proto_1.types.PaymentStatus.ROUTER_DECLINED:
|
|
89
|
+
case proto_1.types.PaymentStatus.FAILURE:
|
|
90
|
+
return 'failed';
|
|
91
|
+
default:
|
|
92
|
+
return typeof status === 'string' ? stripePaymentStatus(status) : 'unknown';
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
function stripePaymentStatus(status) {
|
|
96
|
+
switch (status) {
|
|
97
|
+
case 'requires_payment_method':
|
|
98
|
+
return 'requires_payment_method';
|
|
99
|
+
case 'requires_action':
|
|
100
|
+
case 'requires_confirmation':
|
|
101
|
+
return 'requires_customer_action';
|
|
102
|
+
case 'processing':
|
|
103
|
+
return 'pending_authorization';
|
|
104
|
+
case 'requires_capture':
|
|
105
|
+
return 'authorized';
|
|
106
|
+
case 'succeeded':
|
|
107
|
+
return 'captured';
|
|
108
|
+
case 'canceled':
|
|
109
|
+
return 'canceled';
|
|
110
|
+
default:
|
|
111
|
+
return 'unknown';
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
function normalizedStripeIntentStatus(intent) {
|
|
115
|
+
const status = stripePaymentStatus(intent.status);
|
|
116
|
+
return status === 'requires_payment_method' && intent.failure?.category === 'customer_action'
|
|
117
|
+
? 'requires_customer_action'
|
|
118
|
+
: status;
|
|
119
|
+
}
|
|
120
|
+
function operationMetadata(input) {
|
|
121
|
+
const metadata = {
|
|
122
|
+
alphaweb_operation_id: input.operationId,
|
|
123
|
+
alphaweb_operation_kind: input.kind,
|
|
124
|
+
alphaweb_contract_version: manifest_1.ALPHAWEB_PRISM_COMPATIBILITY_MANIFEST.release,
|
|
125
|
+
};
|
|
126
|
+
for (const key of OPERATION_METADATA_KEYS) {
|
|
127
|
+
const value = input.metadata[key];
|
|
128
|
+
if (typeof value === 'string' && value.length > 0)
|
|
129
|
+
metadata[key] = value;
|
|
130
|
+
}
|
|
131
|
+
return metadata;
|
|
132
|
+
}
|
|
133
|
+
function connectorPaymentIdFrom(response) {
|
|
134
|
+
const data = recordOf(response);
|
|
135
|
+
return stringOf(data.connectorTransactionId) ?? stringOf(data.connectorPaymentId);
|
|
136
|
+
}
|
|
137
|
+
function connectorPaymentIdFromError(error) {
|
|
138
|
+
if (!(error instanceof errors_1.ConnectorError))
|
|
139
|
+
return null;
|
|
140
|
+
const proto = recordOf(error.proto);
|
|
141
|
+
const errorInfo = recordOf(proto.errorInfo ?? proto.error_info);
|
|
142
|
+
const connectorDetails = recordOf(errorInfo.connectorDetails ?? errorInfo.connector_details);
|
|
143
|
+
return (stringOf(connectorDetails.connectorTransactionId) ??
|
|
144
|
+
stringOf(connectorDetails.connector_transaction_id));
|
|
145
|
+
}
|
|
146
|
+
function reconciliationRequiredError(code, safeMessage, cause, retryable = true) {
|
|
147
|
+
return new error_1.PrismOperationError({
|
|
148
|
+
category: 'conflict',
|
|
149
|
+
providerCode: code,
|
|
150
|
+
retryable,
|
|
151
|
+
outcome: 'ambiguous',
|
|
152
|
+
safeMessage,
|
|
153
|
+
}, cause instanceof Error ? { cause } : undefined);
|
|
154
|
+
}
|
|
155
|
+
function readSecret(value) {
|
|
156
|
+
return stringOf(recordOf(value).value);
|
|
157
|
+
}
|
|
158
|
+
function buildResult(response, money, fallbackConnectorPaymentId = null) {
|
|
159
|
+
const data = recordOf(response);
|
|
160
|
+
return {
|
|
161
|
+
connectorPaymentId: connectorPaymentIdFrom(response) ?? fallbackConnectorPaymentId,
|
|
162
|
+
status: paymentStatus(data.status),
|
|
163
|
+
money,
|
|
164
|
+
};
|
|
165
|
+
}
|
|
166
|
+
/**
|
|
167
|
+
* Package-owned intersection of Stripe's API support contract and the payment
|
|
168
|
+
* method types represented by Prism's recurring-payment ABI.
|
|
169
|
+
*
|
|
170
|
+
* An interactive PaymentIntent can still use any method Stripe exposes through
|
|
171
|
+
* automatic payment methods. This table only governs server-side reuse after
|
|
172
|
+
* Stripe has attached the method to the proven Customer.
|
|
173
|
+
*/
|
|
174
|
+
const STRIPE_REUSABLE_PAYMENT_METHOD_PROFILES = Object.freeze({
|
|
175
|
+
amazon_pay: {
|
|
176
|
+
prismPaymentMethodType: proto_1.types.PaymentMethodType.AMAZON_PAY,
|
|
177
|
+
supportsFutureUsage: true,
|
|
178
|
+
supportsManualCapture: true,
|
|
179
|
+
},
|
|
180
|
+
card: {
|
|
181
|
+
prismPaymentMethodType: proto_1.types.PaymentMethodType.CREDIT,
|
|
182
|
+
supportsFutureUsage: true,
|
|
183
|
+
supportsManualCapture: true,
|
|
184
|
+
},
|
|
185
|
+
cashapp: {
|
|
186
|
+
prismPaymentMethodType: proto_1.types.PaymentMethodType.CASHAPP,
|
|
187
|
+
supportsFutureUsage: true,
|
|
188
|
+
supportsManualCapture: true,
|
|
189
|
+
},
|
|
190
|
+
ideal: {
|
|
191
|
+
prismPaymentMethodType: proto_1.types.PaymentMethodType.IDEAL,
|
|
192
|
+
supportsFutureUsage: true,
|
|
193
|
+
supportsManualCapture: false,
|
|
194
|
+
},
|
|
195
|
+
link: {
|
|
196
|
+
prismPaymentMethodType: proto_1.types.PaymentMethodType.CREDIT,
|
|
197
|
+
supportsFutureUsage: true,
|
|
198
|
+
supportsManualCapture: true,
|
|
199
|
+
},
|
|
200
|
+
paypal: {
|
|
201
|
+
prismPaymentMethodType: proto_1.types.PaymentMethodType.PAY_PAL,
|
|
202
|
+
supportsFutureUsage: true,
|
|
203
|
+
supportsManualCapture: true,
|
|
204
|
+
},
|
|
205
|
+
revolut_pay: {
|
|
206
|
+
prismPaymentMethodType: proto_1.types.PaymentMethodType.REVOLUT_PAY,
|
|
207
|
+
supportsFutureUsage: true,
|
|
208
|
+
supportsManualCapture: true,
|
|
209
|
+
},
|
|
210
|
+
sepa_debit: {
|
|
211
|
+
prismPaymentMethodType: proto_1.types.PaymentMethodType.SEPA,
|
|
212
|
+
supportsFutureUsage: true,
|
|
213
|
+
supportsManualCapture: false,
|
|
214
|
+
},
|
|
215
|
+
twint: {
|
|
216
|
+
prismPaymentMethodType: proto_1.types.PaymentMethodType.TWINT,
|
|
217
|
+
supportsFutureUsage: true,
|
|
218
|
+
supportsManualCapture: false,
|
|
219
|
+
},
|
|
220
|
+
us_bank_account: {
|
|
221
|
+
prismPaymentMethodType: proto_1.types.PaymentMethodType.ACH,
|
|
222
|
+
supportsFutureUsage: true,
|
|
223
|
+
supportsManualCapture: false,
|
|
224
|
+
},
|
|
225
|
+
});
|
|
226
|
+
function reusablePaymentMethodProfile(type) {
|
|
227
|
+
const profile = STRIPE_REUSABLE_PAYMENT_METHOD_PROFILES[type];
|
|
228
|
+
if (!profile?.supportsFutureUsage) {
|
|
229
|
+
throw configurationError('The saved Stripe payment method is not supported for a reusable Prism payment.', 'PRISM_REUSABLE_METHOD_UNSUPPORTED');
|
|
230
|
+
}
|
|
231
|
+
return profile;
|
|
232
|
+
}
|
|
233
|
+
const STRIPE_HARD_DECLINE_CODES = new Set([
|
|
234
|
+
'authentication_required',
|
|
235
|
+
'highest_risk_level',
|
|
236
|
+
'incorrect_number',
|
|
237
|
+
'lost_card',
|
|
238
|
+
'pickup_card',
|
|
239
|
+
'revocation_of_all_authorizations',
|
|
240
|
+
'revocation_of_authorization',
|
|
241
|
+
'stolen_card',
|
|
242
|
+
'transaction_not_allowed',
|
|
243
|
+
]);
|
|
244
|
+
const STRIPE_RETRYABLE_DECLINE_CODES = new Set([
|
|
245
|
+
'approve_with_id',
|
|
246
|
+
'insufficient_funds',
|
|
247
|
+
'issuer_not_available',
|
|
248
|
+
'processing_error',
|
|
249
|
+
'try_again_later',
|
|
250
|
+
]);
|
|
251
|
+
function stripePaymentFailure(value) {
|
|
252
|
+
const failure = recordOf(value);
|
|
253
|
+
const providerCode = stringOf(failure.code);
|
|
254
|
+
const declineCode = stringOf(failure.decline_code);
|
|
255
|
+
const chargeOutcome = recordOf(recordOf(failure.charge).outcome);
|
|
256
|
+
const adviceCode = stringOf(failure.advice_code) ?? stringOf(chargeOutcome.advice_code);
|
|
257
|
+
if (!providerCode && !declineCode && !adviceCode)
|
|
258
|
+
return undefined;
|
|
259
|
+
const category = declineCode === 'authentication_required'
|
|
260
|
+
? 'customer_action'
|
|
261
|
+
: declineCode || providerCode === 'card_declined'
|
|
262
|
+
? 'declined'
|
|
263
|
+
: providerCode?.includes('payment_method') || providerCode?.includes('card')
|
|
264
|
+
? 'invalid_payment_method'
|
|
265
|
+
: 'unknown';
|
|
266
|
+
const retryable = adviceCode === 'try_again_later' ||
|
|
267
|
+
(!adviceCode &&
|
|
268
|
+
!!declineCode &&
|
|
269
|
+
STRIPE_RETRYABLE_DECLINE_CODES.has(declineCode) &&
|
|
270
|
+
!STRIPE_HARD_DECLINE_CODES.has(declineCode));
|
|
271
|
+
return {
|
|
272
|
+
category,
|
|
273
|
+
...(providerCode ? { providerCode } : {}),
|
|
274
|
+
...(declineCode ? { declineCode } : {}),
|
|
275
|
+
...(adviceCode ? { adviceCode } : {}),
|
|
276
|
+
retryable,
|
|
277
|
+
};
|
|
278
|
+
}
|
|
279
|
+
function parseStripePaymentIntent(value) {
|
|
280
|
+
const data = recordOf(value);
|
|
281
|
+
const id = stringOf(data.id);
|
|
282
|
+
const amount = integerOf(data.amount);
|
|
283
|
+
const currency = stringOf(data.currency);
|
|
284
|
+
const status = stringOf(data.status);
|
|
285
|
+
if (!id || amount === null || !currency || !status || typeof data.livemode !== 'boolean') {
|
|
286
|
+
throw validationError('Stripe returned an invalid PaymentIntent shape.', 'PRISM_STRIPE_SHAPE');
|
|
287
|
+
}
|
|
288
|
+
const failure = stripePaymentFailure(data.last_payment_error);
|
|
289
|
+
return {
|
|
290
|
+
id,
|
|
291
|
+
amount,
|
|
292
|
+
currency,
|
|
293
|
+
status,
|
|
294
|
+
customer: stringOf(data.customer),
|
|
295
|
+
payment_method: stringOf(data.payment_method),
|
|
296
|
+
mandate: stringOf(data.mandate),
|
|
297
|
+
setup_future_usage: stringOf(data.setup_future_usage),
|
|
298
|
+
livemode: data.livemode,
|
|
299
|
+
metadata: Object.fromEntries(Object.entries(recordOf(data.metadata)).filter((entry) => typeof entry[1] === 'string')),
|
|
300
|
+
...(failure ? { failure } : {}),
|
|
301
|
+
};
|
|
302
|
+
}
|
|
303
|
+
function parseStripePaymentMethod(value) {
|
|
304
|
+
const data = recordOf(value);
|
|
305
|
+
const id = stringOf(data.id);
|
|
306
|
+
const type = stringOf(data.type);
|
|
307
|
+
if (!id || !type || typeof data.livemode !== 'boolean') {
|
|
308
|
+
throw validationError('Stripe returned an invalid PaymentMethod shape.', 'PRISM_STRIPE_SHAPE');
|
|
309
|
+
}
|
|
310
|
+
const card = recordOf(data.card);
|
|
311
|
+
const expMonth = integerOf(card.exp_month);
|
|
312
|
+
const expYear = integerOf(card.exp_year);
|
|
313
|
+
const brand = stringOf(card.brand);
|
|
314
|
+
const last4 = stringOf(card.last4);
|
|
315
|
+
const safeDisplay = type === 'card'
|
|
316
|
+
? {
|
|
317
|
+
...(brand ? { brand } : {}),
|
|
318
|
+
...(last4 ? { last4 } : {}),
|
|
319
|
+
...(expMonth !== null ? { expMonth } : {}),
|
|
320
|
+
...(expYear !== null ? { expYear } : {}),
|
|
321
|
+
}
|
|
322
|
+
: undefined;
|
|
323
|
+
return {
|
|
324
|
+
id,
|
|
325
|
+
type,
|
|
326
|
+
customer: stringOf(data.customer),
|
|
327
|
+
livemode: data.livemode,
|
|
328
|
+
...(safeDisplay && Object.keys(safeDisplay).length > 0 ? { safeDisplay } : {}),
|
|
329
|
+
};
|
|
330
|
+
}
|
|
331
|
+
function stringMetadata(value) {
|
|
332
|
+
return Object.fromEntries(Object.entries(recordOf(value)).filter((entry) => typeof entry[1] === 'string'));
|
|
333
|
+
}
|
|
334
|
+
function parseStripeRefund(value) {
|
|
335
|
+
const data = recordOf(value);
|
|
336
|
+
const id = stringOf(data.id);
|
|
337
|
+
const paymentIntentId = stringOf(data.payment_intent);
|
|
338
|
+
const amount = integerOf(data.amount);
|
|
339
|
+
const currency = stringOf(data.currency);
|
|
340
|
+
const status = stringOf(data.status);
|
|
341
|
+
if (!id ||
|
|
342
|
+
!paymentIntentId ||
|
|
343
|
+
amount === null ||
|
|
344
|
+
amount < 0 ||
|
|
345
|
+
!currency ||
|
|
346
|
+
!status) {
|
|
347
|
+
throw validationError('Stripe returned an invalid Refund shape.', 'PRISM_STRIPE_SHAPE');
|
|
348
|
+
}
|
|
349
|
+
return {
|
|
350
|
+
id,
|
|
351
|
+
paymentIntentId,
|
|
352
|
+
amount,
|
|
353
|
+
currency: currency.toLowerCase(),
|
|
354
|
+
status,
|
|
355
|
+
metadata: stringMetadata(data.metadata),
|
|
356
|
+
};
|
|
357
|
+
}
|
|
358
|
+
function parseStripeDispute(value) {
|
|
359
|
+
const data = recordOf(value);
|
|
360
|
+
const evidence = recordOf(data.evidence_details);
|
|
361
|
+
const id = stringOf(data.id);
|
|
362
|
+
const chargeId = stringOf(data.charge);
|
|
363
|
+
const paymentIntentId = stringOf(data.payment_intent);
|
|
364
|
+
const amount = integerOf(data.amount);
|
|
365
|
+
const currency = stringOf(data.currency);
|
|
366
|
+
const status = stringOf(data.status);
|
|
367
|
+
const created = integerOf(data.created);
|
|
368
|
+
const evidenceDueBy = integerOf(evidence.due_by);
|
|
369
|
+
if (!id ||
|
|
370
|
+
!chargeId ||
|
|
371
|
+
!paymentIntentId ||
|
|
372
|
+
amount === null ||
|
|
373
|
+
amount < 0 ||
|
|
374
|
+
!currency ||
|
|
375
|
+
!status ||
|
|
376
|
+
created === null ||
|
|
377
|
+
typeof data.livemode !== 'boolean') {
|
|
378
|
+
throw validationError('Stripe returned an invalid Dispute shape.', 'PRISM_STRIPE_SHAPE');
|
|
379
|
+
}
|
|
380
|
+
return {
|
|
381
|
+
id,
|
|
382
|
+
chargeId,
|
|
383
|
+
paymentIntentId,
|
|
384
|
+
amount,
|
|
385
|
+
currency: currency.toLowerCase(),
|
|
386
|
+
status,
|
|
387
|
+
reason: stringOf(data.reason),
|
|
388
|
+
livemode: data.livemode,
|
|
389
|
+
created,
|
|
390
|
+
evidenceDueBy,
|
|
391
|
+
isChargeRefundable: typeof data.is_charge_refundable === 'boolean' ? data.is_charge_refundable : null,
|
|
392
|
+
};
|
|
393
|
+
}
|
|
394
|
+
function normalizedRefundStatus(status) {
|
|
395
|
+
if (status === 'succeeded')
|
|
396
|
+
return 'succeeded';
|
|
397
|
+
if (status === 'failed' || status === 'canceled')
|
|
398
|
+
return 'failed';
|
|
399
|
+
if (status === 'pending' || status === 'requires_action')
|
|
400
|
+
return 'pending';
|
|
401
|
+
return 'unknown';
|
|
402
|
+
}
|
|
403
|
+
function normalizedDisputeStatus(status) {
|
|
404
|
+
if (status === 'warning_needs_response')
|
|
405
|
+
return 'warning_needs_response';
|
|
406
|
+
if (status === 'needs_response')
|
|
407
|
+
return 'needs_response';
|
|
408
|
+
if (status === 'under_review')
|
|
409
|
+
return 'under_review';
|
|
410
|
+
if (status === 'won')
|
|
411
|
+
return 'won';
|
|
412
|
+
if (status === 'lost')
|
|
413
|
+
return 'lost';
|
|
414
|
+
if (status === 'closed')
|
|
415
|
+
return 'closed';
|
|
416
|
+
return 'unknown';
|
|
417
|
+
}
|
|
418
|
+
function assertUniqueBindings(bindings) {
|
|
419
|
+
const bySecret = new Map();
|
|
420
|
+
for (const binding of bindings) {
|
|
421
|
+
const owner = `${binding.credentialBindingId}:${binding.providerAccountId}:${binding.endpointId}`;
|
|
422
|
+
const existing = bySecret.get(binding.secret);
|
|
423
|
+
if (existing && existing !== owner) {
|
|
424
|
+
throw configurationError('A Stripe webhook signing secret is assigned to more than one runtime binding.', 'PRISM_WEBHOOK_BINDING_AMBIGUOUS');
|
|
425
|
+
}
|
|
426
|
+
bySecret.set(binding.secret, owner);
|
|
427
|
+
}
|
|
428
|
+
}
|
|
429
|
+
function objectKind(type) {
|
|
430
|
+
if (type.startsWith('payment_intent.'))
|
|
431
|
+
return 'payment_intent';
|
|
432
|
+
if (type.startsWith('setup_intent.'))
|
|
433
|
+
return 'setup_intent';
|
|
434
|
+
if (type.startsWith('refund.'))
|
|
435
|
+
return 'refund';
|
|
436
|
+
if (type.startsWith('charge.dispute.'))
|
|
437
|
+
return 'dispute';
|
|
438
|
+
if (type.startsWith('mandate.'))
|
|
439
|
+
return 'mandate';
|
|
440
|
+
if (type.startsWith('payment_method.'))
|
|
441
|
+
return 'payment_method';
|
|
442
|
+
return 'unknown';
|
|
443
|
+
}
|
|
444
|
+
function webhookMoney(object) {
|
|
445
|
+
const amount = integerOf(object.amount);
|
|
446
|
+
const currency = stringOf(object.currency);
|
|
447
|
+
return amount !== null && amount >= 0 && currency
|
|
448
|
+
? { amountMinor: amount.toString(), currency: currency.toLowerCase() }
|
|
449
|
+
: null;
|
|
450
|
+
}
|
|
451
|
+
function normalizedWebhookStatus(type, object) {
|
|
452
|
+
const status = stringOf(object.status);
|
|
453
|
+
if (type === 'payment_intent.amount_capturable_updated')
|
|
454
|
+
return 'authorized';
|
|
455
|
+
if (type === 'payment_intent.payment_failed') {
|
|
456
|
+
const failure = stripePaymentFailure(object.last_payment_error);
|
|
457
|
+
if (failure?.category === 'customer_action')
|
|
458
|
+
return 'requires_customer_action';
|
|
459
|
+
return status ? stripePaymentStatus(status) : 'failed';
|
|
460
|
+
}
|
|
461
|
+
if (type === 'setup_intent.setup_failed')
|
|
462
|
+
return 'failed';
|
|
463
|
+
if (type === 'payment_intent.canceled' || type === 'setup_intent.canceled')
|
|
464
|
+
return 'canceled';
|
|
465
|
+
if (type === 'payment_intent.requires_action' || type === 'setup_intent.requires_action') {
|
|
466
|
+
return 'requires_customer_action';
|
|
467
|
+
}
|
|
468
|
+
return status ? stripePaymentStatus(status) : null;
|
|
469
|
+
}
|
|
470
|
+
function safeWebhookData(type, object) {
|
|
471
|
+
const allowedByFamily = {
|
|
472
|
+
payment_intent: ['status', 'capture_method', 'cancellation_reason', 'amount_capturable'],
|
|
473
|
+
setup_intent: ['status', 'usage', 'cancellation_reason'],
|
|
474
|
+
refund: ['status', 'reason', 'failure_reason'],
|
|
475
|
+
dispute: ['status', 'reason', 'is_charge_refundable'],
|
|
476
|
+
mandate: ['status', 'type'],
|
|
477
|
+
payment_method: ['type', 'allow_redisplay'],
|
|
478
|
+
};
|
|
479
|
+
const family = objectKind(type);
|
|
480
|
+
const result = {};
|
|
481
|
+
for (const key of allowedByFamily[family] ?? []) {
|
|
482
|
+
const value = object[key];
|
|
483
|
+
if (value === null || ['string', 'number', 'boolean'].includes(typeof value)) {
|
|
484
|
+
result[key] = value;
|
|
485
|
+
}
|
|
486
|
+
}
|
|
487
|
+
if (family === 'payment_intent') {
|
|
488
|
+
const failure = stripePaymentFailure(object.last_payment_error);
|
|
489
|
+
if (failure) {
|
|
490
|
+
result.payment_failure_category = failure.category;
|
|
491
|
+
result.payment_failure_retryable = failure.retryable;
|
|
492
|
+
if (failure.providerCode)
|
|
493
|
+
result.payment_failure_provider_code = failure.providerCode;
|
|
494
|
+
if (failure.declineCode)
|
|
495
|
+
result.payment_failure_decline_code = failure.declineCode;
|
|
496
|
+
if (failure.adviceCode)
|
|
497
|
+
result.payment_failure_advice_code = failure.adviceCode;
|
|
498
|
+
}
|
|
499
|
+
}
|
|
500
|
+
return result;
|
|
501
|
+
}
|
|
502
|
+
function parseWebhookDocument(input, binding) {
|
|
503
|
+
let decoded;
|
|
504
|
+
try {
|
|
505
|
+
decoded = JSON.parse(Buffer.from(input.rawBody).toString('utf8'));
|
|
506
|
+
}
|
|
507
|
+
catch {
|
|
508
|
+
throw validationError('The verified Stripe webhook body is not valid JSON.', 'PRISM_WEBHOOK_JSON');
|
|
509
|
+
}
|
|
510
|
+
const event = recordOf(decoded);
|
|
511
|
+
const providerEventId = stringOf(event.id);
|
|
512
|
+
const type = stringOf(event.type);
|
|
513
|
+
const created = integerOf(event.created);
|
|
514
|
+
const apiVersion = event.api_version === null ? null : stringOf(event.api_version);
|
|
515
|
+
const livemode = event.livemode;
|
|
516
|
+
const object = recordOf(recordOf(event.data).object);
|
|
517
|
+
const objectId = stringOf(object.id);
|
|
518
|
+
if (!providerEventId ||
|
|
519
|
+
!type ||
|
|
520
|
+
created === null ||
|
|
521
|
+
!objectId ||
|
|
522
|
+
typeof livemode !== 'boolean') {
|
|
523
|
+
throw validationError('The verified Stripe event identity is incomplete.', 'PRISM_WEBHOOK_IDENTITY');
|
|
524
|
+
}
|
|
525
|
+
if (livemode !== binding.livemode) {
|
|
526
|
+
throw validationError('Stripe webhook livemode does not match its verified binding.', 'PRISM_WEBHOOK_MODE');
|
|
527
|
+
}
|
|
528
|
+
const eventAccount = stringOf(event.account);
|
|
529
|
+
if (eventAccount && eventAccount !== binding.providerAccountId) {
|
|
530
|
+
throw validationError('Stripe webhook account does not match its verified binding.', 'PRISM_WEBHOOK_ACCOUNT');
|
|
531
|
+
}
|
|
532
|
+
const kind = objectKind(type);
|
|
533
|
+
const paymentIntentId = kind === 'payment_intent' ? objectId : stringOf(object.payment_intent);
|
|
534
|
+
const bodyDigest = (0, node_crypto_1.createHash)('sha256').update(input.rawBody).digest('hex');
|
|
535
|
+
const envelope = {
|
|
536
|
+
provider: 'stripe',
|
|
537
|
+
providerEventId,
|
|
538
|
+
credentialBindingId: binding.credentialBindingId,
|
|
539
|
+
providerAccountId: binding.providerAccountId,
|
|
540
|
+
livemode: binding.livemode,
|
|
541
|
+
apiVersion,
|
|
542
|
+
type,
|
|
543
|
+
createdAt: new Date(created * 1000).toISOString(),
|
|
544
|
+
objectKind: kind,
|
|
545
|
+
objectId,
|
|
546
|
+
paymentIntentId,
|
|
547
|
+
bodyDigest,
|
|
548
|
+
};
|
|
549
|
+
if (apiVersion !== input.expectedApiVersion) {
|
|
550
|
+
return {
|
|
551
|
+
status: 'normalization_failed',
|
|
552
|
+
envelope,
|
|
553
|
+
error: {
|
|
554
|
+
code: 'WEBHOOK_SCHEMA_VERSION_MISMATCH',
|
|
555
|
+
safeMessage: 'The Stripe event uses an unsupported API schema version.',
|
|
556
|
+
},
|
|
557
|
+
};
|
|
558
|
+
}
|
|
559
|
+
if (!manifest_1.ALPHAWEB_STRIPE_WEBHOOK_EVENTS.includes(type)) {
|
|
560
|
+
return {
|
|
561
|
+
status: 'normalization_failed',
|
|
562
|
+
envelope,
|
|
563
|
+
error: {
|
|
564
|
+
code: 'WEBHOOK_EVENT_UNSUPPORTED',
|
|
565
|
+
safeMessage: 'The verified Stripe event type is not in the compatibility manifest.',
|
|
566
|
+
},
|
|
567
|
+
};
|
|
568
|
+
}
|
|
569
|
+
const metadata = recordOf(object.metadata);
|
|
570
|
+
const money = webhookMoney(object);
|
|
571
|
+
const refundMerchantOperationId = kind === 'refund' && metadata.is_refund_id_as_reference === 'true'
|
|
572
|
+
? stringOf(metadata.order_id)
|
|
573
|
+
: null;
|
|
574
|
+
const normalized = {
|
|
575
|
+
...envelope,
|
|
576
|
+
customerId: stringOf(object.customer),
|
|
577
|
+
paymentMethodId: stringOf(object.payment_method),
|
|
578
|
+
mandateId: stringOf(object.mandate),
|
|
579
|
+
merchantOperationId: refundMerchantOperationId ?? stringOf(metadata.alphaweb_operation_id),
|
|
580
|
+
medusaPaymentSessionId: stringOf(metadata.medusa_payment_session_id),
|
|
581
|
+
status: normalizedWebhookStatus(type, object),
|
|
582
|
+
money,
|
|
583
|
+
refunds: kind === 'refund' && money
|
|
584
|
+
? [
|
|
585
|
+
{
|
|
586
|
+
refundId: objectId,
|
|
587
|
+
chargeId: stringOf(object.charge),
|
|
588
|
+
paymentIntentId,
|
|
589
|
+
status: (() => {
|
|
590
|
+
const status = stringOf(object.status);
|
|
591
|
+
if (status === 'succeeded')
|
|
592
|
+
return 'succeeded';
|
|
593
|
+
if (status === 'failed')
|
|
594
|
+
return 'failed';
|
|
595
|
+
if (status === 'canceled')
|
|
596
|
+
return 'canceled';
|
|
597
|
+
if (status === 'pending' || status === 'requires_action')
|
|
598
|
+
return 'pending';
|
|
599
|
+
return 'unknown';
|
|
600
|
+
})(),
|
|
601
|
+
money,
|
|
602
|
+
},
|
|
603
|
+
]
|
|
604
|
+
: [],
|
|
605
|
+
safeData: safeWebhookData(type, object),
|
|
606
|
+
};
|
|
607
|
+
if (kind === 'refund' && !money) {
|
|
608
|
+
return {
|
|
609
|
+
status: 'normalization_failed',
|
|
610
|
+
envelope,
|
|
611
|
+
error: {
|
|
612
|
+
code: 'WEBHOOK_OBJECT_INVALID',
|
|
613
|
+
safeMessage: 'The Stripe refund event is missing amount or currency.',
|
|
614
|
+
},
|
|
615
|
+
};
|
|
616
|
+
}
|
|
617
|
+
return { status: 'normalized', event: normalized };
|
|
618
|
+
}
|
|
619
|
+
class PrismStripeConnector {
|
|
620
|
+
options;
|
|
621
|
+
dependencies;
|
|
622
|
+
constructor(options, dependencies) {
|
|
623
|
+
this.options = Object.freeze({ ...options });
|
|
624
|
+
this.validateOptions();
|
|
625
|
+
const config = {
|
|
626
|
+
options: {
|
|
627
|
+
environment: options.environment === 'test' ? proto_1.types.Environment.SANDBOX : proto_1.types.Environment.PRODUCTION,
|
|
628
|
+
},
|
|
629
|
+
connectorConfig: { stripe: { apiKey: secret(options.secretKey) } },
|
|
630
|
+
};
|
|
631
|
+
this.dependencies = dependencies ?? {
|
|
632
|
+
merchantAuthentication: new _generated_connector_client_flows_1.MerchantAuthenticationClient(config, {}, options.nativeLibraryPath),
|
|
633
|
+
payments: new _generated_connector_client_flows_1.PaymentClient(config, {}, options.nativeLibraryPath),
|
|
634
|
+
recurringPayments: new _generated_connector_client_flows_1.RecurringPaymentClient(config, {}, options.nativeLibraryPath),
|
|
635
|
+
events: new _generated_connector_client_flows_1.EventClient(config, {}, options.nativeLibraryPath),
|
|
636
|
+
fetch,
|
|
637
|
+
};
|
|
638
|
+
}
|
|
639
|
+
validateOptions() {
|
|
640
|
+
assertIdentifier(this.options.credentialBindingId, 'credentialBindingId');
|
|
641
|
+
assertIdentifier(this.options.providerAccountId, 'providerAccountId');
|
|
642
|
+
const test = this.options.environment === 'test';
|
|
643
|
+
if (!this.options.secretKey.startsWith(test ? 'sk_test_' : 'sk_live_')) {
|
|
644
|
+
throw configurationError('Stripe secret key mode does not match Prism environment.', 'PRISM_KEY_MODE');
|
|
645
|
+
}
|
|
646
|
+
if (this.options.publishableKey &&
|
|
647
|
+
!this.options.publishableKey.startsWith(test ? 'pk_test_' : 'pk_live_')) {
|
|
648
|
+
throw configurationError('Stripe publishable key mode does not match Prism environment.', 'PRISM_PUBLISHABLE_KEY_MODE');
|
|
649
|
+
}
|
|
650
|
+
}
|
|
651
|
+
assertOperation(input) {
|
|
652
|
+
assertIdentifier(input.operationId, 'operationId');
|
|
653
|
+
assertIdentifier(input.idempotencyKey, 'idempotencyKey');
|
|
654
|
+
if (input.credentialBindingId !== this.options.credentialBindingId) {
|
|
655
|
+
throw configurationError('Payment operation credential binding is stale.', 'PRISM_BINDING_MISMATCH');
|
|
656
|
+
}
|
|
657
|
+
}
|
|
658
|
+
async recoverRepeatPayment(connectorPaymentId, input, expectedOwnerId, expectedConnectorCustomerId) {
|
|
659
|
+
try {
|
|
660
|
+
return await this.retrievePayment({
|
|
661
|
+
connectorPaymentId,
|
|
662
|
+
money: input.money,
|
|
663
|
+
operationId: input.operationId,
|
|
664
|
+
expectedOwnerId,
|
|
665
|
+
expectedConnectorCustomerId,
|
|
666
|
+
});
|
|
667
|
+
}
|
|
668
|
+
catch (recoveryError) {
|
|
669
|
+
const normalizedRecovery = (0, error_1.normalizePrismError)(recoveryError);
|
|
670
|
+
const ownershipConflict = normalizedRecovery.shape.providerCode === 'PRISM_PAYMENT_OWNERSHIP';
|
|
671
|
+
throw reconciliationRequiredError(ownershipConflict ? 'PRISM_REPEAT_RESULT_CONFLICT' : 'PRISM_REPEAT_RESULT_UNRESOLVED', ownershipConflict
|
|
672
|
+
? 'The provider payment identity does not match the reusable payment operation.'
|
|
673
|
+
: 'The reusable payment result must be reconciled before retrying.', recoveryError, !ownershipConflict);
|
|
674
|
+
}
|
|
675
|
+
}
|
|
676
|
+
async stripeRequest(method, path, body, idempotencyKey) {
|
|
677
|
+
const controller = new AbortController();
|
|
678
|
+
const timeout = setTimeout(() => controller.abort(), this.options.requestTimeoutMs ?? 15_000);
|
|
679
|
+
try {
|
|
680
|
+
const response = await this.dependencies.fetch(`${STRIPE_API_BASE}${path}`, {
|
|
681
|
+
method,
|
|
682
|
+
headers: {
|
|
683
|
+
Authorization: `Bearer ${this.options.secretKey}`,
|
|
684
|
+
'Stripe-Version': manifest_1.ALPHAWEB_PRISM_COMPATIBILITY_MANIFEST.stripeApiVersion,
|
|
685
|
+
...(body ? { 'Content-Type': 'application/x-www-form-urlencoded' } : {}),
|
|
686
|
+
...(idempotencyKey ? { 'Idempotency-Key': idempotencyKey } : {}),
|
|
687
|
+
},
|
|
688
|
+
body: body?.toString(),
|
|
689
|
+
signal: controller.signal,
|
|
690
|
+
});
|
|
691
|
+
const requestId = response.headers.get('request-id') ?? undefined;
|
|
692
|
+
const text = await response.text();
|
|
693
|
+
let payload = null;
|
|
694
|
+
if (text.length > 0) {
|
|
695
|
+
try {
|
|
696
|
+
payload = JSON.parse(text);
|
|
697
|
+
}
|
|
698
|
+
catch {
|
|
699
|
+
payload = null;
|
|
700
|
+
}
|
|
701
|
+
}
|
|
702
|
+
if (!response.ok) {
|
|
703
|
+
const provider = recordOf(recordOf(payload).error);
|
|
704
|
+
const declinedIntent = recordOf(provider.payment_intent);
|
|
705
|
+
if (response.status === 402 &&
|
|
706
|
+
method === 'POST' &&
|
|
707
|
+
path.endsWith('/confirm') &&
|
|
708
|
+
stringOf(declinedIntent.id)) {
|
|
709
|
+
const lastPaymentError = recordOf(declinedIntent.last_payment_error);
|
|
710
|
+
return {
|
|
711
|
+
...declinedIntent,
|
|
712
|
+
last_payment_error: {
|
|
713
|
+
...lastPaymentError,
|
|
714
|
+
...(stringOf(lastPaymentError.code) || !stringOf(provider.code)
|
|
715
|
+
? {}
|
|
716
|
+
: { code: stringOf(provider.code) }),
|
|
717
|
+
...(stringOf(lastPaymentError.decline_code) || !stringOf(provider.decline_code)
|
|
718
|
+
? {}
|
|
719
|
+
: { decline_code: stringOf(provider.decline_code) }),
|
|
720
|
+
...(stringOf(lastPaymentError.advice_code) || !stringOf(provider.advice_code)
|
|
721
|
+
? {}
|
|
722
|
+
: { advice_code: stringOf(provider.advice_code) }),
|
|
723
|
+
},
|
|
724
|
+
};
|
|
725
|
+
}
|
|
726
|
+
const providerCode = stringOf(provider.code);
|
|
727
|
+
throw new error_1.PrismOperationError({
|
|
728
|
+
category: response.status === 401 || response.status === 403
|
|
729
|
+
? 'authentication'
|
|
730
|
+
: response.status === 429
|
|
731
|
+
? 'rate_limited'
|
|
732
|
+
: response.status >= 500
|
|
733
|
+
? 'provider_server'
|
|
734
|
+
: 'validation',
|
|
735
|
+
...(providerCode ? { providerCode } : {}),
|
|
736
|
+
...(requestId ? { providerRequestId: requestId } : {}),
|
|
737
|
+
retryable: response.status === 429 || response.status >= 500,
|
|
738
|
+
outcome: response.status >= 500 ? 'ambiguous' : 'definitely_not_applied',
|
|
739
|
+
safeMessage: 'Stripe rejected the payment operation.',
|
|
740
|
+
});
|
|
741
|
+
}
|
|
742
|
+
return payload;
|
|
743
|
+
}
|
|
744
|
+
catch (error) {
|
|
745
|
+
throw (0, error_1.normalizePrismError)(error);
|
|
746
|
+
}
|
|
747
|
+
finally {
|
|
748
|
+
clearTimeout(timeout);
|
|
749
|
+
}
|
|
750
|
+
}
|
|
751
|
+
async createPayment(input) {
|
|
752
|
+
this.assertOperation(input);
|
|
753
|
+
if (input.methodSelection.kind !== 'interactive_dynamic' || input.reusableReference) {
|
|
754
|
+
throw validationError('Interactive payment input is invalid.', 'PRISM_INTERACTIVE_INPUT');
|
|
755
|
+
}
|
|
756
|
+
if (!this.options.publishableKey) {
|
|
757
|
+
throw configurationError('Interactive Stripe checkout requires a publishable key.', 'PRISM_PUBLISHABLE_KEY_REQUIRED');
|
|
758
|
+
}
|
|
759
|
+
try {
|
|
760
|
+
const response = await this.dependencies.merchantAuthentication.createClientAuthenticationToken({
|
|
761
|
+
merchantClientSessionId: input.idempotencyKey,
|
|
762
|
+
testMode: this.options.environment === 'test',
|
|
763
|
+
payment: {
|
|
764
|
+
amount: moneyMessage(input.money),
|
|
765
|
+
connectorCustomerId: input.customerReference,
|
|
766
|
+
setupFutureUsage: input.metadata.alphaweb_setup_future_usage === 'off_session'
|
|
767
|
+
? proto_1.types.FutureUsage.OFF_SESSION
|
|
768
|
+
: proto_1.types.FutureUsage.FUTURE_USAGE_UNSPECIFIED,
|
|
769
|
+
captureMethod: input.metadata.alphaweb_capture_method === 'manual'
|
|
770
|
+
? proto_1.types.CaptureMethod.MANUAL
|
|
771
|
+
: proto_1.types.CaptureMethod.AUTOMATIC,
|
|
772
|
+
paymentMethodConfigurationId: input.methodSelection.paymentMethodConfigurationId,
|
|
773
|
+
merchantMetadata: operationMetadata(input),
|
|
774
|
+
},
|
|
775
|
+
});
|
|
776
|
+
const stripe = response.sessionData?.connectorSpecific?.stripe;
|
|
777
|
+
const clientSecret = readSecret(stripe?.clientSecret);
|
|
778
|
+
const connectorPaymentId = stringOf(stripe?.connectorPaymentId);
|
|
779
|
+
if (!clientSecret || !connectorPaymentId) {
|
|
780
|
+
throw validationError('Prism did not return a usable Stripe client session.', 'PRISM_SESSION');
|
|
781
|
+
}
|
|
782
|
+
if (stripe?.livemode !== (this.options.environment === 'live')) {
|
|
783
|
+
throw configurationError('Stripe PaymentIntent mode does not match runtime mode.', 'PRISM_MODE');
|
|
784
|
+
}
|
|
785
|
+
return {
|
|
786
|
+
connectorPaymentId,
|
|
787
|
+
status: 'requires_payment_method',
|
|
788
|
+
money: input.money,
|
|
789
|
+
clientSecret,
|
|
790
|
+
publishableKey: this.options.publishableKey,
|
|
791
|
+
};
|
|
792
|
+
}
|
|
793
|
+
catch (error) {
|
|
794
|
+
throw (0, error_1.normalizePrismError)(error);
|
|
795
|
+
}
|
|
796
|
+
}
|
|
797
|
+
async repeatPayment(input) {
|
|
798
|
+
this.assertOperation(input);
|
|
799
|
+
const expectedOwnerId = input.metadata.alphaweb_owner_id;
|
|
800
|
+
assertIdentifier(expectedOwnerId, 'alphaweb_owner_id');
|
|
801
|
+
const reference = input.reusableReference;
|
|
802
|
+
if (input.methodSelection.kind !== 'reusable_reference' || !reference) {
|
|
803
|
+
throw validationError('Reusable payment input is invalid.', 'PRISM_REUSABLE_INPUT');
|
|
804
|
+
}
|
|
805
|
+
if (reference.connector !== 'stripe' ||
|
|
806
|
+
reference.credentialBindingId !== this.options.credentialBindingId ||
|
|
807
|
+
reference.providerAccountId !== this.options.providerAccountId ||
|
|
808
|
+
reference.livemode !== (this.options.environment === 'live')) {
|
|
809
|
+
throw configurationError('Reusable payment reference ownership does not match runtime.', 'PRISM_REFERENCE_OWNER');
|
|
810
|
+
}
|
|
811
|
+
const paymentMethod = parseStripePaymentMethod(await this.stripeRequest('GET', `/payment_methods/${encodeURIComponent(reference.connectorPaymentMethodId)}`));
|
|
812
|
+
if (paymentMethod.id !== reference.connectorPaymentMethodId ||
|
|
813
|
+
paymentMethod.customer !== reference.connectorCustomerId ||
|
|
814
|
+
paymentMethod.livemode !== reference.livemode) {
|
|
815
|
+
throw configurationError('Saved Stripe payment method ownership validation failed.', 'PRISM_REFERENCE_STALE');
|
|
816
|
+
}
|
|
817
|
+
try {
|
|
818
|
+
const response = await this.dependencies.recurringPayments.charge({
|
|
819
|
+
merchantChargeId: input.idempotencyKey,
|
|
820
|
+
connectorRecurringPaymentId: {
|
|
821
|
+
connectorMandateId: {
|
|
822
|
+
connectorMandateId: reference.connectorMandateId ?? reference.connectorPaymentMethodId,
|
|
823
|
+
paymentMethodId: reference.connectorPaymentMethodId,
|
|
824
|
+
},
|
|
825
|
+
},
|
|
826
|
+
amount: moneyMessage(input.money),
|
|
827
|
+
merchantOrderId: input.operationId,
|
|
828
|
+
captureMethod: input.metadata.alphaweb_capture_method === 'manual'
|
|
829
|
+
? proto_1.types.CaptureMethod.MANUAL
|
|
830
|
+
: proto_1.types.CaptureMethod.AUTOMATIC,
|
|
831
|
+
connectorCustomerId: reference.connectorCustomerId,
|
|
832
|
+
paymentMethodType: reusablePaymentMethodProfile(paymentMethod.type).prismPaymentMethodType,
|
|
833
|
+
offSession: true,
|
|
834
|
+
metadata: secret(JSON.stringify(operationMetadata(input))),
|
|
835
|
+
});
|
|
836
|
+
const result = buildResult(response, input.money);
|
|
837
|
+
if (result.status === 'failed' && result.connectorPaymentId) {
|
|
838
|
+
return this.recoverRepeatPayment(result.connectorPaymentId, input, expectedOwnerId, reference.connectorCustomerId);
|
|
839
|
+
}
|
|
840
|
+
return result;
|
|
841
|
+
}
|
|
842
|
+
catch (error) {
|
|
843
|
+
if (error instanceof errors_1.ConnectorError) {
|
|
844
|
+
const connectorPaymentId = connectorPaymentIdFromError(error);
|
|
845
|
+
if (connectorPaymentId) {
|
|
846
|
+
return this.recoverRepeatPayment(connectorPaymentId, input, expectedOwnerId, reference.connectorCustomerId);
|
|
847
|
+
}
|
|
848
|
+
throw reconciliationRequiredError('PRISM_REPEAT_RESULT_UNRESOLVED', 'The reusable payment result must be reconciled before retrying.', error, false);
|
|
849
|
+
}
|
|
850
|
+
throw (0, error_1.normalizePrismError)(error);
|
|
851
|
+
}
|
|
852
|
+
}
|
|
853
|
+
async retryPayment(input) {
|
|
854
|
+
this.assertOperation(input);
|
|
855
|
+
assertIdentifier(input.connectorPaymentId, 'connectorPaymentId');
|
|
856
|
+
const expectedOwnerId = input.metadata.alphaweb_owner_id;
|
|
857
|
+
assertIdentifier(expectedOwnerId, 'alphaweb_owner_id');
|
|
858
|
+
const reference = input.reusableReference;
|
|
859
|
+
if (input.methodSelection.kind !== 'reusable_reference' || !reference) {
|
|
860
|
+
throw validationError('Reusable payment input is invalid.', 'PRISM_REUSABLE_INPUT');
|
|
861
|
+
}
|
|
862
|
+
if (reference.connector !== 'stripe' ||
|
|
863
|
+
reference.credentialBindingId !== this.options.credentialBindingId ||
|
|
864
|
+
reference.providerAccountId !== this.options.providerAccountId ||
|
|
865
|
+
reference.livemode !== (this.options.environment === 'live')) {
|
|
866
|
+
throw configurationError('Reusable payment reference ownership does not match runtime.', 'PRISM_REFERENCE_OWNER');
|
|
867
|
+
}
|
|
868
|
+
const expectedMoney = (0, money_1.assertSafeProviderMoney)(input.money);
|
|
869
|
+
const intent = parseStripePaymentIntent(await this.stripeRequest('GET', `/payment_intents/${encodeURIComponent(input.connectorPaymentId)}`));
|
|
870
|
+
if (intent.id !== input.connectorPaymentId ||
|
|
871
|
+
intent.status !== 'requires_payment_method' ||
|
|
872
|
+
intent.metadata.alphaweb_operation_id !== input.operationId ||
|
|
873
|
+
intent.metadata.alphaweb_owner_id !== expectedOwnerId ||
|
|
874
|
+
intent.customer !== reference.connectorCustomerId ||
|
|
875
|
+
intent.livemode !== reference.livemode ||
|
|
876
|
+
intent.amount !== expectedMoney.amountMinor ||
|
|
877
|
+
intent.currency.toLowerCase() !== expectedMoney.currency) {
|
|
878
|
+
throw configurationError('Existing Stripe PaymentIntent is not eligible for this retry.', 'PRISM_RETRY_OWNERSHIP');
|
|
879
|
+
}
|
|
880
|
+
const paymentMethod = parseStripePaymentMethod(await this.stripeRequest('GET', `/payment_methods/${encodeURIComponent(reference.connectorPaymentMethodId)}`));
|
|
881
|
+
if (paymentMethod.id !== reference.connectorPaymentMethodId ||
|
|
882
|
+
paymentMethod.customer !== reference.connectorCustomerId ||
|
|
883
|
+
paymentMethod.livemode !== reference.livemode) {
|
|
884
|
+
throw configurationError('Existing Stripe PaymentIntent is not eligible for this retry.', 'PRISM_RETRY_OWNERSHIP');
|
|
885
|
+
}
|
|
886
|
+
reusablePaymentMethodProfile(paymentMethod.type);
|
|
887
|
+
const body = new URLSearchParams({
|
|
888
|
+
payment_method: reference.connectorPaymentMethodId,
|
|
889
|
+
off_session: 'true',
|
|
890
|
+
});
|
|
891
|
+
const retried = parseStripePaymentIntent(await this.stripeRequest('POST', `/payment_intents/${encodeURIComponent(input.connectorPaymentId)}/confirm`, body, input.idempotencyKey));
|
|
892
|
+
if (retried.id !== input.connectorPaymentId ||
|
|
893
|
+
retried.metadata.alphaweb_operation_id !== input.operationId ||
|
|
894
|
+
retried.metadata.alphaweb_owner_id !== expectedOwnerId ||
|
|
895
|
+
retried.customer !== reference.connectorCustomerId ||
|
|
896
|
+
retried.livemode !== reference.livemode ||
|
|
897
|
+
retried.amount !== expectedMoney.amountMinor ||
|
|
898
|
+
retried.currency.toLowerCase() !== expectedMoney.currency) {
|
|
899
|
+
throw configurationError('Stripe retry response does not match the payment operation.', 'PRISM_RETRY_RESULT_CONFLICT');
|
|
900
|
+
}
|
|
901
|
+
return {
|
|
902
|
+
connectorPaymentId: retried.id,
|
|
903
|
+
status: normalizedStripeIntentStatus(retried),
|
|
904
|
+
money: input.money,
|
|
905
|
+
...(retried.failure ? { failure: retried.failure } : {}),
|
|
906
|
+
};
|
|
907
|
+
}
|
|
908
|
+
async retrievePayment(input) {
|
|
909
|
+
assertIdentifier(input.operationId, 'operationId');
|
|
910
|
+
if (input.expectedOwnerId || input.expectedConnectorCustomerId) {
|
|
911
|
+
if (input.expectedOwnerId)
|
|
912
|
+
assertIdentifier(input.expectedOwnerId, 'expectedOwnerId');
|
|
913
|
+
if (input.expectedConnectorCustomerId) {
|
|
914
|
+
assertIdentifier(input.expectedConnectorCustomerId, 'expectedConnectorCustomerId');
|
|
915
|
+
}
|
|
916
|
+
const expectedMoney = (0, money_1.assertSafeProviderMoney)(input.money);
|
|
917
|
+
const intent = parseStripePaymentIntent(await this.stripeRequest('GET', `/payment_intents/${encodeURIComponent(input.connectorPaymentId)}`));
|
|
918
|
+
if (intent.id !== input.connectorPaymentId ||
|
|
919
|
+
intent.metadata.alphaweb_operation_id !== input.operationId ||
|
|
920
|
+
(input.expectedOwnerId &&
|
|
921
|
+
intent.metadata.alphaweb_owner_id !== input.expectedOwnerId) ||
|
|
922
|
+
(input.expectedConnectorCustomerId &&
|
|
923
|
+
intent.customer !== input.expectedConnectorCustomerId) ||
|
|
924
|
+
intent.livemode !== (this.options.environment === 'live') ||
|
|
925
|
+
intent.amount !== expectedMoney.amountMinor ||
|
|
926
|
+
intent.currency !== expectedMoney.currency) {
|
|
927
|
+
throw configurationError('Stripe payment ownership does not match the payment operation.', 'PRISM_PAYMENT_OWNERSHIP');
|
|
928
|
+
}
|
|
929
|
+
return {
|
|
930
|
+
connectorPaymentId: intent.id,
|
|
931
|
+
status: normalizedStripeIntentStatus(intent),
|
|
932
|
+
money: {
|
|
933
|
+
amountMinor: intent.amount.toString(),
|
|
934
|
+
currency: intent.currency,
|
|
935
|
+
},
|
|
936
|
+
...(intent.failure ? { failure: intent.failure } : {}),
|
|
937
|
+
};
|
|
938
|
+
}
|
|
939
|
+
try {
|
|
940
|
+
const response = await this.dependencies.payments.get({
|
|
941
|
+
merchantTransactionId: input.operationId,
|
|
942
|
+
connectorTransactionId: input.connectorPaymentId,
|
|
943
|
+
amount: moneyMessage(input.money),
|
|
944
|
+
merchantRequestId: `${input.operationId}:retrieve:v1`,
|
|
945
|
+
});
|
|
946
|
+
return buildResult(response, input.money, input.connectorPaymentId);
|
|
947
|
+
}
|
|
948
|
+
catch (error) {
|
|
949
|
+
throw (0, error_1.normalizePrismError)(error);
|
|
950
|
+
}
|
|
951
|
+
}
|
|
952
|
+
async establishReusableReference(input) {
|
|
953
|
+
assertIdentifier(input.operationId, 'operationId');
|
|
954
|
+
assertIdentifier(input.expectedOwnerId, 'expectedOwnerId');
|
|
955
|
+
const expectedMoney = (0, money_1.assertSafeProviderMoney)(input.money);
|
|
956
|
+
const intent = parseStripePaymentIntent(await this.stripeRequest('GET', `/payment_intents/${encodeURIComponent(input.connectorPaymentId)}`));
|
|
957
|
+
if (intent.id !== input.connectorPaymentId ||
|
|
958
|
+
intent.metadata.alphaweb_operation_id !== input.operationId ||
|
|
959
|
+
intent.metadata.alphaweb_owner_id !== input.expectedOwnerId ||
|
|
960
|
+
intent.livemode !== (this.options.environment === 'live') ||
|
|
961
|
+
intent.amount !== expectedMoney.amountMinor ||
|
|
962
|
+
intent.currency.toLowerCase() !== expectedMoney.currency ||
|
|
963
|
+
(intent.status !== 'succeeded' && intent.status !== 'requires_capture')) {
|
|
964
|
+
throw configurationError('Stripe reusable-payment ownership proof does not match checkout.', 'PRISM_REFERENCE_OWNERSHIP');
|
|
965
|
+
}
|
|
966
|
+
if (intent.setup_future_usage !== 'off_session' ||
|
|
967
|
+
!intent.customer ||
|
|
968
|
+
!intent.payment_method) {
|
|
969
|
+
throw validationError('Stripe has not established future-use payment ownership.', 'PRISM_REFERENCE_NOT_READY');
|
|
970
|
+
}
|
|
971
|
+
const paymentMethod = parseStripePaymentMethod(await this.stripeRequest('GET', `/payment_methods/${encodeURIComponent(intent.payment_method)}`));
|
|
972
|
+
if (paymentMethod.id !== intent.payment_method ||
|
|
973
|
+
paymentMethod.customer !== intent.customer ||
|
|
974
|
+
paymentMethod.livemode !== intent.livemode) {
|
|
975
|
+
throw configurationError('Stripe reusable payment method ownership does not match checkout.', 'PRISM_REFERENCE_OWNERSHIP');
|
|
976
|
+
}
|
|
977
|
+
const profile = STRIPE_REUSABLE_PAYMENT_METHOD_PROFILES[paymentMethod.type];
|
|
978
|
+
if (!profile?.supportsFutureUsage) {
|
|
979
|
+
return { status: 'not_supported', reason: 'payment_method_not_reusable' };
|
|
980
|
+
}
|
|
981
|
+
const requested = new Set(input.purposes);
|
|
982
|
+
const capabilities = ['off_session'];
|
|
983
|
+
if (requested.has('post_purchase') && profile.supportsManualCapture) {
|
|
984
|
+
capabilities.push('post_purchase', 'manual_capture');
|
|
985
|
+
}
|
|
986
|
+
if (requested.has('subscription'))
|
|
987
|
+
capabilities.push('subscription');
|
|
988
|
+
if ((requested.has('post_purchase') && !capabilities.includes('post_purchase')) ||
|
|
989
|
+
(requested.has('subscription') && !capabilities.includes('subscription'))) {
|
|
990
|
+
if (capabilities.length === 1) {
|
|
991
|
+
return {
|
|
992
|
+
status: 'not_supported',
|
|
993
|
+
reason: 'requested_capability_not_supported',
|
|
994
|
+
};
|
|
995
|
+
}
|
|
996
|
+
}
|
|
997
|
+
return {
|
|
998
|
+
status: 'established',
|
|
999
|
+
reference: {
|
|
1000
|
+
version: 1,
|
|
1001
|
+
connector: 'stripe',
|
|
1002
|
+
credentialBindingId: this.options.credentialBindingId,
|
|
1003
|
+
providerAccountId: this.options.providerAccountId,
|
|
1004
|
+
livemode: this.options.environment === 'live',
|
|
1005
|
+
connectorCustomerId: intent.customer,
|
|
1006
|
+
connectorPaymentMethodId: paymentMethod.id,
|
|
1007
|
+
...(intent.mandate ? { connectorMandateId: intent.mandate } : {}),
|
|
1008
|
+
sourcePaymentIntentId: intent.id,
|
|
1009
|
+
capabilities,
|
|
1010
|
+
},
|
|
1011
|
+
...(paymentMethod.safeDisplay ? { safeDisplay: paymentMethod.safeDisplay } : {}),
|
|
1012
|
+
};
|
|
1013
|
+
}
|
|
1014
|
+
async findPaymentsByMerchantOperation(input) {
|
|
1015
|
+
assertIdentifier(input.operationId, 'operationId');
|
|
1016
|
+
if (input.expectedOwnerId)
|
|
1017
|
+
assertIdentifier(input.expectedOwnerId, 'expectedOwnerId');
|
|
1018
|
+
if (input.expectedConnectorCustomerId) {
|
|
1019
|
+
assertIdentifier(input.expectedConnectorCustomerId, 'expectedConnectorCustomerId');
|
|
1020
|
+
}
|
|
1021
|
+
const expectedMoney = input.money ? (0, money_1.assertSafeProviderMoney)(input.money) : null;
|
|
1022
|
+
const clauses = [`metadata['alphaweb_operation_id']:'${input.operationId}'`];
|
|
1023
|
+
if (input.createdAfter) {
|
|
1024
|
+
const createdAfter = Date.parse(input.createdAfter);
|
|
1025
|
+
if (!Number.isFinite(createdAfter)) {
|
|
1026
|
+
throw validationError('Payment search boundary is invalid.', 'PRISM_SEARCH_BOUNDARY');
|
|
1027
|
+
}
|
|
1028
|
+
clauses.push(`created>=${Math.floor(createdAfter / 1000)}`);
|
|
1029
|
+
}
|
|
1030
|
+
const matches = [];
|
|
1031
|
+
let page = null;
|
|
1032
|
+
for (let pageCount = 0; pageCount < 100; pageCount += 1) {
|
|
1033
|
+
const search = new URLSearchParams({ query: clauses.join(' AND '), limit: '100' });
|
|
1034
|
+
if (page)
|
|
1035
|
+
search.set('page', page);
|
|
1036
|
+
const payload = recordOf(await this.stripeRequest('GET', `/payment_intents/search?${search}`));
|
|
1037
|
+
const data = Array.isArray(payload.data) ? payload.data : [];
|
|
1038
|
+
for (const entry of data) {
|
|
1039
|
+
const intent = parseStripePaymentIntent(entry);
|
|
1040
|
+
if (intent.metadata.alphaweb_operation_id !== input.operationId ||
|
|
1041
|
+
(input.expectedOwnerId &&
|
|
1042
|
+
intent.metadata.alphaweb_owner_id !== input.expectedOwnerId) ||
|
|
1043
|
+
(input.expectedConnectorCustomerId &&
|
|
1044
|
+
intent.customer !== input.expectedConnectorCustomerId) ||
|
|
1045
|
+
intent.livemode !== (this.options.environment === 'live') ||
|
|
1046
|
+
(expectedMoney && intent.amount !== expectedMoney.amountMinor) ||
|
|
1047
|
+
(expectedMoney && intent.currency !== expectedMoney.currency)) {
|
|
1048
|
+
throw validationError('Stripe search returned a payment outside the requested operation ownership.', 'PRISM_SEARCH_OWNERSHIP');
|
|
1049
|
+
}
|
|
1050
|
+
matches.push({
|
|
1051
|
+
connectorPaymentId: intent.id,
|
|
1052
|
+
status: normalizedStripeIntentStatus(intent),
|
|
1053
|
+
money: { amountMinor: intent.amount.toString(), currency: intent.currency },
|
|
1054
|
+
...(intent.failure ? { failure: intent.failure } : {}),
|
|
1055
|
+
});
|
|
1056
|
+
}
|
|
1057
|
+
if (payload.has_more !== true)
|
|
1058
|
+
return matches;
|
|
1059
|
+
const nextPage = stringOf(payload.next_page);
|
|
1060
|
+
if (!nextPage || nextPage === page) {
|
|
1061
|
+
throw validationError('Stripe payment search pagination is invalid.', 'PRISM_SEARCH_PAGE');
|
|
1062
|
+
}
|
|
1063
|
+
page = nextPage;
|
|
1064
|
+
}
|
|
1065
|
+
throw validationError('Stripe payment search exceeded the bounded reconciliation window.', 'PRISM_SEARCH_LIMIT');
|
|
1066
|
+
}
|
|
1067
|
+
async capturePayment(input) {
|
|
1068
|
+
assertIdentifier(input.idempotencyKey, 'idempotencyKey');
|
|
1069
|
+
try {
|
|
1070
|
+
const response = await this.dependencies.payments.capture({
|
|
1071
|
+
merchantCaptureId: input.operationId,
|
|
1072
|
+
connectorTransactionId: input.connectorPaymentId,
|
|
1073
|
+
amountToCapture: moneyMessage(input.money),
|
|
1074
|
+
captureMethod: proto_1.types.CaptureMethod.MANUAL,
|
|
1075
|
+
merchantRequestId: input.idempotencyKey,
|
|
1076
|
+
});
|
|
1077
|
+
return buildResult(response, input.money, input.connectorPaymentId);
|
|
1078
|
+
}
|
|
1079
|
+
catch (error) {
|
|
1080
|
+
throw (0, error_1.normalizePrismError)(error);
|
|
1081
|
+
}
|
|
1082
|
+
}
|
|
1083
|
+
async cancelPayment(input) {
|
|
1084
|
+
assertIdentifier(input.idempotencyKey, 'idempotencyKey');
|
|
1085
|
+
try {
|
|
1086
|
+
const response = await this.dependencies.payments.void({
|
|
1087
|
+
merchantVoidId: input.operationId,
|
|
1088
|
+
connectorTransactionId: input.connectorPaymentId,
|
|
1089
|
+
merchantRequestId: input.idempotencyKey,
|
|
1090
|
+
});
|
|
1091
|
+
return buildResult(response, { amountMinor: '0', currency: 'unknown' }, input.connectorPaymentId);
|
|
1092
|
+
}
|
|
1093
|
+
catch (error) {
|
|
1094
|
+
throw (0, error_1.normalizePrismError)(error);
|
|
1095
|
+
}
|
|
1096
|
+
}
|
|
1097
|
+
async refundPayment(input) {
|
|
1098
|
+
assertIdentifier(input.idempotencyKey, 'idempotencyKey');
|
|
1099
|
+
try {
|
|
1100
|
+
const response = await this.dependencies.payments.refund({
|
|
1101
|
+
merchantRefundId: input.refundId,
|
|
1102
|
+
connectorTransactionId: input.connectorPaymentId,
|
|
1103
|
+
refundAmount: moneyMessage(input.money),
|
|
1104
|
+
merchantRequestId: input.idempotencyKey,
|
|
1105
|
+
});
|
|
1106
|
+
return {
|
|
1107
|
+
connectorRefundId: stringOf(response.connectorRefundId),
|
|
1108
|
+
status: response.status === proto_1.types.RefundStatus.REFUND_SUCCESS
|
|
1109
|
+
? 'succeeded'
|
|
1110
|
+
: response.status === proto_1.types.RefundStatus.REFUND_PENDING
|
|
1111
|
+
? 'pending'
|
|
1112
|
+
: response.status === proto_1.types.RefundStatus.REFUND_FAILURE ||
|
|
1113
|
+
response.status === proto_1.types.RefundStatus.REFUND_TRANSACTION_FAILURE
|
|
1114
|
+
? 'failed'
|
|
1115
|
+
: 'unknown',
|
|
1116
|
+
money: input.money,
|
|
1117
|
+
};
|
|
1118
|
+
}
|
|
1119
|
+
catch (error) {
|
|
1120
|
+
throw (0, error_1.normalizePrismError)(error);
|
|
1121
|
+
}
|
|
1122
|
+
}
|
|
1123
|
+
async retrieveRefund(input) {
|
|
1124
|
+
if (input.operationId)
|
|
1125
|
+
assertIdentifier(input.operationId, 'operationId');
|
|
1126
|
+
const expected = input.money ? (0, money_1.assertSafeProviderMoney)(input.money) : null;
|
|
1127
|
+
const refund = parseStripeRefund(await this.stripeRequest('GET', `/refunds/${encodeURIComponent(input.connectorRefundId)}`));
|
|
1128
|
+
if (refund.id !== input.connectorRefundId ||
|
|
1129
|
+
refund.paymentIntentId !== input.connectorPaymentId ||
|
|
1130
|
+
(expected && refund.amount !== expected.amountMinor) ||
|
|
1131
|
+
(expected && refund.currency !== expected.currency) ||
|
|
1132
|
+
(refund.metadata.alphaweb_operation_id &&
|
|
1133
|
+
input.operationId &&
|
|
1134
|
+
refund.metadata.alphaweb_operation_id !== input.operationId)) {
|
|
1135
|
+
throw configurationError('Stripe refund ownership does not match the payment operation.', 'PRISM_REFUND_OWNERSHIP');
|
|
1136
|
+
}
|
|
1137
|
+
return {
|
|
1138
|
+
connectorRefundId: refund.id,
|
|
1139
|
+
connectorPaymentId: refund.paymentIntentId,
|
|
1140
|
+
status: normalizedRefundStatus(refund.status),
|
|
1141
|
+
money: {
|
|
1142
|
+
amountMinor: refund.amount.toString(),
|
|
1143
|
+
currency: refund.currency,
|
|
1144
|
+
},
|
|
1145
|
+
};
|
|
1146
|
+
}
|
|
1147
|
+
async findRefundsByMerchantOperation(input) {
|
|
1148
|
+
assertIdentifier(input.operationId, 'operationId');
|
|
1149
|
+
const expected = (0, money_1.assertSafeProviderMoney)(input.money);
|
|
1150
|
+
const matches = [];
|
|
1151
|
+
let startingAfter = null;
|
|
1152
|
+
for (let pageCount = 0; pageCount < 100; pageCount += 1) {
|
|
1153
|
+
const query = new URLSearchParams({
|
|
1154
|
+
payment_intent: input.connectorPaymentId,
|
|
1155
|
+
limit: '100',
|
|
1156
|
+
});
|
|
1157
|
+
if (startingAfter)
|
|
1158
|
+
query.set('starting_after', startingAfter);
|
|
1159
|
+
const payload = recordOf(await this.stripeRequest('GET', `/refunds?${query.toString()}`));
|
|
1160
|
+
const data = Array.isArray(payload.data) ? payload.data : [];
|
|
1161
|
+
for (const entry of data) {
|
|
1162
|
+
const refund = parseStripeRefund(entry);
|
|
1163
|
+
if (refund.paymentIntentId !== input.connectorPaymentId) {
|
|
1164
|
+
throw validationError('Stripe refund list returned an object outside the requested payment ownership.', 'PRISM_REFUND_SEARCH_OWNERSHIP');
|
|
1165
|
+
}
|
|
1166
|
+
const ownsOperation = refund.metadata.order_id === input.operationId &&
|
|
1167
|
+
refund.metadata.is_refund_id_as_reference === 'true';
|
|
1168
|
+
if (!ownsOperation)
|
|
1169
|
+
continue;
|
|
1170
|
+
if (refund.amount !== expected.amountMinor || refund.currency !== expected.currency) {
|
|
1171
|
+
throw validationError('Stripe refund search returned conflicting operation money.', 'PRISM_REFUND_SEARCH_OWNERSHIP');
|
|
1172
|
+
}
|
|
1173
|
+
matches.push({
|
|
1174
|
+
connectorRefundId: refund.id,
|
|
1175
|
+
connectorPaymentId: refund.paymentIntentId,
|
|
1176
|
+
status: normalizedRefundStatus(refund.status),
|
|
1177
|
+
money: {
|
|
1178
|
+
amountMinor: refund.amount.toString(),
|
|
1179
|
+
currency: refund.currency,
|
|
1180
|
+
},
|
|
1181
|
+
});
|
|
1182
|
+
}
|
|
1183
|
+
if (payload.has_more !== true)
|
|
1184
|
+
return matches;
|
|
1185
|
+
const last = data.length > 0 ? parseStripeRefund(data[data.length - 1]) : null;
|
|
1186
|
+
if (!last || last.id === startingAfter) {
|
|
1187
|
+
throw validationError('Stripe refund pagination is invalid.', 'PRISM_REFUND_SEARCH_PAGE');
|
|
1188
|
+
}
|
|
1189
|
+
startingAfter = last.id;
|
|
1190
|
+
}
|
|
1191
|
+
throw validationError('Stripe refund search exceeded the bounded reconciliation window.', 'PRISM_REFUND_SEARCH_LIMIT');
|
|
1192
|
+
}
|
|
1193
|
+
async retrieveDispute(input) {
|
|
1194
|
+
const dispute = parseStripeDispute(await this.stripeRequest('GET', `/disputes/${encodeURIComponent(input.connectorDisputeId)}`));
|
|
1195
|
+
if (dispute.id !== input.connectorDisputeId ||
|
|
1196
|
+
dispute.livemode !== (this.options.environment === 'live')) {
|
|
1197
|
+
throw configurationError('Stripe dispute ownership does not match the credential binding.', 'PRISM_DISPUTE_OWNERSHIP');
|
|
1198
|
+
}
|
|
1199
|
+
return {
|
|
1200
|
+
connectorDisputeId: dispute.id,
|
|
1201
|
+
connectorChargeId: dispute.chargeId,
|
|
1202
|
+
connectorPaymentId: dispute.paymentIntentId,
|
|
1203
|
+
status: normalizedDisputeStatus(dispute.status),
|
|
1204
|
+
money: {
|
|
1205
|
+
amountMinor: dispute.amount.toString(),
|
|
1206
|
+
currency: dispute.currency,
|
|
1207
|
+
},
|
|
1208
|
+
...(dispute.reason ? { reason: dispute.reason } : {}),
|
|
1209
|
+
...(dispute.evidenceDueBy !== null
|
|
1210
|
+
? { evidenceDueAt: new Date(dispute.evidenceDueBy * 1000).toISOString() }
|
|
1211
|
+
: {}),
|
|
1212
|
+
providerCreatedAt: new Date(dispute.created * 1000).toISOString(),
|
|
1213
|
+
safeData: {
|
|
1214
|
+
...(dispute.isChargeRefundable !== null
|
|
1215
|
+
? { is_charge_refundable: dispute.isChargeRefundable }
|
|
1216
|
+
: {}),
|
|
1217
|
+
},
|
|
1218
|
+
};
|
|
1219
|
+
}
|
|
1220
|
+
async parseWebhookEvent(input) {
|
|
1221
|
+
if (input.rawBody.length === 0 || input.signingSecretBindings.length === 0) {
|
|
1222
|
+
throw validationError('Stripe webhook verification input is incomplete.', 'PRISM_WEBHOOK_INPUT');
|
|
1223
|
+
}
|
|
1224
|
+
assertUniqueBindings(input.signingSecretBindings);
|
|
1225
|
+
const matches = [];
|
|
1226
|
+
let nativeProcessingFailures = 0;
|
|
1227
|
+
for (const binding of input.signingSecretBindings) {
|
|
1228
|
+
try {
|
|
1229
|
+
const response = await this.dependencies.events.handleEvent({
|
|
1230
|
+
merchantEventId: 'alphaweb-webhook-verification',
|
|
1231
|
+
requestDetails: {
|
|
1232
|
+
method: proto_1.types.HttpMethod.HTTP_METHOD_POST,
|
|
1233
|
+
uri: '/webhooks/prism/stripe',
|
|
1234
|
+
headers: { 'stripe-signature': input.signatureHeader },
|
|
1235
|
+
body: input.rawBody,
|
|
1236
|
+
},
|
|
1237
|
+
webhookSecrets: { secret: binding.secret },
|
|
1238
|
+
});
|
|
1239
|
+
if (response.sourceVerified)
|
|
1240
|
+
matches.push(binding);
|
|
1241
|
+
}
|
|
1242
|
+
catch {
|
|
1243
|
+
nativeProcessingFailures += 1;
|
|
1244
|
+
}
|
|
1245
|
+
}
|
|
1246
|
+
if (matches.length === 0 && nativeProcessingFailures > 0) {
|
|
1247
|
+
throw new error_1.PrismOperationError({
|
|
1248
|
+
category: 'configuration',
|
|
1249
|
+
providerCode: 'PRISM_WEBHOOK_NATIVE_PROCESSING',
|
|
1250
|
+
retryable: true,
|
|
1251
|
+
outcome: 'definitely_not_applied',
|
|
1252
|
+
safeMessage: 'The pinned native Stripe webhook parser could not process this event.',
|
|
1253
|
+
});
|
|
1254
|
+
}
|
|
1255
|
+
if (matches.length !== 1) {
|
|
1256
|
+
throw validationError(matches.length === 0
|
|
1257
|
+
? 'Stripe webhook signature verification failed.'
|
|
1258
|
+
: 'Stripe webhook signature matched more than one runtime binding.', matches.length === 0 ? 'PRISM_WEBHOOK_SIGNATURE' : 'PRISM_WEBHOOK_BINDING_AMBIGUOUS');
|
|
1259
|
+
}
|
|
1260
|
+
return parseWebhookDocument(input, matches[0]);
|
|
1261
|
+
}
|
|
1262
|
+
async createWebhookEndpoint(input) {
|
|
1263
|
+
assertIdentifier(input.idempotencyKey, 'idempotencyKey');
|
|
1264
|
+
const body = new URLSearchParams({
|
|
1265
|
+
url: input.callbackUrl,
|
|
1266
|
+
api_version: manifest_1.ALPHAWEB_PRISM_COMPATIBILITY_MANIFEST.stripeApiVersion,
|
|
1267
|
+
});
|
|
1268
|
+
for (const event of manifest_1.ALPHAWEB_STRIPE_WEBHOOK_EVENTS)
|
|
1269
|
+
body.append('enabled_events[]', event);
|
|
1270
|
+
for (const [key, value] of Object.entries(input.metadata)) {
|
|
1271
|
+
if (/^[A-Za-z0-9_]{1,40}$/.test(key) && value.length <= 500) {
|
|
1272
|
+
body.append(`metadata[${key}]`, value);
|
|
1273
|
+
}
|
|
1274
|
+
}
|
|
1275
|
+
const payload = recordOf(await this.stripeRequest('POST', '/webhook_endpoints', body, input.idempotencyKey));
|
|
1276
|
+
const endpoint = this.normalizeEndpoint(payload);
|
|
1277
|
+
const signingSecret = stringOf(payload.secret);
|
|
1278
|
+
if (!signingSecret) {
|
|
1279
|
+
throw validationError('Stripe did not return a webhook signing secret.', 'PRISM_WEBHOOK_SECRET');
|
|
1280
|
+
}
|
|
1281
|
+
return { ...endpoint, signingSecret };
|
|
1282
|
+
}
|
|
1283
|
+
async updateWebhookEndpoint(input) {
|
|
1284
|
+
assertIdentifier(input.idempotencyKey, 'idempotencyKey');
|
|
1285
|
+
const body = new URLSearchParams();
|
|
1286
|
+
if (input.callbackUrl)
|
|
1287
|
+
body.set('url', input.callbackUrl);
|
|
1288
|
+
if (input.enabled !== undefined)
|
|
1289
|
+
body.set('disabled', input.enabled ? 'false' : 'true');
|
|
1290
|
+
for (const event of manifest_1.ALPHAWEB_STRIPE_WEBHOOK_EVENTS)
|
|
1291
|
+
body.append('enabled_events[]', event);
|
|
1292
|
+
return this.normalizeEndpoint(await this.stripeRequest('POST', `/webhook_endpoints/${encodeURIComponent(input.endpointId)}`, body, input.idempotencyKey));
|
|
1293
|
+
}
|
|
1294
|
+
async deleteWebhookEndpoint(input) {
|
|
1295
|
+
assertIdentifier(input.idempotencyKey, 'idempotencyKey');
|
|
1296
|
+
await this.stripeRequest('DELETE', `/webhook_endpoints/${encodeURIComponent(input.endpointId)}`, undefined, input.idempotencyKey);
|
|
1297
|
+
}
|
|
1298
|
+
async retrieveWebhookEndpoint(input) {
|
|
1299
|
+
return this.normalizeEndpoint(await this.stripeRequest('GET', `/webhook_endpoints/${encodeURIComponent(input.endpointId)}`));
|
|
1300
|
+
}
|
|
1301
|
+
normalizeEndpoint(value) {
|
|
1302
|
+
const data = recordOf(value);
|
|
1303
|
+
const endpointId = stringOf(data.id);
|
|
1304
|
+
const url = stringOf(data.url);
|
|
1305
|
+
const apiVersion = stringOf(data.api_version);
|
|
1306
|
+
if (!endpointId || !url || !apiVersion || !Array.isArray(data.enabled_events)) {
|
|
1307
|
+
throw validationError('Stripe returned an invalid webhook endpoint shape.', 'PRISM_WEBHOOK_ENDPOINT');
|
|
1308
|
+
}
|
|
1309
|
+
return {
|
|
1310
|
+
endpointId,
|
|
1311
|
+
url,
|
|
1312
|
+
enabled: data.status === 'enabled',
|
|
1313
|
+
apiVersion,
|
|
1314
|
+
events: data.enabled_events.filter((event) => typeof event === 'string'),
|
|
1315
|
+
metadata: Object.fromEntries(Object.entries(recordOf(data.metadata)).filter((entry) => typeof entry[1] === 'string')),
|
|
1316
|
+
};
|
|
1317
|
+
}
|
|
1318
|
+
}
|
|
1319
|
+
exports.PrismStripeConnector = PrismStripeConnector;
|
|
1320
|
+
/**
|
|
1321
|
+
* Verifies that one Stripe key pair resolves to the expected merchant account
|
|
1322
|
+
* and that the pinned native Prism runtime can be constructed for it. Consumer
|
|
1323
|
+
* applications receive only the verified account identity and mode.
|
|
1324
|
+
*/
|
|
1325
|
+
async function verifyPrismStripeRuntimeCredential(input, dependencies) {
|
|
1326
|
+
const test = input.environment === 'test';
|
|
1327
|
+
if (!input.secretKey.startsWith(test ? 'sk_test_' : 'sk_live_')) {
|
|
1328
|
+
throw configurationError('Stripe secret key mode does not match Prism environment.', 'PRISM_KEY_MODE');
|
|
1329
|
+
}
|
|
1330
|
+
if (input.publishableKey &&
|
|
1331
|
+
!input.publishableKey.startsWith(test ? 'pk_test_' : 'pk_live_')) {
|
|
1332
|
+
throw configurationError('Stripe publishable key mode does not match Prism environment.', 'PRISM_PUBLISHABLE_KEY_MODE');
|
|
1333
|
+
}
|
|
1334
|
+
const controller = new AbortController();
|
|
1335
|
+
const timeout = setTimeout(() => controller.abort(), input.requestTimeoutMs ?? 15_000);
|
|
1336
|
+
try {
|
|
1337
|
+
const request = dependencies?.fetch ?? fetch;
|
|
1338
|
+
const response = await request(`${STRIPE_API_BASE}/account`, {
|
|
1339
|
+
method: 'GET',
|
|
1340
|
+
headers: {
|
|
1341
|
+
Authorization: `Bearer ${input.secretKey}`,
|
|
1342
|
+
'Stripe-Version': manifest_1.ALPHAWEB_PRISM_COMPATIBILITY_MANIFEST.stripeApiVersion,
|
|
1343
|
+
},
|
|
1344
|
+
signal: controller.signal,
|
|
1345
|
+
});
|
|
1346
|
+
const requestId = response.headers.get('request-id') ?? undefined;
|
|
1347
|
+
const text = await response.text();
|
|
1348
|
+
let payload = null;
|
|
1349
|
+
if (text.length > 0) {
|
|
1350
|
+
try {
|
|
1351
|
+
payload = JSON.parse(text);
|
|
1352
|
+
}
|
|
1353
|
+
catch {
|
|
1354
|
+
payload = null;
|
|
1355
|
+
}
|
|
1356
|
+
}
|
|
1357
|
+
if (!response.ok) {
|
|
1358
|
+
throw new error_1.PrismOperationError({
|
|
1359
|
+
category: response.status === 401 || response.status === 403
|
|
1360
|
+
? 'authentication'
|
|
1361
|
+
: response.status === 429
|
|
1362
|
+
? 'rate_limited'
|
|
1363
|
+
: response.status >= 500
|
|
1364
|
+
? 'provider_server'
|
|
1365
|
+
: 'validation',
|
|
1366
|
+
...(requestId ? { providerRequestId: requestId } : {}),
|
|
1367
|
+
retryable: response.status === 429 || response.status >= 500,
|
|
1368
|
+
outcome: response.status >= 500 ? 'ambiguous' : 'definitely_not_applied',
|
|
1369
|
+
safeMessage: 'Stripe runtime verification failed.',
|
|
1370
|
+
});
|
|
1371
|
+
}
|
|
1372
|
+
const providerAccountId = stringOf(recordOf(payload).id);
|
|
1373
|
+
if (!providerAccountId?.startsWith('acct_')) {
|
|
1374
|
+
throw validationError('Stripe returned an invalid account identity.', 'PRISM_ACCOUNT_IDENTITY');
|
|
1375
|
+
}
|
|
1376
|
+
if (input.expectedProviderAccountId &&
|
|
1377
|
+
input.expectedProviderAccountId !== providerAccountId) {
|
|
1378
|
+
throw configurationError('Stripe account does not match the runtime credential binding.', 'PRISM_ACCOUNT_MISMATCH');
|
|
1379
|
+
}
|
|
1380
|
+
new PrismStripeConnector({
|
|
1381
|
+
environment: input.environment,
|
|
1382
|
+
credentialBindingId: 'binding_runtime_verification',
|
|
1383
|
+
providerAccountId,
|
|
1384
|
+
secretKey: input.secretKey,
|
|
1385
|
+
...(input.publishableKey ? { publishableKey: input.publishableKey } : {}),
|
|
1386
|
+
...(input.nativeLibraryPath
|
|
1387
|
+
? { nativeLibraryPath: input.nativeLibraryPath }
|
|
1388
|
+
: {}),
|
|
1389
|
+
...(input.requestTimeoutMs
|
|
1390
|
+
? { requestTimeoutMs: input.requestTimeoutMs }
|
|
1391
|
+
: {}),
|
|
1392
|
+
}, dependencies);
|
|
1393
|
+
return { providerAccountId, livemode: !test };
|
|
1394
|
+
}
|
|
1395
|
+
catch (error) {
|
|
1396
|
+
throw (0, error_1.normalizePrismError)(error);
|
|
1397
|
+
}
|
|
1398
|
+
finally {
|
|
1399
|
+
clearTimeout(timeout);
|
|
1400
|
+
}
|
|
1401
|
+
}
|
|
1402
|
+
//# sourceMappingURL=stripe-connector.js.map
|