@envarai/envarpay 0.2.0-alpha.19 → 0.2.0-alpha.20

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.
Files changed (54) hide show
  1. package/README.md +18 -63
  2. package/dist/commerce/alerts.d.ts +5 -15
  3. package/dist/commerce/alerts.js +21 -77
  4. package/dist/commerce/card.js +1 -1
  5. package/dist/commerce/cli.js +17 -145
  6. package/dist/commerce/config.d.ts +1 -4
  7. package/dist/commerce/config.js +4 -82
  8. package/dist/commerce/configure.d.ts +0 -1
  9. package/dist/commerce/configure.js +0 -80
  10. package/dist/commerce/discovery.d.ts +1 -5
  11. package/dist/commerce/discovery.js +1 -4
  12. package/dist/commerce/envar-balance.js +1 -1
  13. package/dist/commerce/envar.d.ts +1 -32
  14. package/dist/commerce/envar.js +3 -82
  15. package/dist/commerce/operations.d.ts +1 -2
  16. package/dist/commerce/operations.js +17 -20
  17. package/dist/commerce/pricing.d.ts +0 -1
  18. package/dist/commerce/pricing.js +1 -3
  19. package/dist/commerce/runtime.d.ts +0 -2
  20. package/dist/commerce/runtime.js +0 -2
  21. package/dist/commerce/server.d.ts +0 -1
  22. package/dist/commerce/server.js +2 -3
  23. package/dist/commerce/setup.js +4 -3
  24. package/dist/commerce/types.d.ts +0 -59
  25. package/examples/envar-integration-policy.json +1 -9
  26. package/examples/seller.json +143 -26
  27. package/package.json +3 -16
  28. package/schemas/envarpay.config.schema.json +1 -80
  29. package/dist/commerce/buyer-store.d.ts +0 -101
  30. package/dist/commerce/buyer-store.js +0 -168
  31. package/dist/commerce/buyer.d.ts +0 -138
  32. package/dist/commerce/buyer.js +0 -775
  33. package/dist/commerce/chain.d.ts +0 -24
  34. package/dist/commerce/chain.js +0 -196
  35. package/dist/commerce/client-runtime.d.ts +0 -8
  36. package/dist/commerce/client-runtime.js +0 -7
  37. package/dist/commerce/management.d.ts +0 -14
  38. package/dist/commerce/management.js +0 -140
  39. package/dist/commerce/mpp-client.d.ts +0 -70
  40. package/dist/commerce/mpp-client.js +0 -29
  41. package/dist/commerce/mpp-runtime.d.ts +0 -4
  42. package/dist/commerce/mpp-runtime.js +0 -4
  43. package/dist/commerce/mpp.d.ts +0 -53
  44. package/dist/commerce/mpp.js +0 -163
  45. package/dist/commerce/peer-proxy.d.ts +0 -44
  46. package/dist/commerce/peer-proxy.js +0 -673
  47. package/dist/commerce/stripe-issuer.d.ts +0 -41
  48. package/dist/commerce/stripe-issuer.js +0 -351
  49. package/dist/commerce/stripe-provider.d.ts +0 -11
  50. package/dist/commerce/stripe-provider.js +0 -132
  51. package/dist/commerce/x402.d.ts +0 -38
  52. package/dist/commerce/x402.js +0 -181
  53. package/examples/buyer-policy.json +0 -18
  54. package/schemas/buyer-policy.schema.json +0 -253
