@stacksjs/payments 0.75.169 → 0.75.170

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/index.d.ts CHANGED
@@ -27,3 +27,6 @@ export { subscriptionGrantsAccess, subscriptionPeriod } from './subscription-sta
27
27
  // Bank debits (SEPA Direct Debit, Bacs, ACH, BECS): methods per currency,
28
28
  // debit webhook outcomes and mandates. Pure; also `@stacksjs/payments/direct-debit`.
29
29
  export * from './direct-debit';
30
+ // Card-present payments with Stripe Terminal: locations, readers, charges on a
31
+ // reader, refunds to the card. Also `@stacksjs/payments/terminal`.
32
+ export * from './terminal';
package/dist/index.js CHANGED
@@ -1 +1 @@
1
- export*from"./payment";export{default as Payment}from"./payment";export*from"./driver";export*from"./billable";export*from"./connect";export*from"./drivers/stripe";export{freshIdempotencyKey,stacksIdempotencyKey}from"./idempotency";export{subscriptionGrantsAccess,subscriptionPeriod}from"./subscription-state";export*from"./direct-debit";
1
+ export*from"./payment";export{default as Payment}from"./payment";export*from"./driver";export*from"./billable";export*from"./connect";export*from"./drivers/stripe";export{freshIdempotencyKey,stacksIdempotencyKey}from"./idempotency";export{subscriptionGrantsAccess,subscriptionPeriod}from"./subscription-state";export*from"./direct-debit";export*from"./terminal";
@@ -0,0 +1,163 @@
1
+ /**
2
+ * Stripe's form encoding: nested objects as `a[b]=c`, arrays as `a[0]=x`,
3
+ * booleans as `true`/`false`; null and undefined are left out.
4
+ */
5
+ export declare function stripeFormEncode(params: Record<string, FormValue>): string;
6
+ /** A minimal Stripe REST client: one call, Stripe's errors as StripeApiError. */
7
+ export declare function stripeRest(options: StripeRestOptions): StripeRest;
8
+ /** The Terminal calls a till makes, against Stripe or a double of it (`apiBase`). */
9
+ export declare function stripeTerminal(options: StripeRestOptions): StripeTerminal;
10
+ /**
11
+ * Reads a charge's state the way Stripe's server-driven guide does: the
12
+ * PaymentIntent decides success and cancellation; while it waits, the
13
+ * reader's action for this PaymentIntent says whether the card was declined
14
+ * or the reader gave up. A failed attempt leaves the PaymentIntent
15
+ * `requires_payment_method`, so the same charge can go to the reader again.
16
+ */
17
+ export declare function terminalChargeState(paymentIntent: { id?: string, status?: string, last_payment_error?: { code?: string, decline_code?: string, message?: string } | null } | null, reader?: Pick<TerminalReader, 'action'> | null): TerminalChargeOutcome;
18
+ /** The card details of a charge (or of a PaymentIntent with `latest_charge` expanded). */
19
+ export declare function cardPresentDetails(chargeOrIntent: any): CardPresentDetails | null;
20
+ /** The charge id behind a PaymentIntent (`latest_charge`, expanded or not). */
21
+ export declare function latestChargeId(paymentIntent: any): string | null;
22
+ /** Whether a secret key is a test-mode key (simulated readers, test cards). */
23
+ export declare function isTestKey(secretKey: string | null | undefined): boolean;
24
+ /**
25
+ * The API version these calls are written for. Pinned, so an account's
26
+ * default version cannot change the shapes: on it a card-present
27
+ * PaymentIntent names its method with `allowed_payment_method_types` (the
28
+ * older `payment_method_types` is refused since 2026-09-30.endive).
29
+ */
30
+ export declare const STRIPE_TERMINAL_API_VERSION: '2026-09-30.endive';
31
+ declare const path: (value: string) => string;
32
+ /** Registration codes Stripe accepts for a simulated reader in test mode. */
33
+ export declare const SIMULATED_READER_CODES: readonly ['simulated-wpe', 'simulated-s700', 'simulated-s710', 'simulated-v660p', 'simulated-wp3'];
34
+ export declare interface StripeRestOptions {
35
+ secretKey: string
36
+ apiBase?: string
37
+ apiVersion?: string | null
38
+ stripeAccount?: string
39
+ fetch?: typeof fetch
40
+ timeoutMs?: number
41
+ }
42
+ export declare interface StripeRequestOptions {
43
+ idempotencyKey?: string
44
+ }
45
+ export declare interface StripeRest {
46
+ request: <T = any>(method: 'GET' | 'POST' | 'DELETE', path: string, params?: Record<string, FormValue>, options?: StripeRequestOptions) => Promise<T>
47
+ }
48
+ export declare interface TerminalAddress {
49
+ line1: string
50
+ line2?: string | null
51
+ city?: string | null
52
+ postal_code?: string | null
53
+ state?: string | null
54
+ country: string
55
+ }
56
+ export declare interface TerminalLocation {
57
+ id: string
58
+ object: 'terminal.location'
59
+ display_name: string
60
+ address: TerminalAddress
61
+ metadata: Record<string, string>
62
+ livemode: boolean
63
+ }
64
+ export declare interface ReaderAction {
65
+ type: 'process_payment_intent' | 'process_setup_intent' | 'refund_payment' | 'set_reader_display' | string
66
+ status: 'in_progress' | 'succeeded' | 'failed'
67
+ failure_code: string | null
68
+ failure_message: string | null
69
+ process_payment_intent?: { payment_intent: string } | null
70
+ }
71
+ export declare interface TerminalReader {
72
+ id: string
73
+ object: 'terminal.reader'
74
+ label: string
75
+ device_type: string
76
+ serial_number: string
77
+ status: 'online' | 'offline' | null
78
+ location: string | null
79
+ ip_address: string | null
80
+ device_sw_version: string | null
81
+ action: ReaderAction | null
82
+ metadata: Record<string, string>
83
+ livemode: boolean
84
+ }
85
+ export declare interface CardPresentPaymentIntentInput {
86
+ amount: number
87
+ currency: string
88
+ description?: string
89
+ metadata?: Record<string, string>
90
+ onBehalfOf?: string
91
+ transferDestination?: string
92
+ captureMethod?: 'automatic' | 'manual'
93
+ statementDescriptorSuffix?: string
94
+ }
95
+ export declare interface StripeTerminal {
96
+ connectionToken: (location?: string) => Promise<{ object: 'terminal.connection_token', secret: string, location?: string }>
97
+ createLocation: (input: { displayName: string, address: TerminalAddress, metadata?: Record<string, string> }, idempotencyKey?: string) => Promise<TerminalLocation>
98
+ retrieveLocation: (id: string) => Promise<TerminalLocation>
99
+ registerReader: (input: { registrationCode: string, label?: string, location: string, metadata?: Record<string, string> }) => Promise<TerminalReader>
100
+ retrieveReader: (id: string) => Promise<TerminalReader>
101
+ listReaders: (filter?: { location?: string, limit?: number }) => Promise<TerminalReader[]>
102
+ deleteReader: (id: string) => Promise<{ id: string, deleted: boolean }>
103
+ createPaymentIntent: (input: CardPresentPaymentIntentInput, idempotencyKey: string) => Promise<any>
104
+ retrievePaymentIntent: (id: string) => Promise<any>
105
+ cancelPaymentIntent: (id: string) => Promise<any>
106
+ processPaymentIntent: (readerId: string, paymentIntentId: string, idempotencyKey?: string) => Promise<TerminalReader>
107
+ cancelReaderAction: (readerId: string) => Promise<TerminalReader>
108
+ presentPaymentMethod: (readerId: string, card?: { number?: string, type?: 'card_present' | 'interac_present' }) => Promise<TerminalReader>
109
+ refund: (input: { paymentIntent: string, amount?: number, reverseTransfer?: boolean, metadata?: Record<string, string>, reason?: 'duplicate' | 'fraudulent' | 'requested_by_customer' }, idempotencyKey: string) => Promise<any>
110
+ }
111
+ export declare interface TerminalChargeOutcome {
112
+ state: TerminalChargeState
113
+ failureCode: string | null
114
+ failureMessage: string | null
115
+ }
116
+ /** What a card receipt prints about the card, from a card-present charge. */
117
+ export declare interface CardPresentDetails {
118
+ brand: string | null
119
+ last4: string | null
120
+ funding: string | null
121
+ readMethod: string | null
122
+ applicationName: string | null
123
+ applicationId: string | null
124
+ authorizationCode: string | null
125
+ authorizationResponseCode: string | null
126
+ accountType: string | null
127
+ cardholderVerification: string | null
128
+ }
129
+ export type FormValue = string | number | boolean | null | undefined | FormValue[] | { [key: string]: FormValue }
130
+ /** Where a card-present charge stands, from the PaymentIntent and the reader that has it. */
131
+ export type TerminalChargeState = 'waiting' | 'succeeded' | 'failed' | 'canceled';
132
+ /**
133
+ * Card-present payments through Stripe Terminal, the server-driven way: the
134
+ * till asks the server to charge, the server tells a smart reader (WisePOS E,
135
+ * S700, or a simulated one) to collect the card, and the till asks the server
136
+ * how it went. Also `@stacksjs/payments/terminal`.
137
+ *
138
+ * - A Terminal **location** groups readers (one per shop or gym location).
139
+ * - A **reader** is registered with the code it shows on screen and a label.
140
+ * - A charge is a PaymentIntent with `payment_method_types: ['card_present']`,
141
+ * handed to a reader with `process_payment_intent`. The reader's `action`
142
+ * says whether it is waiting for the card, done, or failed (a decline, a
143
+ * timeout); `cancel_action` takes the charge off its screen.
144
+ * - A refund to the card is an ordinary refund of that PaymentIntent (not
145
+ * Interac, which needs the card back at the reader).
146
+ * - A connection token is for the client SDKs (Tap to Pay, Bluetooth readers).
147
+ *
148
+ * The client talks to Stripe's REST API directly (form-encoded requests, JSON
149
+ * answers, `Idempotency-Key` on every create) instead of through the `stripe`
150
+ * SDK: it needs no optional dependency, and `apiBase` lets a test point it at
151
+ * an HTTP double of Stripe. The pure helpers (`terminalChargeState`,
152
+ * `cardPresentDetails`) read Stripe's objects without any client.
153
+ */
154
+ /** Stripe's error object (`{ error: { type, code, decline_code, message, param } }`). */
155
+ export declare class StripeApiError extends Error {
156
+ readonly type: string;
157
+ readonly code: string | null;
158
+ readonly declineCode: string | null;
159
+ readonly param: string | null;
160
+ readonly status: number;
161
+ constructor(message: string, status: number, error?: { type?: string, code?: string, decline_code?: string, param?: string });
162
+ get definitive(): boolean;
163
+ }
@@ -0,0 +1 @@
1
+ export class StripeApiError extends Error{status;type;code;declineCode;param;constructor(message,status,error={}){super(message);this.status=status;this.name="StripeApiError";this.type=error.type||(status===0?"api_connection_error":"api_error");this.code=error.code??null;this.declineCode=error.decline_code??null;this.param=error.param??null}get definitive(){return["invalid_request_error","card_error","authentication_error","permission_error"].includes(this.type)}}export const STRIPE_TERMINAL_API_VERSION="2026-09-30.endive";export function stripeFormEncode(params){const pairs=[],add=(key,value)=>{if(value===null||value===void 0)return;if(Array.isArray(value)){value.forEach((item,index)=>add(`${key}[${index}]`,item));return}if(typeof value==="object"){for(const[child,inner]of Object.entries(value))add(`${key}[${child}]`,inner);return}pairs.push(`${encodeURIComponent(key)}=${encodeURIComponent(String(value))}`)};for(const[key,value]of Object.entries(params))add(key,value);return pairs.join("&")}export function stripeRest(options){const base=(options.apiBase||"https://api.stripe.com").replace(/\/$/,""),doFetch=options.fetch??fetch;return{async request(method,path,params={},requestOptions={}){const body=stripeFormEncode(params),url=method==="POST"||!body?`${base}${path}`:`${base}${path}?${body}`,headers={Authorization:`Bearer ${options.secretKey}`,Accept:"application/json"};if(method==="POST")headers["Content-Type"]="application/x-www-form-urlencoded";const version=options.apiVersion===void 0?STRIPE_TERMINAL_API_VERSION:options.apiVersion;if(version)headers["Stripe-Version"]=version;if(options.stripeAccount)headers["Stripe-Account"]=options.stripeAccount;if(requestOptions.idempotencyKey)headers["Idempotency-Key"]=requestOptions.idempotencyKey;let res;try{res=await doFetch(url,{method,headers,body:method==="POST"?body:void 0,signal:AbortSignal.timeout(options.timeoutMs??30000)})}catch(error){throw new StripeApiError(`Could not reach Stripe: ${error instanceof Error?error.message:String(error)}`,0)}const text=await res.text();let json=null;try{json=text?JSON.parse(text):null}catch{json=null}if(!res.ok){const error=json?.error&&typeof json.error==="object"?json.error:{};throw new StripeApiError(typeof error.message==="string"&&error.message?error.message:`Stripe answered ${res.status}`,res.status,error)}return json}}}const path=(value)=>encodeURIComponent(value);export function stripeTerminal(options){const api=stripeRest(options);return{connectionToken:(location)=>api.request("POST","/v1/terminal/connection_tokens",{location}),createLocation:(input,idempotencyKey)=>api.request("POST","/v1/terminal/locations",{display_name:input.displayName,address:{...input.address},metadata:input.metadata},{idempotencyKey}),retrieveLocation:(id)=>api.request("GET",`/v1/terminal/locations/${path(id)}`),registerReader:(input)=>api.request("POST","/v1/terminal/readers",{registration_code:input.registrationCode,label:input.label,location:input.location,metadata:input.metadata}),retrieveReader:(id)=>api.request("GET",`/v1/terminal/readers/${path(id)}`),async listReaders(filter={}){const page=await api.request("GET","/v1/terminal/readers",{location:filter.location,limit:filter.limit??100});return Array.isArray(page?.data)?page.data:[]},deleteReader:(id)=>api.request("DELETE",`/v1/terminal/readers/${path(id)}`),createPaymentIntent:(input,idempotencyKey)=>api.request("POST","/v1/payment_intents",{amount:input.amount,currency:input.currency.toLowerCase(),allowed_payment_method_types:["card_present"],capture_method:input.captureMethod??"automatic",description:input.description,metadata:input.metadata,on_behalf_of:input.onBehalfOf,transfer_data:input.transferDestination?{destination:input.transferDestination}:void 0,statement_descriptor_suffix:input.statementDescriptorSuffix},{idempotencyKey}),retrievePaymentIntent:(id)=>api.request("GET",`/v1/payment_intents/${path(id)}`,{expand:["latest_charge"]}),cancelPaymentIntent:(id)=>api.request("POST",`/v1/payment_intents/${path(id)}/cancel`),processPaymentIntent:(readerId,paymentIntentId,idempotencyKey)=>api.request("POST",`/v1/terminal/readers/${path(readerId)}/process_payment_intent`,{payment_intent:paymentIntentId},{idempotencyKey}),cancelReaderAction:(readerId)=>api.request("POST",`/v1/terminal/readers/${path(readerId)}/cancel_action`),presentPaymentMethod:(readerId,card={})=>api.request("POST",`/v1/test_helpers/terminal/readers/${path(readerId)}/present_payment_method`,{type:card.type,card_present:card.number?{number:card.number}:void 0}),refund:(input,idempotencyKey)=>api.request("POST","/v1/refunds",{payment_intent:input.paymentIntent,amount:input.amount,reverse_transfer:input.reverseTransfer,reason:input.reason,metadata:input.metadata},{idempotencyKey})}}export function terminalChargeState(paymentIntent,reader){const status=paymentIntent?.status;if(status==="succeeded"||status==="requires_capture")return{state:"succeeded",failureCode:null,failureMessage:null};if(status==="canceled")return{state:"canceled",failureCode:null,failureMessage:null};const action=reader?.action,forThis=!!action&&action.type==="process_payment_intent"&&(!paymentIntent?.id||action.process_payment_intent?.payment_intent===paymentIntent.id);if(forThis&&action.status==="failed")return{state:"failed",failureCode:action.failure_code||"reader_action_failed",failureMessage:action.failure_message||null};if(forThis&&action.status==="in_progress")return{state:"waiting",failureCode:null,failureMessage:null};const error=paymentIntent?.last_payment_error;if(status==="requires_payment_method"&&error)return{state:"failed",failureCode:error.decline_code||error.code||"card_declined",failureMessage:error.message||null};return{state:"waiting",failureCode:null,failureMessage:null}}export function cardPresentDetails(chargeOrIntent){const charge=chargeOrIntent?.object==="payment_intent"?typeof chargeOrIntent.latest_charge==="object"&&chargeOrIntent.latest_charge?chargeOrIntent.latest_charge:chargeOrIntent.charges?.data?.[0]:chargeOrIntent,card=charge?.payment_method_details?.card_present??charge?.payment_method_details?.interac_present;if(!card)return null;const receipt=card.receipt??{},text=(value)=>typeof value==="string"&&value?value:null;return{brand:text(card.brand),last4:text(card.last4),funding:text(card.funding),readMethod:text(card.read_method),applicationName:text(receipt.application_preferred_name),applicationId:text(receipt.dedicated_file_name),authorizationCode:text(charge.authorization_code??receipt.authorization_code),authorizationResponseCode:text(receipt.authorization_response_code),accountType:text(receipt.account_type),cardholderVerification:text(receipt.cardholder_verification_method)}}export function latestChargeId(paymentIntent){const charge=paymentIntent?.latest_charge;if(typeof charge==="string"&&charge)return charge;if(charge&&typeof charge.id==="string")return charge.id;const legacy=paymentIntent?.charges?.data?.[0]?.id;return typeof legacy==="string"?legacy:null}export const SIMULATED_READER_CODES=["simulated-wpe","simulated-s700","simulated-s710","simulated-v660p","simulated-wp3"];export function isTestKey(secretKey){return typeof secretKey==="string"&&/^(sk|rk)_test_/.test(secretKey)}
package/package.json CHANGED
@@ -2,7 +2,7 @@
2
2
  "name": "@stacksjs/payments",
