@envarai/envarpay 0.2.0-alpha.3 → 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.
@@ -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;
@@ -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) || !['test', 'live'].includes(options.mpp.mode) || typeof options.mpp.createToken !== 'function' || typeof options.mpp.verifyReceipt !== 'function'))
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))
@@ -123,15 +123,36 @@ async function run() {
123
123
  throw new CommerceError('buyer_key', 'Signer file must contain one 32-byte 0x-prefixed private key');
124
124
  signer = privateKeyToAccount(key);
125
125
  }
126
- if (needsMpp && !secrets.mppAdapterModule)
127
- throw new CommerceError('mpp_adapter_module', 'MPP peers require an explicit local adapter module for authorized funding and receipt verification');
128
- const mpp = needsMpp ? await loadMppAdapter(secrets.mppAdapterModule) : undefined;
126
+ if (secrets.mppAdapterModule && secrets.mppStripe)
127
+ throw new CommerceError('mpp_configuration', 'Choose the built-in Stripe issuer or one explicit adapter module');
128
+ if (needsMpp && !secrets.mppAdapterModule && !secrets.mppStripe)
129
+ throw new CommerceError('mpp_configuration', 'MPP peers require a configured Stripe issuer or explicit authorized funding adapter');
130
+ const buyerVault = secrets.vaultKeyFile ? new CredentialVault(join(dirname(values.state), 'buyer-authorizations'), privateFile(secrets.vaultKeyFile)) : undefined;
131
+ let mpp;
132
+ if (needsMpp && secrets.mppStripe) {
133
+ const configured = secrets.mppStripe;
134
+ if (!configured.secretKeyFile || !configured.sellers || typeof configured.sellers !== 'object' || Array.isArray(configured.sellers))
135
+ throw new CommerceError('mpp_configuration', 'Stripe issuer and seller read credentials must use private key files');
136
+ const { createStripeMppBuyer } = await import('./stripe-issuer.js');
137
+ const adapter = createStripeMppBuyer({
138
+ payer: configured.payer, mode: configured.mode, issuerAccountId: configured.issuerAccountId,
139
+ secretKey: privateFile(configured.secretKeyFile).toString('utf8').trim(), paymentMethod: configured.paymentMethod,
140
+ ...(configured.publishableKey !== undefined ? { publishableKey: configured.publishableKey } : {}),
141
+ sellers: Object.fromEntries(Object.entries(configured.sellers).map(([id, seller]) => [id, { accountId: seller.accountId, secretKey: privateFile(seller.secretKeyFile).toString('utf8').trim() }])),
142
+ ...(configured.issuance ? { issuance: configured.issuance } : {}), ...(configured.returnUrl ? { returnUrl: configured.returnUrl } : {}),
143
+ }, { statePath: values.state + '.stripe-issuer.sqlite3', vault: buyerVault });
144
+ closers.push(() => adapter.close());
145
+ await adapter.assertReady();
146
+ mpp = adapter;
147
+ }
148
+ else if (needsMpp)
149
+ mpp = await loadMppAdapter(secrets.mppAdapterModule);
129
150
  const { BuyerStore } = await import('./buyer-store.js');
130
151
  const { CommerceBuyer } = await import('./buyer.js');
131
152
  const { BuyerManagement, listenBuyerManagement } = await import('./management.js');
132
153
  const store = new BuyerStore(values.state);
133
154
  closers.push(() => store.close());
134
- const buyer = new CommerceBuyer({ policy, store, vault: secrets.vaultKeyFile ? new CredentialVault(join(dirname(values.state), 'buyer-authorizations'), privateFile(secrets.vaultKeyFile)) : undefined,
155
+ const buyer = new CommerceBuyer({ policy, store, vault: buyerVault,
135
156
  signer, mpp, peerTokens: secrets.peerTokens, peerEndpoints: secrets.peerEndpoints,
136
157
  ...(needsEvm ? { verifyReceipt: evmReceiptVerifier(secrets.rpcUrls), ...evmSettlementRecovery(secrets.rpcUrls) } : {}) });
137
158
  closers.push(() => buyer.stop());
@@ -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';
@@ -2,6 +2,8 @@ import { type BuyerPolicy, type CommerceConfig, type PaymentProfile } from './ty
2
2
  export declare function atomic(value: string): bigint;
3
3
  export declare function validateUrl(value: string, allowPrivate?: boolean): URL;
4
4
  export declare function currencyOf(profile: PaymentProfile): string;
5
+ /** Stripe documents sandbox Business Profile IDs as profile_test_<opaque id>. */
6
+ export declare function isStripeProfileId(value: unknown): value is string;
5
7
  export declare function digest(value: unknown): string;
6
8
  export declare function loadCommerceConfig(value: unknown): CommerceConfig;
7
9
  export declare function loadBuyerPolicy(value: unknown): BuyerPolicy;
@@ -45,6 +45,10 @@ export function validateUrl(value, allowPrivate = false) {
45
45
  export function currencyOf(profile) {
46
46
  return profile.adapter === 'x402' ? `${profile.network}/erc20:${profile.asset.toLowerCase()}` : profile.currency;
47
47
  }
48
+ /** Stripe documents sandbox Business Profile IDs as profile_test_<opaque id>. */
49
+ export function isStripeProfileId(value) {
50
+ return typeof value === 'string' && /^profile_(?:test_)?[A-Za-z0-9]{1,120}$/.test(value);
51
+ }
48
52
  function assertJson(value) {
49
53
  if (typeof value === 'number' && (!Number.isFinite(value) || (Number.isInteger(value) && !Number.isSafeInteger(value))))
50
54
  throw new CommerceError('unsafe_json_number', 'Use strings for integers outside the safe JSON number range');
@@ -172,7 +176,7 @@ export function loadBuyerPolicy(value) {
172
176
  throw new CommerceError('invalid_recipient', 'x402 peers require a nonzero EVM recipient and USDC asset');
173
177
  peer.recipient = peer.recipient.toLowerCase();
174
178
  }
175
- else if (peer.currency !== 'usd' || !/^profile_[A-Za-z0-9]+$/.test(peer.recipient))
179
+ else if (peer.currency !== 'usd' || !isStripeProfileId(peer.recipient))
176
180
  throw new CommerceError('invalid_recipient', 'MPP peers require USD and a verified Stripe merchant profile');
177
181
  const limit = limits.get(peer.currency);
178
182
  if (limit === undefined || atomic(peer.maxPerPurchase) > limit)
@@ -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 })] });
@@ -1,3 +1,4 @@
1
1
  /** Opt-in MPP server adapter. Account eligibility is checked at startup. */