@@ -1,41 +0,0 @@
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;
@@ -1,351 +0,0 @@
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
- }
@@ -1,11 +0,0 @@
1
- import type { StripeProvider } from './mpp.js';
2
- export interface StripeProviderConfig {
3
- secretKey: string;
4
- accountId: string;
5
- merchantProfile: string;
6
- mode: 'test' | 'live';
7
- }
8
- /** Public Stripe Business Profiles and Shared Payment Tokens contract. */
9
- export declare const STRIPE_MPP_API_VERSION = "2026-09-30.preview";
10
- /** Stripe REST transport used by the official mppx Stripe method. No SPT creation. */
11
- export declare function stripeProvider(config: StripeProviderConfig, fetchImpl?: typeof fetch): StripeProvider;
@@ -1,132 +0,0 @@
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';
5
- /** Stripe REST transport used by the official mppx Stripe method. No SPT creation. */
6
- export function stripeProvider(config, fetchImpl = fetch) {
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_'))) {
10
- throw new CommerceError('stripe_configuration', 'Explicit matching Stripe account, profile and test/live credentials are required');
11
- }
12
- const call = async (path, method = 'GET', params, options) => {
13
- const body = new URLSearchParams();
14
- const add = (key, value) => {
15
- if (value === undefined || value === null)
16
- return;
17
- if (Array.isArray(value)) {
18
- value.forEach((v, i) => add(`${key}[${i}]`, v));
19
- return;
20
- }
21
- if (typeof value === 'object') {
22
- for (const [k, v] of Object.entries(value))
23
- add(key ? `${key}[${k}]` : k, v);
24
- return;
25
- }
26
- if (!['boolean', 'number', 'string'].includes(typeof value))
27
- throw new CommerceError('stripe_request', 'Unsupported Stripe value');
28
- body.set(key, String(value));
29
- };
30
- for (const [key, value] of Object.entries(params ?? {}))
31
- add(key, value);
32
- const headers = new Headers({ Authorization: `Bearer ${config.secretKey}` });
33
- headers.set('Stripe-Version', STRIPE_MPP_API_VERSION);
34
- if (options?.idempotencyKey)
35
- headers.set('Idempotency-Key', options.idempotencyKey);
36
- if (method !== 'GET')
37
- headers.set('Content-Type', 'application/x-www-form-urlencoded');
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');
50
- const reader = response.body?.getReader();
51
- let size = 0;
52
- const chunks = [];
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);
65
- }
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
- }
72
- let result;
73
- try {
74
- result = JSON.parse(Buffer.concat(chunks).toString('utf8'));
75
- }
76
- catch {
77
- throw new CommerceError('stripe_response', 'Stripe returned invalid JSON');
78
- }
79
- // Do not surface provider error text that might echo SPT or customer details.
80
- if (!response.ok)
81
- throw new CommerceError(response.status === 429 ? 'stripe_rate_limit' : 'stripe_request_failed', `Stripe request failed (HTTP ${response.status})`);
82
- return { ...result, lastResponse: { headers: { 'idempotent-replayed': response.headers.get('idempotent-replayed') ?? 'false' } } };
83
- };
84
- const validateIntent = (value) => {
85
- if (typeof value.id !== 'string' || !/^pi_[A-Za-z0-9]+$/.test(value.id) || typeof value.status !== 'string' ||
86
- !Number.isSafeInteger(value.amount) || typeof value.livemode !== 'boolean' || !value.metadata || typeof value.metadata !== 'object') {
87
- throw new CommerceError('stripe_response', 'Stripe returned an invalid PaymentIntent');
88
- }
89
- return value;
90
- };
91
- return {
92
- accountId: config.accountId, merchantProfile: config.merchantProfile, mode: config.mode,
93
- async assertReady() {
94
- const account = await call('/v1/account');
95
- if (account.object !== 'account' || account.id !== config.accountId)
96
- throw new CommerceError('stripe_account_mismatch', 'Stripe key belongs to another account');
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));
116
- },
117
- async retrieve(id) {
118
- if (!/^pi_[A-Za-z0-9]+$/.test(id))
119
- throw new CommerceError('stripe_reference', 'Invalid original PaymentIntent');
120
- return validateIntent(await call(`/v1/payment_intents/${id}`));
121
- },
122
- async findOriginal(orderId) {
123
- if (!/^[0-9a-f-]{36}$/.test(orderId))
124
- throw new CommerceError('order_reference', 'Invalid original order');
125
- const result = await call('/v1/payment_intents/search', 'GET', { query: `metadata['envarpay_order']:'${orderId}'`, limit: 2 });
126
- if (!Array.isArray(result.data) || result.has_more || result.data.length > 1)
127
- throw new CommerceError('ambiguous_payment', 'Multiple intents require original-operation reconciliation');
128
- // Stripe search may be eventually consistent. No match remains unknown.
129
- return result.data.length ? validateIntent(result.data[0]) : undefined;
130
- },
131
- };
132
- }
@@ -1,38 +0,0 @@
1
- import { type FacilitatorClient } from '@x402/core/server';
2
- import type { PaymentPayload, PaymentRequirements, SettleResponse } from '@x402/core/types';
3
- import { type PaymentProfile } from './types.js';
4
- import { CommerceStore, type OrderRecord } from './store.js';
5
- import { CredentialVault } from './vault.js';
6
- type X402Profile = Extract<PaymentProfile, {
7
- adapter: 'x402';
8
- }>;
9
- export interface X402GateOptions {
10
- store: CommerceStore;
11
- vault: CredentialVault;
12
- payerFor: (caller: string) => string | undefined;
13
- facilitator?: (profile: X402Profile) => FacilitatorClient;
14
- verifyReceipt: (receipt: SettleResponse, payload: PaymentPayload, requirements: PaymentRequirements) => Promise<boolean>;
15
- /** Read chain before broadcast, so a lost facilitator response remains recoverable. */
16
- checkpoint?: (requirements: PaymentRequirements) => Promise<string>;
17
- findOriginalReceipt?: (payload: PaymentPayload, requirements: PaymentRequirements, fromBlock: string) => Promise<SettleResponse | undefined>;
18
- proveExpiredUnused?: (payload: PaymentPayload, requirements: PaymentRequirements) => Promise<boolean>;
19
- }
20
- export type GateResult = {
21
- response: Response;
22
- } | {
23
- headers: Record<string, string>;
24
- };
25
- /** Official x402 v2 exact settlement, with independent chain evidence before work. */
26
- export declare class X402Gate {
27
- private options;
28
- private servers;
29
- constructor(options: X402GateOptions);
30
- check(profiles: readonly X402Profile[]): Promise<void>;
31
- recover(orderId: string, caller: string): Promise<{
32
- state: string;
33
- taskId: string;
34
- }>;
35
- private resource;
36
- handle(request: Request, order: OrderRecord): Promise<GateResult>;
37
- }
38
- export {};