3
3
  "type": "module",
4
4
  "sideEffects": false,
5
- "version": "0.75.169",
5
+ "version": "0.75.170",
6
6
  "description": "The Stacks payments package. Currently supporting Stripe.",
7
7
  "author": "Chris Breuer",
8
8
  "contributors": [
@@ -60,12 +60,12 @@
60
60
  "prepublishOnly": "bun run build"
61
61
  },
62
62
  "dependencies": {
63
- "@stacksjs/config": "0.75.169",
64
- "@stacksjs/database": "0.75.169",
65
- "@stacksjs/error-handling": "0.75.169",
66
- "@stacksjs/logging": "0.75.169",
67
- "@stacksjs/orm": "0.75.169",
68
- "@stacksjs/types": "0.75.169"
63
+ "@stacksjs/config": "0.75.170",
64
+ "@stacksjs/database": "0.75.170",
65
+ "@stacksjs/error-handling": "0.75.170",
66
+ "@stacksjs/logging": "0.75.170",
67
+ "@stacksjs/orm": "0.75.170",
68
+ "@stacksjs/types": "0.75.170"
69
69
  },
70
70
  "peerDependencies": {
71
71
  "@stripe/stripe-js": "^10.0.0",
@@ -80,7 +80,7 @@
80
80
  }
81
81
  },
82
82
  "devDependencies": {
83
- "@stacksjs/utils": "0.75.169",
83
+ "@stacksjs/utils": "0.75.170",
84
84
  "@stripe/stripe-js": "^10.0.0",
85
85
  "better-dx": "^0.2.24",
86
86
  "stripe": "23.0.0"