2
2
  export * from './mpp.js';
3
3
  export * from './stripe-provider.js';
4
+ export * from './stripe-issuer.js';
@@ -1,3 +1,4 @@
1
1
  /** Opt-in MPP server adapter. Account eligibility is checked at startup. */
2
2
  export * from './mpp.js';
3
3
  export * from './stripe-provider.js';
4
+ export * from './stripe-issuer.js';
@@ -0,0 +1,41 @@
1
+ import { CredentialVault } from './vault.js';
2
+ import type { MppBuyerOptions } from './mpp-client.js';
3
+ /** Buyer and seller use the same public preview, independently of mppx internals. */
4
+ export declare const STRIPE_ISSUER_API_VERSION = "2026-09-30.preview";
5
+ export interface StripeSellerReadBinding {
6
+ accountId: string;
7
+ /** Account-scoped read credential for account, profile and PaymentIntent reads. */
8
+ secretKey: string;
9
+ }
10
+ export interface StripeMppBuyerConfig {
11
+ payer: string;
12
+ mode: 'test' | 'live';
13
+ issuerAccountId: string;
14
+ secretKey: string;
15
+ /** Issuer's Stripe.js key; optional until customer authentication is needed. */
16
+ publishableKey?: string;
17
+ /** Previously owner-authorized PaymentMethod; this adapter never collects card data. */
18
+ paymentMethod: string;
19
+ sellers: Readonly<Record<string, StripeSellerReadBinding>>;
20
+ returnUrl?: string;
21
+ /** Real Stripe test infrastructure, selected explicitly; never a live fallback. */
22
+ issuance?: 'issued-token' | 'test-helper';
23
+ }
24
+ export interface StripeMppBuyerRuntime {
25
+ statePath: string;
26
+ vault: CredentialVault;
27
+ fetchImpl?: typeof fetch;
28
+ now?: () => number;
29
+ }
30
+ export interface StripeAuthenticationAction {
31
+ type: 'use_stripe_sdk';
32
+ hashedValue: string;
33
+ }
34
+ export interface StripeMppBuyerAdapter extends MppBuyerOptions {
35
+ assertReady(): Promise<void>;
36
+ close(): void;
37
+ /** Private owner-only API. Pass this value directly to Stripe.js, never logs. */
38
+ authenticationAction(operationId: string): Promise<StripeAuthenticationAction | undefined>;
39
+ }
40
+ /** Official SPT issuer with exact scope and original-ID-only recovery. No automatic funding. */
41
+ export declare function createStripeMppBuyer(input: StripeMppBuyerConfig, runtime: StripeMppBuyerRuntime): StripeMppBuyerAdapter;
@@ -0,0 +1,351 @@
1
+ import { DatabaseSync } from 'node:sqlite';
2
+ import { chmodSync, mkdirSync } from 'node:fs';
3
+ import { dirname } from 'node:path';
4
+ import { Challenge } from 'mppx';
5
+ import { digest, isStripeProfileId } from './config.js';
6
+ import { CommerceError } from './types.js';
7
+ import { STRIPE_MPP_API_VERSION } from './stripe-provider.js';
8
+ import { isStripeTestPaymentMethod } from './mpp-client.js';
9
+ /** Buyer and seller use the same public preview, independently of mppx internals. */
10
+ export const STRIPE_ISSUER_API_VERSION = STRIPE_MPP_API_VERSION;
11
+ const ACCOUNT = /^acct_[A-Za-z0-9]{1,128}$/;
12
+ const TOKEN = /^spt_[A-Za-z0-9_]{1,200}$/;
13
+ const PAYMENT_METHOD = /^pm_[A-Za-z0-9]{1,128}$/;
14
+ const OPERATION = /^[A-Za-z0-9_-]{1,200}$/;
15
+ const UUID = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/;
16
+ const object = (value) => value !== null && typeof value === 'object' && !Array.isArray(value) ? value : {};
17
+ /** Durable private references only; exact requests and tokens live in CredentialVault. */
18
+ class IssuerLedger {
19
+ db;
20
+ owner;
21
+ closed = false;
22
+ constructor(path) {
23
+ if (typeof path !== 'string' || !path.trim() || path === ':memory:')
24
+ throw new CommerceError('stripe_issuer_state', 'SPT issuance requires a durable private ledger');
25
+ mkdirSync(dirname(path), { recursive: true, mode: 0o700 });
26
+ this.owner = new DatabaseSync(path + '.owner.sqlite3');
27
+ chmodSync(path + '.owner.sqlite3', 0o600);
28
+ try {
29
+ this.owner.exec('PRAGMA busy_timeout=0; BEGIN EXCLUSIVE');
30
+ }
31
+ catch {
32
+ this.owner.close();
33
+ throw new CommerceError('store_locked', 'Stripe issuer ledger already has an active process');
34
+ }
35
+ try {
36
+ this.db = new DatabaseSync(path);
37
+ chmodSync(path, 0o600);
38
+ this.db.exec(`PRAGMA journal_mode=WAL; PRAGMA synchronous=FULL;
39
+ CREATE TABLE IF NOT EXISTS stripe_issuer_meta(version INTEGER NOT NULL);
40
+ INSERT INTO stripe_issuer_meta SELECT 1 WHERE NOT EXISTS(SELECT 1 FROM stripe_issuer_meta);
41
+ CREATE TABLE IF NOT EXISTS stripe_issuer_operations(
42
+ operation_id TEXT PRIMARY KEY, idempotency_key TEXT NOT NULL UNIQUE, purchase_id TEXT NOT NULL UNIQUE,
43
+ fingerprint TEXT NOT NULL, request_ref TEXT NOT NULL, token_ref TEXT,
44
+ created_at INTEGER NOT NULL
45
+ );`);
46
+ if (this.db.prepare('SELECT version FROM stripe_issuer_meta').get()?.version !== 1)
47
+ throw new CommerceError('store_version', 'Unsupported Stripe issuer ledger version');
48
+ }
49
+ catch (error) {
50
+ this.db?.close();
51
+ this.owner.close();
52
+ throw error;
53
+ }
54
+ }
55
+ get(id) {
56
+ const row = this.db.prepare('SELECT * FROM stripe_issuer_operations WHERE operation_id=?').get(id);
57
+ return row ? { operationId: String(row.operation_id), fingerprint: String(row.fingerprint), requestRef: String(row.request_ref), ...(row.token_ref ? { tokenRef: String(row.token_ref) } : {}) } : undefined;
58
+ }
59
+ insert(operation, fingerprint, requestRef, now) {
60
+ try {
61
+ this.db.prepare('INSERT INTO stripe_issuer_operations VALUES(?,?,?,?,?,NULL,?)').run(operation.operationId, operation.idempotencyKey, operation.purchaseId, fingerprint, requestRef, now);
62
+ }
63
+ catch {
64
+ throw new CommerceError('stripe_spt_operation_conflict', 'Purchase or idempotency identity already has an original issuer operation');
65
+ }
66
+ return this.get(operation.operationId);
67
+ }
68
+ token(id, ref) { this.db.prepare('UPDATE stripe_issuer_operations SET token_ref=? WHERE operation_id=? AND token_ref IS NULL').run(ref, id); }
69
+ close() {
70
+ if (this.closed)
71
+ return;
72
+ this.closed = true;
73
+ this.db.close();
74
+ this.owner?.exec('ROLLBACK');
75
+ this.owner?.close();
76
+ }
77
+ }
78
+ /** Official SPT issuer with exact scope and original-ID-only recovery. No automatic funding. */
79
+ export function createStripeMppBuyer(input, runtime) {
80
+ const config = structuredClone(input), issuance = config.issuance ?? 'issued-token';
81
+ const keyValid = (key) => typeof key === 'string' && new RegExp(`^(?:sk|rk)_${config.mode}_[A-Za-z0-9]{1,256}$`).test(key);
82
+ if (!['test', 'live'].includes(config.mode) || !['issued-token', 'test-helper'].includes(issuance) || !ACCOUNT.test(config.issuerAccountId) || !keyValid(config.secretKey) ||
83
+ typeof config.payer !== 'string' || !config.payer || config.payer.length > 200 || !config.sellers || !Object.keys(config.sellers).length ||
84
+ !(PAYMENT_METHOD.test(config.paymentMethod) || (issuance === 'test-helper' && isStripeTestPaymentMethod(config.paymentMethod)))) {
85
+ throw new CommerceError('stripe_issuer_configuration', 'Explicit Stripe issuer, funding method, seller read bindings and mode are required');
86
+ }
87
+ for (const [profile, seller] of Object.entries(config.sellers)) {
88
+ if (!isStripeProfileId(profile) || (config.mode === 'live' && profile.startsWith('profile_test_')) || !ACCOUNT.test(seller.accountId) || !keyValid(seller.secretKey))
89
+ throw new CommerceError('stripe_issuer_configuration', 'Seller profile, account and read credential must match the explicit mode');
90
+ }
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');
96
+ }
97
+ if (config.returnUrl !== undefined) {
98
+ let url;
99
+ try {
100
+ url = new URL(config.returnUrl);
101
+ }
102
+ catch {
103
+ throw new CommerceError('stripe_return_url', 'Stripe return URL must be an owner-configured HTTPS URL');
104
+ }
105
+ if (url.protocol !== 'https:' || url.username || url.password || config.returnUrl.length > 2048)
106
+ throw new CommerceError('stripe_return_url', 'Stripe return URL must be an owner-configured HTTPS URL');
107
+ }
108
+ const ledger = new IssuerLedger(runtime.statePath), vault = runtime.vault;
109
+ const fetchImpl = runtime.fetchImpl ?? fetch, now = runtime.now ?? Date.now;
110
+ const inFlight = new Set();
111
+ async function call(secret, path, body, idempotencyKey) {
112
+ const headers = new Headers({ Authorization: `Bearer ${secret}`, 'Stripe-Version': STRIPE_ISSUER_API_VERSION });
113
+ if (body !== undefined)
114
+ headers.set('Content-Type', 'application/x-www-form-urlencoded');
115
+ if (idempotencyKey)
116
+ headers.set('Idempotency-Key', idempotencyKey);
117
+ let response;
118
+ try {
119
+ response = await fetchImpl(`https://api.stripe.com${path}`, { method: body === undefined ? 'GET' : 'POST', headers, ...(body === undefined ? {} : { body }), redirect: 'error', signal: AbortSignal.timeout(20000) });
120
+ }
121
+ catch {
122
+ throw new CommerceError('stripe_transport_unknown', 'Stripe request outcome is unknown; retain the original operation');
123
+ }
124
+ const reader = response.body?.getReader();
125
+ let size = 0;
126
+ const chunks = [];
127
+ try {
128
+ if (reader)
129
+ for (;;) {
130
+ const { value, done } = await reader.read();
131
+ if (done)
132
+ break;
133
+ size += value.byteLength;
134
+ if (size > 1024 * 1024) {
135
+ await reader.cancel();
136
+ throw new CommerceError('stripe_response_size', 'Stripe response exceeds limit');
137
+ }
138
+ chunks.push(value);
139
+ }
140
+ }
141
+ catch (error) {
142
+ if (error instanceof CommerceError && error.code === 'stripe_response_size')
143
+ throw error;
144
+ throw new CommerceError('stripe_transport_unknown', 'Stripe response was interrupted; retain the original operation');
145
+ }
146
+ if (!response.ok)
147
+ throw new CommerceError(response.status === 429 ? 'stripe_rate_limit' : 'stripe_request_failed', `Stripe request failed (HTTP ${response.status})`);
148
+ let value;
149
+ try {
150
+ value = JSON.parse(Buffer.concat(chunks).toString('utf8'));
151
+ }
152
+ catch {
153
+ throw new CommerceError('stripe_response', 'Stripe returned invalid JSON');
154
+ }
155
+ if (!value || typeof value !== 'object' || Array.isArray(value))
156
+ throw new CommerceError('stripe_response', 'Stripe returned an invalid object');
157
+ return value;
158
+ }
159
+ async function account(secret, id) {
160
+ const result = await call(secret, '/v1/account');
161
+ if (result.id !== id || result.object !== 'account')
162
+ throw new CommerceError('stripe_account_mismatch', 'Stripe credential does not prove the expected account');
163
+ return result;
164
+ }
165
+ async function sellerReady(profile) {
166
+ const seller = config.sellers[profile];
167
+ if (!Object.hasOwn(config.sellers, profile) || !seller)
168
+ throw new CommerceError('stripe_seller_binding', 'Seller is not in the local read-verification allowlist');
169
+ const result = await account(seller.secretKey, seller.accountId);
170
+ if (config.mode === 'live' && (result.charges_enabled !== true || object(result.capabilities).card_payments !== 'active'))
171
+ throw new CommerceError('stripe_charges_disabled', 'Live seller card payments are not enabled');
172
+ const ownProfile = await call(seller.secretKey, '/v2/network/business_profiles/me');
173
+ // Require the documented mode as well as the credential-scoped /me identity.
174
+ if (ownProfile.id !== profile || ownProfile.object !== 'v2.network.business_profile' || ownProfile.livemode !== (config.mode === 'live'))
175
+ throw new CommerceError('stripe_profile_mismatch', 'Seller credential does not prove the expected profile and mode');
176
+ return seller;
177
+ }
178
+ function requestFor(operation) {
179
+ const quote = operation.quote, challenge = operation.challenge;
180
+ const details = object(challenge?.request?.methodDetails), metadata = object(operation.metadata);
181
+ const expiry = Math.floor(Date.parse(quote?.expiresAt) / 1000);
182
+ if (!OPERATION.test(operation.operationId) || operation.idempotencyKey !== operation.operationId || !UUID.test(operation.purchaseId) ||
183
+ operation.mode !== config.mode || operation.paymentMethod !== config.paymentMethod || operation.currency !== 'usd' || !/^[1-9][0-9]{1,7}$/.test(operation.amount) ||
184
+ BigInt(operation.amount) < 50n || BigInt(operation.amount) > 99999999n || operation.amount !== quote.amount || quote.currency !== 'usd' || quote.payer !== config.payer ||
185
+ operation.networkId !== quote.recipient || !isStripeProfileId(operation.networkId) || !Object.hasOwn(config.sellers, operation.networkId) || !Number.isSafeInteger(expiry) || operation.expiresAt !== expiry ||
186
+ challenge?.method !== 'stripe' || challenge.intent !== 'charge' || challenge.header !== 'Payment-Authorization' || challenge.expires !== quote.expiresAt ||
187
+ challenge.request.amount !== quote.amount || challenge.request.currency !== 'usd' || challenge.request.recipient !== quote.recipient || challenge.request.externalId !== quote.quoteId ||
188
+ details.networkId !== quote.recipient || digest(details.metadata ?? {}) !== digest(metadata) ||
189
+ metadata.envarpay_quote !== quote.quoteId || metadata.envarpay_terms !== quote.termsDigest || typeof metadata.envarpay_order !== 'string' || !UUID.test(metadata.envarpay_order) ||
190
+ typeof quote.quoteId !== 'string' || quote.quoteId.length > 500 || typeof quote.termsDigest !== 'string' || quote.termsDigest.length > 500 ||
191
+ Challenge.serialize(challenge).length > 32768)
192
+ throw new CommerceError('stripe_spt_binding', 'SPT request differs from the frozen purchase or approved funding scope');
193
+ const body = new URLSearchParams({ payment_method: operation.paymentMethod,
194
+ 'usage_limits[currency]': 'usd', 'usage_limits[max_amount]': operation.amount, 'usage_limits[expires_at]': String(expiry),
195
+ 'shared_metadata[envarpay_operation]': operation.operationId, 'shared_metadata[envarpay_purchase]': operation.purchaseId,
196
+ 'shared_metadata[envarpay_order]': metadata.envarpay_order, 'shared_metadata[envarpay_quote]': quote.quoteId, 'shared_metadata[envarpay_terms]': quote.termsDigest,
197
+ });
198
+ if (issuance === 'issued-token') {
199
+ body.set('seller_details[network_business_profile]', operation.networkId);
200
+ if (config.returnUrl)
201
+ body.set('return_url', config.returnUrl);
202
+ }
203
+ return { operation: structuredClone(operation), body: body.toString(), binding: { issuerAccountId: config.issuerAccountId, sellerAccountId: config.sellers[operation.networkId].accountId, issuance, ...(config.returnUrl ? { returnUrl: config.returnUrl } : {}) } };
204
+ }
205
+ function saved(row) {
206
+ const original = vault.get(row.requestRef);
207
+ if (row.operationId !== original.operation?.operationId || digest(original) !== row.fingerprint || digest(requestFor(original.operation)) !== row.fingerprint)
208
+ throw new CommerceError('stripe_spt_recovery_binding', 'Original issuer scope changed or failed integrity validation');
209
+ return original;
210
+ }
211
+ function tokenId(row) {
212
+ if (!row.tokenRef)
213
+ throw new CommerceError('stripe_spt_recovery_required', 'Original SPT ID is unknown; do not issue a replacement token');
214
+ const value = vault.get(row.tokenRef);
215
+ if (value.operationId !== row.operationId || value.fingerprint !== row.fingerprint || !TOKEN.test(value.id))
216
+ throw new CommerceError('stripe_spt_recovery_binding', 'Original encrypted SPT binding is invalid');
217
+ return value.id;
218
+ }
219
+ function validateToken(value, original, expectedId) {
220
+ const operation = original.operation, limits = object(value.usage_limits), meta = object(value.shared_metadata);
221
+ if (value.id !== expectedId || value.object !== (issuance === 'issued-token' ? 'shared_payment.issued_token' : 'shared_payment.granted_token') ||
222
+ value.livemode !== (config.mode === 'live') || limits.currency !== 'usd' || !Number.isSafeInteger(limits.max_amount) || String(limits.max_amount) !== operation.amount || limits.expires_at !== operation.expiresAt ||
223
+ meta.envarpay_operation !== operation.operationId || meta.envarpay_purchase !== operation.purchaseId || meta.envarpay_order !== operation.metadata.envarpay_order || meta.envarpay_quote !== operation.quote.quoteId || meta.envarpay_terms !== operation.quote.termsDigest ||
224
+ (issuance === 'issued-token' && (value.payment_method !== operation.paymentMethod || object(value.seller_details).network_business_profile !== operation.networkId)) ||
225
+ (issuance === 'test-helper' && object(value.payment_method_details).type !== 'card'))
226
+ throw new CommerceError('stripe_spt_response_binding', 'Stripe token does not match the original scope and mode');
227
+ }
228
+ function usable(value, original) {
229
+ if (value.status === 'requires_action')
230
+ throw new CommerceError('stripe_spt_requires_action', 'Original SPT requires customer authentication through Stripe.js');
231
+ const captured = object(object(value.usage_details).amount_captured);
232
+ if ((issuance === 'issued-token' && value.status !== 'active') || value.deactivated_at != null || value.deactivated_reason != null ||
233
+ captured.value !== 0 || captured.currency !== 'usd' || original.operation.expiresAt <= Math.floor(now() / 1000))
234
+ throw new CommerceError('stripe_spt_unusable', 'Original SPT is expired, consumed, inactive or not proven unused');
235
+ return String(value.id);
236
+ }
237
+ async function retrieve(row) {
238
+ const original = saved(row), id = tokenId(row);
239
+ await account(config.secretKey, config.issuerAccountId);
240
+ // Test helpers use the seller account itself; issued tokens use the issuer account.
241
+ const value = await call(config.secretKey, `/v1/shared_payment/${issuance === 'issued-token' ? 'issued' : 'granted'}_tokens/${id}`);
242
+ validateToken(value, original, id);
243
+ return { original, value };
244
+ }
245
+ async function exclusive(id, fn) {
246
+ if (inFlight.has(id))
247
+ throw new CommerceError('stripe_spt_operation_busy', 'Original SPT operation is already in progress');
248
+ inFlight.add(id);
249
+ try {
250
+ return await fn();
251
+ }
252
+ finally {
253
+ inFlight.delete(id);
254
+ }
255
+ }
256
+ const adapter = {
257
+ payer: config.payer, mode: config.mode, paymentMethod: config.paymentMethod, issuance,
258
+ async assertReady() {
259
+ await account(config.secretKey, config.issuerAccountId);
260
+ for (const profile of Object.keys(config.sellers))
261
+ await sellerReady(profile);
262
+ },
263
+ close() { ledger.close(); },
264
+ async createToken(operation) {
265
+ return exclusive(operation.operationId, async () => {
266
+ const original = requestFor(operation), fingerprint = digest(original), prior = ledger.get(operation.operationId);
267
+ if (prior) {
268
+ if (prior.fingerprint !== fingerprint)
269
+ throw new CommerceError('stripe_spt_recovery_binding', 'Operation ID is already bound to another SPT request');
270
+ const result = await retrieve(prior);
271
+ return usable(result.value, result.original);
272
+ }
273
+ if (operation.expiresAt <= Math.floor(now() / 1000) || operation.expiresAt > Math.floor(now() / 1000) + 3600)
274
+ throw new CommerceError('stripe_spt_expiry', 'SPT expiry must be within the original one-hour quote window');
275
+ await account(config.secretKey, config.issuerAccountId);
276
+ await sellerReady(operation.networkId);
277
+ if (operation.expiresAt <= Math.floor(now() / 1000))
278
+ throw new CommerceError('stripe_spt_expiry', 'Original quote expired during issuer readiness checks');
279
+ // No await between durable insertion and starting the one permitted POST.
280
+ const row = ledger.insert(operation, fingerprint, vault.put(original), now());
281
+ const value = await call(config.secretKey, issuance === 'issued-token' ? '/v1/shared_payment/issued_tokens' : '/v1/test_helpers/shared_payment/granted_tokens', original.body, operation.idempotencyKey);
282
+ if (typeof value.id !== 'string' || !TOKEN.test(value.id))
283
+ throw new CommerceError('stripe_spt_response_binding', 'Stripe did not return a valid original SPT ID');
284
+ // Save the ID even if the remaining response needs independent reconciliation.
285
+ ledger.token(row.operationId, vault.put({ operationId: row.operationId, fingerprint, id: value.id }));
286
+ validateToken(value, original, value.id);
287
+ return usable(value, original);
288
+ });
289
+ },
290
+ async recoverToken(operation) {
291
+ return exclusive(operation.operationId, async () => {
292
+ const row = ledger.get(operation.operationId);
293
+ if (!row)
294
+ throw new CommerceError('stripe_spt_recovery_required', 'Original issuer operation is absent; do not issue a replacement token');
295
+ if (digest(requestFor(operation)) !== row.fingerprint)
296
+ throw new CommerceError('stripe_spt_recovery_binding', 'Recovery differs from the original SPT request');
297
+ const result = await retrieve(row);
298
+ return usable(result.value, result.original);
299
+ });
300
+ },
301
+ async authenticationAction(operationId) {
302
+ if (!OPERATION.test(operationId))
303
+ throw new CommerceError('stripe_spt_recovery_required', 'Original issuer operation is required');
304
+ const row = ledger.get(operationId);
305
+ if (!row)
306
+ throw new CommerceError('stripe_spt_recovery_required', 'Original issuer operation is absent');
307
+ const { value } = await retrieve(row);
308
+ if (value.status !== 'requires_action')
309
+ return undefined;
310
+ const action = object(value.next_action), sdk = object(action.use_stripe_sdk);
311
+ if (action.type !== 'use_stripe_sdk' || typeof sdk.value !== 'string' || !sdk.value || sdk.value.length > 16384)
312
+ throw new CommerceError('stripe_spt_action_unsupported', 'Original SPT requires customer action not supported by this Stripe.js bridge');
313
+ return { type: 'use_stripe_sdk', hashedValue: sdk.value };
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
+ },
332
+ async verifyReceipt(receipt, context) {
333
+ const row = ledger.get(context.operationId);
334
+ if (!row?.tokenRef)
335
+ return false;
336
+ const original = saved(row), op = original.operation, seller = config.sellers[op.networkId];
337
+ if (context.purchaseId !== op.purchaseId || context.mode !== config.mode || digest(context.quote) !== digest(op.quote) ||
338
+ receipt.method !== 'stripe' || receipt.status !== 'success' || !/^pi_[A-Za-z0-9]{1,128}$/.test(receipt.reference) ||
339
+ receipt.externalId !== op.quote.quoteId || receipt.amount !== op.amount || receipt.currency !== 'usd' || receipt.merchant !== op.networkId ||
340
+ receipt.accountId !== seller.accountId || receipt.livemode !== (config.mode === 'live'))
341
+ return false;
342
+ await sellerReady(op.networkId);
343
+ const intent = await call(seller.secretKey, `/v1/payment_intents/${receipt.reference}`);
344
+ const metadata = object(intent.metadata);
345
+ return intent.id === receipt.reference && intent.object === 'payment_intent' && intent.status === 'succeeded' && intent.livemode === (config.mode === 'live') &&
346
+ Number.isSafeInteger(intent.amount) && String(intent.amount) === op.amount && Number.isSafeInteger(intent.amount_received) && String(intent.amount_received) === op.amount &&
347
+ intent.currency === 'usd' && metadata.envarpay_order === op.metadata.envarpay_order && metadata.envarpay_quote === op.quote.quoteId && metadata.envarpay_terms === op.quote.termsDigest;
348
+ },
349
+ };
350
+ return adapter;
351
+ }
@@ -5,5 +5,7 @@ export interface StripeProviderConfig {
5
5
  merchantProfile: string;
6
6
  mode: 'test' | 'live';
7
7
  }
