@envarai/envarpay 0.2.0-alpha.4 → 0.2.0-alpha.5
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/dist/commerce/buyer.d.ts +7 -1
- package/dist/commerce/buyer.js +22 -2
- package/dist/commerce/cli.js +1 -0
- package/dist/commerce/client-runtime.d.ts +1 -1
- package/dist/commerce/management.js +3 -1
- package/dist/commerce/mpp-client.d.ts +11 -0
- package/dist/commerce/mpp-client.js +5 -0
- package/dist/commerce/stripe-issuer.d.ts +2 -0
- package/dist/commerce/stripe-issuer.js +25 -4
- package/package.json +1 -1
package/dist/commerce/buyer.d.ts
CHANGED
|
@@ -3,7 +3,7 @@ import type { PaymentPayload, PaymentRequirements, SettleResponse } from '@x402/
|
|
|
3
3
|
import { type BuyerPolicy, type Input, type StandardA2APeer } from './types.js';
|
|
4
4
|
import { CredentialVault } from './vault.js';
|
|
5
5
|
import { BuyerStore, type BuyerQuote } from './buyer-store.js';
|
|
6
|
-
import { type MppBuyerOptions, type MppReceipt } from './mpp-client.js';
|
|
6
|
+
import { type MppAuthenticationAction, type MppBuyerOptions, type MppReceipt } from './mpp-client.js';
|
|
7
7
|
export interface BuyerPreviewInput {
|
|
8
8
|
cardUrl: string;
|
|
9
9
|
messageId: string;
|
|
@@ -15,6 +15,10 @@ export interface BuyerConfirmInput {
|
|
|
15
15
|
quoteToken: string;
|
|
16
16
|
messageId: string;
|
|
17
17
|
}
|
|
18
|
+
export interface BuyerAuthentication {
|
|
19
|
+
purchaseId: string;
|
|
20
|
+
action: MppAuthenticationAction | null;
|
|
21
|
+
}
|
|
18
22
|
export interface CommerceBuyerOptions {
|
|
19
23
|
policy: BuyerPolicy;
|
|
20
24
|
store: BuyerStore;
|
|
@@ -114,6 +118,8 @@ export declare class CommerceBuyer {
|
|
|
114
118
|
private verifyMpp;
|
|
115
119
|
private replayMpp;
|
|
116
120
|
private recoverMpp;
|
|
121
|
+
/** Owner-only provider read. No replay, token creation, reservation or payment transition. */
|
|
122
|
+
authentication(caller: string, id: string): Promise<BuyerAuthentication>;
|
|
117
123
|
private assertCurrentPolicy;
|
|
118
124
|
confirm(caller: string, input: BuyerConfirmInput): Promise<BuyerSnapshot>;
|
|
119
125
|
private exclusive;
|
package/dist/commerce/buyer.js
CHANGED
|
@@ -10,7 +10,7 @@ import addFormatsImport from 'ajv-formats';
|
|
|
10
10
|
import { atomic, digest, loadBuyerPolicy, validateUrl } from './config.js';
|
|
11
11
|
import { CommerceError } from './types.js';
|
|
12
12
|
import { Challenge, Receipt, PaymentRequest } from 'mppx';
|
|
13
|
-
import { assertMppDigest, prepareMppCredential, publicMppReceipt, validateMppCredential } from './mpp-client.js';
|
|
13
|
+
import { assertMppDigest, isStripeTestPaymentMethod, prepareMppCredential, publicMppReceipt, validateMppCredential } from './mpp-client.js';
|
|
14
14
|
const COMMERCE = 'urn:envarpay:commerce:1', QUOTE = 'urn:envarpay:quote:1';
|
|
15
15
|
const addFormats = addFormatsImport;
|
|
16
16
|
async function bounded(response, limit = 4 * 1024 * 1024) {
|
|
@@ -108,7 +108,7 @@ export class CommerceBuyer {
|
|
|
108
108
|
this.fetchImpl = options.fetchImpl ?? fetch;
|
|
109
109
|
if (this.policy.peers.some(p => p.protocol === 'x402') && (!options.signer || !/^0x[0-9a-fA-F]{40}$/.test(options.signer.address) || BigInt(options.signer.address) === 0n || !options.verifyReceipt))
|
|
110
110
|
throw new CommerceError('invalid_signer', 'x402 peers require an EVM wallet and independent receipt verifier');
|
|
111
|
-
if (this.policy.peers.some(p => p.protocol === 'mpp') && (!options.mpp || !options.mpp.payer || !(/^pm_[A-Za-z0-9]+$/.test(options.mpp.paymentMethod) || (options.mpp.mode === 'test' && options.mpp.
|
|
111
|
+
if (this.policy.peers.some(p => p.protocol === 'mpp') && (!options.mpp || !options.mpp.payer || !(/^pm_[A-Za-z0-9]+$/.test(options.mpp.paymentMethod) || (options.mpp.mode === 'test' && options.mpp.issuance === 'test-helper' && isStripeTestPaymentMethod(options.mpp.paymentMethod))) || !['test', 'live'].includes(options.mpp.mode) || typeof options.mpp.createToken !== 'function' || typeof options.mpp.verifyReceipt !== 'function'))
|
|
112
112
|
throw new CommerceError('mpp_provider_missing', 'MPP peers require explicit token creation and independent provider verification');
|
|
113
113
|
for (const peer of this.policy.peers)
|
|
114
114
|
if (!(peer.mode === 'standard-a2a' && peer.authentication === 'none') && (!options.peerTokens[peer.id] || options.peerTokens[peer.id].length < 16))
|
|
@@ -485,6 +485,26 @@ export class CommerceBuyer {
|
|
|
485
485
|
return this.snapshot(this.options.store.update(record.id, { state: 'unknown', errorCode: error instanceof CommerceError ? error.code : 'mpp_reconciliation_pending' }));
|
|
486
486
|
}
|
|
487
487
|
}
|
|
488
|
+
/** Owner-only provider read. No replay, token creation, reservation or payment transition. */
|
|
489
|
+
async authentication(caller, id) {
|
|
490
|
+
const record = this.record(caller, id);
|
|
491
|
+
if (record.protocol !== 'mpp')
|
|
492
|
+
throw new CommerceError('mpp_authentication_unavailable', 'Original purchase does not use MPP');
|
|
493
|
+
this.assertCurrentPolicy(record, false);
|
|
494
|
+
if (!record.tokenOperationId || record.paymentState === 'confirmed')
|
|
495
|
+
return { purchaseId: id, action: null };
|
|
496
|
+
const provider = this.options.mpp;
|
|
497
|
+
if (!provider?.getAuthentication)
|
|
498
|
+
throw new CommerceError('mpp_authentication_unavailable', 'Configured provider does not support customer authentication');
|
|
499
|
+
const action = await provider.getAuthentication({ purchaseId: record.id, quote: structuredClone(record.quote), mode: record.mppMode, operationId: record.tokenOperationId });
|
|
500
|
+
if (!action)
|
|
501
|
+
return { purchaseId: id, action: null };
|
|
502
|
+
if (action.type !== 'use_stripe_sdk' || typeof action.hashedValue !== 'string' || !action.hashedValue || action.hashedValue.length > 16384 ||
|
|
503
|
+
typeof action.publishableKey !== 'string' || !new RegExp(`^pk_${record.mppMode}_[A-Za-z0-9]{1,256}$`).test(action.publishableKey)) {
|
|
504
|
+
throw new CommerceError('stripe_spt_action_unsupported', 'Provider returned an unsupported or mismatched customer action');
|
|
505
|
+
}
|
|
506
|
+
return { purchaseId: id, action: { type: 'use_stripe_sdk', hashedValue: action.hashedValue, publishableKey: action.publishableKey } };
|
|
507
|
+
}
|
|
488
508
|
assertCurrentPolicy(record, signing) {
|
|
489
509
|
const peer = this.peer(record.cardUrl);
|
|
490
510
|
if (peer.id !== record.peerId || this.endpoint(peer) !== record.endpoint || peer.currency !== record.quote.currency || peer.recipient !== record.quote.recipient || (peer.protocol !== 'free' && atomic(record.quote.amount) > atomic(peer.maxPerPurchase)) || record.quote.payer !== (peer.protocol === 'free' ? record.caller : peer.protocol === 'mpp' ? this.options.mpp?.payer : this.options.signer?.address.toLowerCase()) || (record.protocol === 'mpp' && record.mppMode !== this.options.mpp?.mode))
|
package/dist/commerce/cli.js
CHANGED
|
@@ -137,6 +137,7 @@ async function run() {
|
|
|
137
137
|
const adapter = createStripeMppBuyer({
|
|
138
138
|
payer: configured.payer, mode: configured.mode, issuerAccountId: configured.issuerAccountId,
|
|
139
139
|
secretKey: privateFile(configured.secretKeyFile).toString('utf8').trim(), paymentMethod: configured.paymentMethod,
|
|
140
|
+
...(configured.publishableKey !== undefined ? { publishableKey: configured.publishableKey } : {}),
|
|
140
141
|
sellers: Object.fromEntries(Object.entries(configured.sellers).map(([id, seller]) => [id, { accountId: seller.accountId, secretKey: privateFile(seller.secretKeyFile).toString('utf8').trim() }])),
|
|
141
142
|
...(configured.issuance ? { issuance: configured.issuance } : {}), ...(configured.returnUrl ? { returnUrl: configured.returnUrl } : {}),
|
|
142
143
|
}, { statePath: values.state + '.stripe-issuer.sqlite3', vault: buyerVault });
|
|
@@ -4,5 +4,5 @@ export * from './buyer.js';
|
|
|
4
4
|
export * from './management.js';
|
|
5
5
|
export * from './vault.js';
|
|
6
6
|
export * from './chain.js';
|
|
7
|
-
export type { MppBuyerOptions, MppTokenOperation, MppVerificationContext, MppReceipt } from './mpp-client.js';
|
|
7
|
+
export type { MppAuthenticationAction, MppBuyerOptions, MppTokenOperation, MppVerificationContext, MppReceipt } from './mpp-client.js';
|
|
8
8
|
export * from './peer-proxy.js';
|
|
@@ -77,9 +77,11 @@ export class BuyerManagement {
|
|
|
77
77
|
throw new CommerceError('invalid_request', 'Confirmation identifiers must be strings');
|
|
78
78
|
return Response.json(await this.options.buyer.confirm(caller, body), { headers });
|
|
79
79
|
}
|
|
80
|
-
const match = /^\/management\/v1\/purchases\/([0-9a-f-]{36})(\/(?:recover|continue))?$/.exec(url.pathname);
|
|
80
|
+
const match = /^\/management\/v1\/purchases\/([0-9a-f-]{36})(\/(?:recover|continue|authentication))?$/.exec(url.pathname);
|
|
81
81
|
if (match && request.method === 'GET' && !match[2])
|
|
82
82
|
return Response.json(this.options.buyer.get(caller, match[1]), { headers });
|
|
83
|
+
if (match && request.method === 'GET' && match[2] === '/authentication')
|
|
84
|
+
return Response.json(await this.options.buyer.authentication(caller, match[1]), { headers });
|
|
83
85
|
if (match && request.method === 'POST' && match[2] === '/continue') {
|
|
84
86
|
const body = await jsonBody(request);
|
|
85
87
|
exactKeys(body, ['messageId', 'input']);
|
|
@@ -28,16 +28,27 @@ export interface MppVerificationContext {
|
|
|
28
28
|
mode: 'test' | 'live';
|
|
29
29
|
operationId: string;
|
|
30
30
|
}
|
|
31
|
+
export interface MppAuthenticationAction {
|
|
32
|
+
type: 'use_stripe_sdk';
|
|
33
|
+
hashedValue: string;
|
|
34
|
+
publishableKey: string;
|
|
35
|
+
}
|
|
36
|
+
/** Explicit official sandbox aliases, never accepted for live funding. */
|
|
37
|
+
export declare const STRIPE_TEST_PAYMENT_METHODS: readonly ["pm_card_visa", "pm_card_visa_chargeDeclined", "pm_card_authenticationRequired"];
|
|
38
|
+
export declare function isStripeTestPaymentMethod(value: string): boolean;
|
|
31
39
|
export interface MppBuyerOptions {
|
|
32
40
|
/** Stable private buyer/PSP identity; never inferred from seller metadata. */
|
|
33
41
|
payer: string;
|
|
34
42
|
mode: 'test' | 'live';
|
|
35
43
|
paymentMethod: string;
|
|
44
|
+
issuance?: 'issued-token' | 'test-helper';
|
|
36
45
|
createToken: (operation: MppTokenOperation) => Promise<string>;
|
|
37
46
|
/** Read the original operation only; never create another SPT on absence. */
|
|
38
47
|
recoverToken?: (operation: MppTokenOperation) => Promise<string | undefined>;
|
|
39
48
|
/** Independently read provider state and match account/mode/merchant/amount/order. */
|
|
40
49
|
verifyReceipt: (receipt: MppReceipt, context: MppVerificationContext) => Promise<boolean>;
|
|
50
|
+
/** Read only, for the owner of this exact original purchase. Never completes payment. */
|
|
51
|
+
getAuthentication?: (context: MppVerificationContext) => Promise<MppAuthenticationAction | undefined>;
|
|
41
52
|
}
|
|
42
53
|
export interface SavedMppAuthorization {
|
|
43
54
|
protocol: 'mpp';
|
|
@@ -3,6 +3,11 @@ import { Mppx } from 'mppx/client';
|
|
|
3
3
|
import { charge } from 'mppx/stripe/client';
|
|
4
4
|
import { createHash } from 'node:crypto';
|
|
5
5
|
import { CommerceError } from './types.js';
|
|
6
|
+
/** Explicit official sandbox aliases, never accepted for live funding. */
|
|
7
|
+
export const STRIPE_TEST_PAYMENT_METHODS = ['pm_card_visa', 'pm_card_visa_chargeDeclined', 'pm_card_authenticationRequired'];
|
|
8
|
+
export function isStripeTestPaymentMethod(value) {
|
|
9
|
+
return STRIPE_TEST_PAYMENT_METHODS.includes(value);
|
|
10
|
+
}
|
|
6
11
|
/** Native SDK preparation only; no fetch polyfill or automatic payment retry. */
|
|
7
12
|
export async function prepareMppCredential(challenge, paymentMethod, createToken) {
|
|
8
13
|
const client = Mppx.create({ polyfill: false, maxPaymentRetries: 0, methods: [charge({ paymentMethod, createToken })] });
|
|
@@ -12,6 +12,8 @@ export interface StripeMppBuyerConfig {
|
|
|
12
12
|
mode: 'test' | 'live';
|
|
13
13
|
issuerAccountId: string;
|
|
14
14
|
secretKey: string;
|
|
15
|
+
/** Issuer's Stripe.js key; optional until customer authentication is needed. */
|
|
16
|
+
publishableKey?: string;
|
|
15
17
|
/** Previously owner-authorized PaymentMethod; this adapter never collects card data. */
|
|
16
18
|
paymentMethod: string;
|
|
17
19
|
sellers: Readonly<Record<string, StripeSellerReadBinding>>;
|
|
@@ -5,6 +5,7 @@ import { Challenge } from 'mppx';
|
|
|
5
5
|
import { digest, isStripeProfileId } from './config.js';
|
|
6
6
|
import { CommerceError } from './types.js';
|
|
7
7
|
import { STRIPE_MPP_API_VERSION } from './stripe-provider.js';
|
|
8
|
+
import { isStripeTestPaymentMethod } from './mpp-client.js';
|
|
8
9
|
/** Buyer and seller use the same public preview, independently of mppx internals. */
|
|
9
10
|
export const STRIPE_ISSUER_API_VERSION = STRIPE_MPP_API_VERSION;
|
|
10
11
|
const ACCOUNT = /^acct_[A-Za-z0-9]{1,128}$/;
|
|
@@ -80,15 +81,18 @@ export function createStripeMppBuyer(input, runtime) {
|
|
|
80
81
|
const keyValid = (key) => typeof key === 'string' && new RegExp(`^(?:sk|rk)_${config.mode}_[A-Za-z0-9]{1,256}$`).test(key);
|
|
81
82
|
if (!['test', 'live'].includes(config.mode) || !['issued-token', 'test-helper'].includes(issuance) || !ACCOUNT.test(config.issuerAccountId) || !keyValid(config.secretKey) ||
|
|
82
83
|
typeof config.payer !== 'string' || !config.payer || config.payer.length > 200 || !config.sellers || !Object.keys(config.sellers).length ||
|
|
83
|
-
!(PAYMENT_METHOD.test(config.paymentMethod) || (issuance === 'test-helper' && config.paymentMethod
|
|
84
|
+
!(PAYMENT_METHOD.test(config.paymentMethod) || (issuance === 'test-helper' && isStripeTestPaymentMethod(config.paymentMethod)))) {
|
|
84
85
|
throw new CommerceError('stripe_issuer_configuration', 'Explicit Stripe issuer, funding method, seller read bindings and mode are required');
|
|
85
86
|
}
|
|
86
87
|
for (const [profile, seller] of Object.entries(config.sellers)) {
|
|
87
88
|
if (!isStripeProfileId(profile) || (config.mode === 'live' && profile.startsWith('profile_test_')) || !ACCOUNT.test(seller.accountId) || !keyValid(seller.secretKey))
|
|
88
89
|
throw new CommerceError('stripe_issuer_configuration', 'Seller profile, account and read credential must match the explicit mode');
|
|
89
90
|
}
|
|
90
|
-
if (issuance === 'test-helper' && (config.mode !== 'test' || config.paymentMethod
|
|
91
|
-
throw new CommerceError('stripe_test_helper_configuration', 'Test helper requires
|
|
91
|
+
if (issuance === 'test-helper' && (config.mode !== 'test' || !isStripeTestPaymentMethod(config.paymentMethod) || Object.values(config.sellers).some(s => s.accountId !== config.issuerAccountId))) {
|
|
92
|
+
throw new CommerceError('stripe_test_helper_configuration', 'Test helper requires an explicitly supported test card and the same configured test seller account');
|
|
93
|
+
}
|
|
94
|
+
if (config.publishableKey !== undefined && (typeof config.publishableKey !== 'string' || !new RegExp(`^pk_${config.mode}_[A-Za-z0-9]{1,256}$`).test(config.publishableKey))) {
|
|
95
|
+
throw new CommerceError('stripe_publishable_key', 'Stripe.js requires an issuer publishable key matching the configured mode');
|
|
92
96
|
}
|
|
93
97
|
if (config.returnUrl !== undefined) {
|
|
94
98
|
let url;
|
|
@@ -250,7 +254,7 @@ export function createStripeMppBuyer(input, runtime) {
|
|
|
250
254
|
}
|
|
251
255
|
}
|
|
252
256
|
const adapter = {
|
|
253
|
-
payer: config.payer, mode: config.mode, paymentMethod: config.paymentMethod,
|
|
257
|
+
payer: config.payer, mode: config.mode, paymentMethod: config.paymentMethod, issuance,
|
|
254
258
|
async assertReady() {
|
|
255
259
|
await account(config.secretKey, config.issuerAccountId);
|
|
256
260
|
for (const profile of Object.keys(config.sellers))
|
|
@@ -308,6 +312,23 @@ export function createStripeMppBuyer(input, runtime) {
|
|
|
308
312
|
throw new CommerceError('stripe_spt_action_unsupported', 'Original SPT requires customer action not supported by this Stripe.js bridge');
|
|
309
313
|
return { type: 'use_stripe_sdk', hashedValue: sdk.value };
|
|
310
314
|
},
|
|
315
|
+
async getAuthentication(context) {
|
|
316
|
+
if (!OPERATION.test(context.operationId))
|
|
317
|
+
throw new CommerceError('stripe_spt_recovery_required', 'Original issuer operation is required');
|
|
318
|
+
const row = ledger.get(context.operationId);
|
|
319
|
+
if (!row)
|
|
320
|
+
throw new CommerceError('stripe_spt_recovery_required', 'Original issuer operation is absent');
|
|
321
|
+
const original = saved(row), op = original.operation;
|
|
322
|
+
if (context.purchaseId !== op.purchaseId || context.mode !== config.mode || digest(context.quote) !== digest(op.quote)) {
|
|
323
|
+
throw new CommerceError('stripe_spt_recovery_binding', 'Authentication differs from the original purchase');
|
|
324
|
+
}
|
|
325
|
+
const action = await adapter.authenticationAction(context.operationId);
|
|
326
|
+
if (!action)
|
|
327
|
+
return undefined;
|
|
328
|
+
if (!config.publishableKey)
|
|
329
|
+
throw new CommerceError('stripe_publishable_key_required', 'Configure the issuer publishable key to authenticate this original payment');
|
|
330
|
+
return { ...action, publishableKey: config.publishableKey };
|
|
331
|
+
},
|
|
311
332
|
async verifyReceipt(receipt, context) {
|
|
312
333
|
const row = ledger.get(context.operationId);
|
|
313
334
|
if (!row?.tokenRef)
|