8
+ /** Public Stripe Business Profiles and Shared Payment Tokens contract. */
9
+ export declare const STRIPE_MPP_API_VERSION = "2026-09-30.preview";
8
10
  /** Stripe REST transport used by the official mppx Stripe method. No SPT creation. */
9
11
  export declare function stripeProvider(config: StripeProviderConfig, fetchImpl?: typeof fetch): StripeProvider;
@@ -1,8 +1,12 @@
1
1
  import { CommerceError } from './types.js';
2
+ import { isStripeProfileId } from './config.js';
3
+ /** Public Stripe Business Profiles and Shared Payment Tokens contract. */
4
+ export const STRIPE_MPP_API_VERSION = '2026-09-30.preview';
2
5
  /** Stripe REST transport used by the official mppx Stripe method. No SPT creation. */
3
6
  export function stripeProvider(config, fetchImpl = fetch) {
4
- if (!/^acct_[A-Za-z0-9]+$/.test(config.accountId) || !/^profile_[A-Za-z0-9]+$/.test(config.merchantProfile) ||
5
- !config.secretKey.startsWith(config.mode === 'live' ? 'sk_live_' : 'sk_test_')) {
7
+ if (!['test', 'live'].includes(config.mode) || !/^acct_[A-Za-z0-9]+$/.test(config.accountId) || !isStripeProfileId(config.merchantProfile) ||
8
+ !new RegExp(`^(?:sk|rk)_${config.mode}_[A-Za-z0-9]{1,256}$`).test(config.secretKey) ||
9
+ (config.mode === 'live' && config.merchantProfile.startsWith('profile_test_'))) {
6
10
  throw new CommerceError('stripe_configuration', 'Explicit matching Stripe account, profile and test/live credentials are required');
7
11
  }
8
12
  const call = async (path, method = 'GET', params, options) => {
@@ -26,30 +30,45 @@ export function stripeProvider(config, fetchImpl = fetch) {
26
30
  for (const [key, value] of Object.entries(params ?? {}))
27
31
  add(key, value);
28
32
  const headers = new Headers({ Authorization: `Bearer ${config.secretKey}` });
29
- if (options?.apiVersion)
30
- headers.set('Stripe-Version', options.apiVersion);
33
+ headers.set('Stripe-Version', STRIPE_MPP_API_VERSION);
31
34
  if (options?.idempotencyKey)
32
35
  headers.set('Idempotency-Key', options.idempotencyKey);
33
36
  if (method !== 'GET')
34
37
  headers.set('Content-Type', 'application/x-www-form-urlencoded');
35
- const response = await fetchImpl(`https://api.stripe.com${path}${method === 'GET' && body.size ? '?' + body.toString() : ''}`, {
36
- method, headers, ...(method === 'GET' ? {} : { body }), redirect: 'error', signal: AbortSignal.timeout(20000),
37
- });
38
+ const url = `https://api.stripe.com${path}${method === 'GET' && body.size ? '?' + body.toString() : ''}`;
39
+ let response;
40
+ try {
41
+ response = await fetchImpl(url, {
42
+ method, headers, ...(method === 'GET' ? {} : { body }), redirect: 'error', signal: AbortSignal.timeout(20000),
43
+ });
44
+ }
45
+ catch {
46
+ throw new CommerceError('stripe_transport_unknown', 'Stripe request outcome is unknown; preserve the original payment');
47
+ }
48
+ if (response.url && response.url !== url)
49
+ throw new CommerceError('stripe_redirect', 'Stripe response escaped the fixed provider request');
38
50
  const reader = response.body?.getReader();
39
51
  let size = 0;
40
52
  const chunks = [];
41
- if (reader)
42
- for (;;) {
43
- const { value, done } = await reader.read();
44
- if (done)
45
- break;
46
- size += value.byteLength;
47
- if (size > 1024 * 1024) {
48
- await reader.cancel();
49
- throw new CommerceError('stripe_response_size', 'Stripe response exceeds limit');
53
+ try {
54
+ if (reader)
55
+ for (;;) {
56
+ const { value, done } = await reader.read();
57
+ if (done)
58
+ break;
59
+ size += value.byteLength;
60
+ if (size > 1024 * 1024) {
61
+ await reader.cancel();
62
+ throw new CommerceError('stripe_response_size', 'Stripe response exceeds limit');
63
+ }
64
+ chunks.push(value);
50
65
  }
51
- chunks.push(value);
52
- }
66
+ }
67
+ catch (error) {
68
+ if (error instanceof CommerceError)
69
+ throw error;
70
+ throw new CommerceError('stripe_transport_unknown', 'Stripe response was interrupted; preserve the original payment');
71
+ }
53
72
  let result;
54
73
  try {
55
74
  result = JSON.parse(Buffer.concat(chunks).toString('utf8'));
@@ -73,12 +92,28 @@ export function stripeProvider(config, fetchImpl = fetch) {
73
92
  accountId: config.accountId, merchantProfile: config.merchantProfile, mode: config.mode,
74
93
  async assertReady() {
75
94
  const account = await call('/v1/account');
76
- if (account.id !== config.accountId)
95
+ if (account.object !== 'account' || account.id !== config.accountId)
77
96
  throw new CommerceError('stripe_account_mismatch', 'Stripe key belongs to another account');
78
- if (config.mode === 'live' && account.charges_enabled !== true)
79
- throw new CommerceError('stripe_charges_disabled', 'Live merchant is not enabled for charges');
97
+ if (config.mode === 'live' && (account.charges_enabled !== true || account.capabilities?.card_payments !== 'active'))
98
+ throw new CommerceError('stripe_charges_disabled', 'Live merchant is not enabled for card charges');
99
+ const profile = await call('/v2/network/business_profiles/me');
100
+ // /me binds the profile to this authenticated merchant; the documented
101
+ // livemode field must also prove the requested test/live boundary.
102
+ if (profile.object !== 'v2.network.business_profile' || profile.id !== config.merchantProfile ||
103
+ profile.livemode !== (config.mode === 'live'))
104
+ throw new CommerceError('stripe_profile_mismatch', 'Stripe profile does not match this merchant and mode');
105
+ },
106
+ async create(params, options) {
107
+ if (!Number.isSafeInteger(params.amount) || Number(params.amount) < 50 || Number(params.amount) > 99999999 || params.currency !== 'usd' || params.confirm !== true ||
108
+ typeof params.shared_payment_granted_token !== 'string' || !/^spt_[A-Za-z0-9_]+$/.test(params.shared_payment_granted_token) ||
109
+ params.payment_method_data !== undefined || params.payment_method !== undefined || typeof options.idempotencyKey !== 'string' || !options.idempotencyKey || options.idempotencyKey.length > 255) {
110
+ throw new CommerceError('stripe_request', 'MPP Stripe charge requires a bound USD amount, original SPT and idempotency key');
111
+ }
112
+ // mppx 0.13.1 supplies the older SDK argument. The pinned public Stripe API
113
+ // consumes SPTs under payment_method_data; preserve the same original token.
114
+ const { shared_payment_granted_token: token, ...rest } = params;
115
+ return validateIntent(await call('/v1/payment_intents', 'POST', { ...rest, payment_method_data: { shared_payment_granted_token: token } }, options));
80
116
  },
81
- async create(params, options) { return validateIntent(await call('/v1/payment_intents', 'POST', params, options)); },
82
117
  async retrieve(id) {
83
118
  if (!/^pi_[A-Za-z0-9]+$/.test(id))
84
119
  throw new CommerceError('stripe_reference', 'Invalid original PaymentIntent');
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@envarai/envarpay",
3
- "version": "0.2.0-alpha.3",
3
+ "version": "0.2.0-alpha.5",
4
4
  "description": "Independent A2A service commerce, budgeted buyers and x402/MPP payments",
5
5
  "type": "module",
6
6
  "license": "MIT",