@peable.to/shared-types 0.10.0 → 0.12.0

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.
@@ -1,6 +1,9 @@
1
- import type { ConnectedAccount } from './settlement';
1
+ import type { Charge } from './charge';
2
+ import type { Customer } from './customer';
2
3
  import type { CurrencyCode } from './money';
3
4
  import type { NetworkType } from './network';
5
+ import type { PaymentMethod } from './paymentMethod';
6
+ import type { ConnectedAccount } from './settlement';
4
7
  /**
5
8
  * Which rail moves the money (ADR 0001 D1).
6
9
  *
@@ -11,65 +14,157 @@ import type { NetworkType } from './network';
11
14
  export type PaymentIntentRail = 'faircoin' | 'card';
12
15
  /** {@link PaymentIntentRail} as the tuple the database CHECK reads. */
13
16
  export declare const PAYMENT_INTENT_RAILS: readonly PaymentIntentRail[];
14
- export type PaymentIntentStatus = 'created' | 'awaiting_approval' | 'approved' | 'broadcast' | 'confirming' | 'requires_action' | 'processing' | 'settled' | 'refunded' | 'partially_refunded' | 'expired' | 'failed' | 'rejected';
15
17
  /**
16
- * Which statuses belong to which rail (ADR 0001 D5).
18
+ * Stripe's seven statuses, and no others (ADR 0019).
17
19
  *
18
- * The four chain states describe a transaction on a blockchain and the four
19
- * card states describe an authorization at a card acquirer; neither set is
20
- * expressible on the other rail. Mapping a card authorization onto `broadcast`
21
- * would be cheaper and would be a lie — nothing was broadcast, there is no
22
- * transaction, and `payment_intents_broadcast_requires_txid_check` would then
23
- * be demanding a txid from a payment that can never have one.
20
+ * The FairCoin rail walks four of them: it is created in `requires_action`
21
+ * (the payer must broadcast a transaction — `next_action.faircoin_payment`),
22
+ * moves to `processing` once a transaction is reported and while it confirms,
23
+ * and ends `succeeded` or `canceled`. A card walks all seven.
24
+ */
25
+ export type PaymentIntentStatus = 'requires_payment_method' | 'requires_confirmation' | 'requires_action' | 'processing' | 'requires_capture' | 'canceled' | 'succeeded';
26
+ /**
27
+ * The statuses only a CARD payment can reach (ADR 0019).
24
28
  *
25
- * `db/schema/payments.ts` renders these two sets into CHECK constraints, so the
26
- * restriction holds against a write that skipped the application.
29
+ * A FairCoin payment has no payment method to require (the payer's wallet signs
30
+ * on their own device), nothing to confirm server-side, and no authorization
31
+ * to capture — coins either arrive or they do not. `db/schema/payments.ts`
32
+ * renders this set into a CHECK, so the restriction holds against a write that
33
+ * skipped the application.
27
34
  */
28
- export declare const CHAIN_ONLY_STATUSES: readonly PaymentIntentStatus[];
29
35
  export declare const CARD_ONLY_STATUSES: readonly PaymentIntentStatus[];
30
36
  export declare function isValidStatusTransition(from: PaymentIntentStatus, to: PaymentIntentStatus): boolean;
31
- /** Every legal `PaymentIntentStatus`, derived from `ALLOWED`'s keys — the
32
- * single source of truth consumed by the Gateway's list-route status filter. */
37
+ /** Every `PaymentIntentStatus`, derived from `ALLOWED`'s keys. */
33
38
  export declare const PAYMENT_INTENT_STATUSES: readonly PaymentIntentStatus[];
39
+ /** The statuses that end a payment: nothing leaves them (bar the reorg exception). */
40
+ export declare const TERMINAL_STATUSES: readonly PaymentIntentStatus[];
34
41
  /**
35
42
  * Whether a payer can still complete a payment against an intent in this
36
- * status.
37
- *
38
- * `settled` answers `false`: it is where paying ENDS. `partially_refunded`
39
- * answers `false` too — money can still move, but not from the payer.
43
+ * status — every non-terminal one. Stated, not derived from reachability:
44
+ * `paymentIntent.test.ts` pins it against the table.
40
45
  */
41
46
  export declare function canStillBePaid(status: PaymentIntentStatus): boolean;
42
47
  /**
43
- * The statuses a payment reaches only AFTER the payer has finished.
48
+ * The statuses an intent may still EXPIRE from (ADR 0012 D9).
44
49
  *
45
- * Exported for the gate below rather than for production use: the property that
46
- * makes `PAYABLE_STATUSES` checkable is that a payable status reaches `settled`
47
- * without going through one of these.
50
+ * The gateway is waiting on a PERSON who has not paid: a FairCoin payer who has
51
+ * not broadcast (`requires_action`), or a card payer who has not completed a
52
+ * payment method or a challenge. Once a FairCoin payer has reported a
53
+ * transaction (`processing`) the intent NEVER expires, whatever `expires_at`
54
+ * says — coins are moving and the settlement watcher owns the row. An
55
+ * authorization held for capture (`requires_capture`) is the merchant's to
56
+ * capture or cancel and does not expire either.
57
+ *
58
+ * Expiry is a cancellation with `cancellation_reason: 'abandoned'`: a platform
59
+ * releases a reservation on `payment_intent.canceled`.
48
60
  */
49
- export declare const POST_SETTLEMENT_STATUSES: readonly PaymentIntentStatus[];
61
+ export declare const EXPIRABLE_STATUSES: readonly PaymentIntentStatus[];
62
+ /** Whether an intent in this status can still expire. See {@link EXPIRABLE_STATUSES}. */
63
+ export declare function canExpire(status: PaymentIntentStatus): boolean;
50
64
  /**
51
- * Whether `settled` is reachable from `status` without passing through a
52
- * post-settlement status. The derivation `canStillBePaid` used to BE, kept as
53
- * the thing that checks it rather than as the thing that answers it.
65
+ * Why an intent was cancelled — Stripe's reasons, plus `underpaid`.
66
+ *
67
+ * - `abandoned` — it expired: the payer never paid (the expiry sweeper).
68
+ * - `requested_by_customer` / `duplicate` / `fraudulent` — the merchant said so
69
+ * through `POST /v1/payment_intents/:id/cancel`.
70
+ * - `underpaid` — FairCoin only: a transaction arrived at the address and paid
71
+ * less than the amount. Peable never holds those coins, so it cannot ask for
72
+ * the difference on the same intent; `last_payment_error` says what happened.
73
+ * - `automatic` — the provider cancelled it on its own.
74
+ *
75
+ * `null` on a cancelled intent whose cause was never recorded (one cancelled
76
+ * without a reason, or migrated from the pre-0019 `rejected`).
54
77
  */
55
- export declare function reachesSettlementFromPayer(status: PaymentIntentStatus): boolean;
78
+ export type PaymentIntentCancellationReason = 'abandoned' | 'automatic' | 'duplicate' | 'fraudulent' | 'requested_by_customer' | 'underpaid';
79
+ export declare const PAYMENT_INTENT_CANCELLATION_REASONS: readonly PaymentIntentCancellationReason[];
80
+ /** The reasons a MERCHANT may give `POST /v1/payment_intents/:id/cancel`. */
81
+ export declare const MERCHANT_CANCELLATION_REASONS: readonly ["abandoned", "duplicate", "fraudulent", "requested_by_customer"];
82
+ /** When a card is captured: at confirmation, or by `POST …/capture` (card only). */
83
+ export type PaymentIntentCaptureMethod = 'automatic' | 'manual';
84
+ export declare const PAYMENT_INTENT_CAPTURE_METHODS: readonly PaymentIntentCaptureMethod[];
85
+ /** Card only: keep the payment method on the customer for later (ADR 0016). */
86
+ export type PaymentIntentSetupFutureUsage = 'on_session' | 'off_session';
87
+ export declare const PAYMENT_INTENT_SETUP_FUTURE_USAGES: readonly PaymentIntentSetupFutureUsage[];
88
+ /** The payment method types a rail accepts. One per rail today. */
89
+ export type PaymentMethodType = 'card' | 'faircoin';
90
+ /** Why the last attempt failed. Peable's words, never the provider's raw prose. */
91
+ export interface PaymentIntentLastPaymentError {
92
+ /** `card_error` for a declined card; `payment_error` for a FairCoin underpayment. */
93
+ type: 'card_error' | 'payment_error' | 'api_error';
94
+ code: string | null;
95
+ decline_code: string | null;
96
+ message: string;
97
+ }
98
+ /** A postal address — Stripe's shape. */
99
+ export interface Address {
100
+ line1: string | null;
101
+ line2: string | null;
102
+ city: string | null;
103
+ state: string | null;
104
+ postal_code: string | null;
105
+ /** ISO 3166-1 alpha-2. */
106
+ country: string | null;
107
+ }
108
+ export interface Shipping {
109
+ name: string;
110
+ phone: string | null;
111
+ address: Address;
112
+ carrier: string | null;
113
+ tracking_number: string | null;
114
+ }
115
+ /** An address as a request carries it: every field optional. */
116
+ export interface AddressParams {
117
+ line1?: string;
118
+ line2?: string;
119
+ city?: string;
120
+ state?: string;
121
+ postal_code?: string;
122
+ country?: string;
123
+ }
124
+ /** Shipping as a request carries it: only `name` and `address` are required. */
125
+ export interface ShippingParams {
126
+ name: string;
127
+ phone?: string;
128
+ address: AddressParams;
129
+ carrier?: string;
130
+ tracking_number?: string;
131
+ }
56
132
  /**
57
- * The statuses an intent may still EXPIRE from (ADR 0012 D9).
133
+ * What the payer must do next (`status: 'requires_action'`).
58
134
  *
59
- * Exactly those whose transition list contains `expired`: the gateway is
60
- * waiting on a PERSON who has not paid. Once a FairCoin payer has reported a
61
- * broadcast (`broadcast`), or the chain is confirming it (`confirming`), the
62
- * intent NEVER expires, whatever `expiresAt` says — coins are moving and the
63
- * settlement watcher owns the row. A platform must therefore never cancel an
64
- * order because `expiresAt` passed: it releases a reservation on
65
- * `payment_intent.expired` (or `rejected`), and on nothing else.
135
+ * - `faircoin_payment` — send exactly `amount` (base units) to `address` on
136
+ * `network`; `uri` is the BIP21-style `faircoin:` URI a wallet scans.
137
+ * - `use_peable_sdk` — a card challenge; `@peable.to/sdk/checkout` fetches the
138
+ * provider's confirmation credential through `POST …/client_action` and
139
+ * completes it. The credential is never on the intent itself.
140
+ */
141
+ export type PaymentIntentNextAction = {
142
+ type: 'faircoin_payment';
143
+ faircoin_payment: {
144
+ address: string;
145
+ network: NetworkType;
146
+ amount: string;
147
+ currency: 'FAIR';
148
+ uri: string;
149
+ };
150
+ } | {
151
+ type: 'use_peable_sdk';
152
+ };
153
+ /**
154
+ * The chain side of a FairCoin payment. `null` on a card payment.
66
155
  *
67
- * Stated rather than derived, for `PAYABLE_STATUSES`' reason; the gateway's
68
- * test suite pins it against the transition table.
156
+ * `txid` and `confirmations` describe the transaction the payer reported:
157
+ * `null`/`0` while the intent is `requires_action`, and the watcher's count
158
+ * against `required_confirmations` while it is `processing`.
69
159
  */
70
- export declare const EXPIRABLE_STATUSES: readonly PaymentIntentStatus[];
71
- /** Whether an intent in this status can still expire. See {@link EXPIRABLE_STATUSES}. */
72
- export declare function canExpire(status: PaymentIntentStatus): boolean;
160
+ export interface PaymentIntentFaircoin {
161
+ network: NetworkType;
162
+ /** Watch-only receive address derived per intent from the merchant's (or seller's) xpub. */
163
+ address: string;
164
+ txid: string | null;
165
+ confirmations: number;
166
+ required_confirmations: number;
167
+ }
73
168
  export interface PaymentIntent {
74
169
  id: string;
75
170
  object: 'payment_intent';
@@ -81,83 +176,119 @@ export interface PaymentIntent {
81
176
  status: PaymentIntentStatus;
82
177
  /** Which rail moves this payment. Decides which fields below are populated. */
83
178
  rail: PaymentIntentRail;
179
+ payment_method_types: PaymentMethodType[];
84
180
  /**
85
181
  * Amount in the currency's SMALLEST unit as a canonical integer string.
86
182
  * Never a float, and never interpretable without `currency`: m⊜ (10^-8 FAIR)
87
183
  * on the faircoin rail, cents on a fiat one.
88
184
  */
89
185
  amount: string;
186
+ /** What can still be captured: the authorized amount while `requires_capture`, else `"0"`. */
187
+ amount_capturable: string;
188
+ /** What was received: the captured amount once `succeeded`, else `"0"`. */
189
+ amount_received: string;
190
+ /** The sum of SUCCEEDED refunds on this payment. Refund state lives on the refunds. */
191
+ amount_refunded: string;
90
192
  currency: CurrencyCode;
91
- /** FairCoin rail only. `null` on a card payment, which has no network. */
92
- network: NetworkType | null;
93
- /**
94
- * Watch-only receive address derived per-intent from the merchant xpub.
95
- * FairCoin rail only; `null` on a card payment, which reserves no address.
96
- */
97
- address: string | null;
193
+ capture_method: PaymentIntentCaptureMethod;
194
+ setup_future_usage: PaymentIntentSetupFutureUsage | null;
195
+ /** `cus_…`, or the {@link Customer} with `expand[]=customer`. */
196
+ customer: string | Customer | null;
197
+ /** `pm_…`, or the {@link PaymentMethod} with `expand[]=payment_method`. Card only. */
198
+ payment_method: string | PaymentMethod | null;
199
+ /** `ch_…`, or the {@link Charge} with `expand[]=latest_charge`. Card only. */
200
+ latest_charge: string | Charge | null;
201
+ next_action: PaymentIntentNextAction | null;
202
+ last_payment_error: PaymentIntentLastPaymentError | null;
203
+ cancellation_reason: PaymentIntentCancellationReason | null;
204
+ canceled_at: number | null;
205
+ description: string | null;
206
+ receipt_email: string | null;
207
+ /** Card only. */
208
+ statement_descriptor_suffix: string | null;
209
+ shipping: Shipping | null;
210
+ /** The chain side. FairCoin rail only; `null` on a card payment. */
211
+ faircoin: PaymentIntentFaircoin | null;
212
+ /** The merchant's PUBLIC `merch_…`. */
98
213
  merchant_id: string;
99
214
  /**
100
215
  * The seller this payment is made ON BEHALF OF (`ca_…`), or `null` for a
101
216
  * payment to the merchant itself (ADR 0012 D5). On-behalf payments are
102
- * FairCoin only, and `address` derives from that seller's own key.
103
- *
104
- * The seller's `ca_…`, or the {@link ConnectedAccount} itself when the
105
- * request asked for `expand[]=connected_account`.
217
+ * FairCoin only, and `faircoin.address` derives from that seller's own key.
106
218
  */
107
219
  connected_account: string | ConnectedAccount | null;
108
- txid: string | null;
109
- /** On-chain confirmations. Structurally `0` for the whole life of a card payment. */
110
- confirmations: number;
111
220
  client_secret: string;
112
221
  metadata: Record<string, string>;
113
- /**
114
- * When an UNPAID intent expires. Meaningful only while {@link canExpire}
115
- * is true for `status` — see `canExpire` below.
116
- */
222
+ /** When an UNPAID intent expires. Meaningful only while `can_expire`. */
117
223
  expires_at: number;
118
- /**
119
- * Whether this intent can still expire (ADR 0012 D9). `false` once the payer
120
- * has reported a broadcast: a FairCoin payment that is `broadcast` or
121
- * `confirming` NEVER expires, so a platform must not cancel the order for it.
122
- *
123
- * Optional in the type only for fixtures written against earlier versions;
124
- * a gateway at this version always sends it.
125
- */
126
- can_expire?: boolean;
224
+ /** Whether this intent can still expire (ADR 0012 D9). */
225
+ can_expire: boolean;
127
226
  created: number;
128
227
  updated_at: number;
129
228
  }
229
+ /** `POST /v1/payment_intents`. */
130
230
  export interface CreatePaymentIntentParams {
131
231
  /** Amount in the currency's smallest unit, as a canonical integer string. */
132
232
  amount: string;
133
- /**
134
- * Defaults to `'faircoin'` — the rail this gateway shipped with, so every
135
- * integration written before ADR 0001 keeps working unchanged.
136
- */
233
+ /** Defaults to `'faircoin'`. */
137
234
  rail?: PaymentIntentRail;
138
235
  /** Required on the faircoin rail; refused on the card rail, which has none. */
139
236
  network?: NetworkType;
140
237
  /** Defaults to `'FAIR'` on the faircoin rail; required on the card rail. */
141
238
  currency?: CurrencyCode;
239
+ /** Must name the rail's own type: `['card']` or `['faircoin']`. */
240
+ payment_method_types?: PaymentMethodType[];
241
+ /** `cus_…`. */
242
+ customer?: string;
243
+ /** `pm_…`, card only. A saved payment method of `customer`. */
244
+ payment_method?: string;
245
+ /** Card only: confirm in the same call (needs `payment_method`). */
246
+ confirm?: boolean;
247
+ /** Card only, with `confirm`: the customer is not present (needs `customer`). */
248
+ off_session?: boolean;
249
+ /** Card only. */
250
+ setup_future_usage?: PaymentIntentSetupFutureUsage;
251
+ /** Card only. Defaults to `'automatic'`. */
252
+ capture_method?: PaymentIntentCaptureMethod;
253
+ receipt_email?: string;
254
+ description?: string;
255
+ /** Card only. */
256
+ statement_descriptor_suffix?: string;
257
+ shipping?: ShippingParams;
142
258
  metadata?: Record<string, string>;
143
259
  expires_in_seconds?: number;
144
- /**
145
- * Create the payment ON BEHALF OF this connected account (`ca_…`) — the
146
- * address derives from the seller's own key and the coins go straight to
147
- * their wallet (ADR 0012 D5). FairCoin only: combined with `rail: 'card'` it
148
- * is refused. The account's FairCoin half must be `active` and `network`
149
- * must be the account's network.
150
- */
260
+ /** FairCoin only: pay this seller (`ca_…`, ADR 0012 D5). */
151
261
  connected_account?: string;
262
+ /** Card only, with `confirm`: where a redirect-based challenge returns. */
263
+ return_url?: string;
152
264
  }
153
265
  /**
154
- * `POST /v1/payment_intents/:id`. Only the merchant's own `metadata` is
155
- * mutable on an intent: everything else is either the payment itself or the
156
- * state machine's, which moves only through its own operations.
157
- *
158
- * `metadata` is a PATCH: named keys are set, a key sent as `""` is removed,
159
- * keys not named are kept.
266
+ * `POST /v1/payment_intents/:id`. The merchant's own bookkeeping only —
267
+ * the amount, rail and status are the payment and move through their own
268
+ * operations. `metadata` is a PATCH (`""` unsets a key).
160
269
  */
161
270
  export interface UpdatePaymentIntentParams {
162
271
  metadata?: Record<string, string>;
272
+ description?: string;
273
+ receipt_email?: string;
274
+ shipping?: ShippingParams;
275
+ }
276
+ /** `POST /v1/payment_intents/:id/confirm` (card only). */
277
+ export interface ConfirmPaymentIntentParams {
278
+ /** `pm_…`; defaults to the intent's own. */
279
+ payment_method?: string;
280
+ off_session?: boolean;
281
+ setup_future_usage?: PaymentIntentSetupFutureUsage;
282
+ return_url?: string;
283
+ receipt_email?: string;
284
+ shipping?: ShippingParams;
285
+ }
286
+ /** `POST /v1/payment_intents/:id/capture` (card only, `requires_capture`). */
287
+ export interface CapturePaymentIntentParams {
288
+ /** Base units, at most the authorized amount; defaults to all of it. */
289
+ amount_to_capture?: string;
290
+ }
291
+ /** `POST /v1/payment_intents/:id/cancel`. */
292
+ export interface CancelPaymentIntentParams {
293
+ cancellation_reason?: (typeof MERCHANT_CANCELLATION_REASONS)[number];
163
294
  }
@@ -0,0 +1,33 @@
1
+ import type { CardDetails } from './charge';
2
+ import type { Customer } from './customer';
3
+ import type { Address, AddressParams } from './paymentIntent';
4
+ export interface BillingDetails {
5
+ address: Address | null;
6
+ email: string | null;
7
+ name: string | null;
8
+ phone: string | null;
9
+ }
10
+ export interface PaymentMethod {
11
+ id: string;
12
+ object: 'payment_method';
13
+ livemode: boolean;
14
+ type: 'card';
15
+ card: CardDetails;
16
+ billing_details: BillingDetails;
17
+ /** `cus_…`, or the {@link Customer} with `expand[]=customer`; `null` once detached. */
18
+ customer: string | Customer | null;
19
+ metadata: Record<string, string>;
20
+ created: number;
21
+ }
22
+ export interface UpdatePaymentMethodParams {
23
+ billing_details?: {
24
+ address?: AddressParams;
25
+ email?: string;
26
+ name?: string;
27
+ phone?: string;
28
+ };
29
+ metadata?: Record<string, string>;
30
+ }
31
+ export interface AttachPaymentMethodParams {
32
+ customer: string;
33
+ }
@@ -229,15 +229,6 @@ export interface Refund {
229
229
  amount: string;
230
230
  currency: CurrencyCode | string;
231
231
  status: RefundStatus;
232
- /**
233
- * Where the PAYMENT stands after this refund.
234
- *
235
- * On the refund deliberately: a caller that has just refunded needs to know
236
- * whether the payment is now `partially_refunded` or `refunded`, and making
237
- * them re-read the intent is a round trip whose answer can move on before it
238
- * arrives.
239
- */
240
- payment_status: string;
241
232
  failure_code: string | null;
242
233
  created: number;
243
234
  updated_at: number;
@@ -0,0 +1,39 @@
1
+ import type { Customer } from './customer';
2
+ import type { PaymentIntentLastPaymentError } from './paymentIntent';
3
+ import type { PaymentMethod } from './paymentMethod';
4
+ export type SetupIntentStatus = 'requires_payment_method' | 'requires_confirmation' | 'requires_action' | 'processing' | 'canceled' | 'succeeded';
5
+ export declare const SETUP_INTENT_STATUSES: readonly SetupIntentStatus[];
6
+ export type SetupIntentUsage = 'on_session' | 'off_session';
7
+ export type SetupIntentCancellationReason = 'abandoned' | 'duplicate' | 'requested_by_customer';
8
+ export interface SetupIntent {
9
+ id: string;
10
+ object: 'setup_intent';
11
+ livemode: boolean;
12
+ status: SetupIntentStatus;
13
+ usage: SetupIntentUsage;
14
+ payment_method_types: ['card'];
15
+ /** `cus_…`, or the {@link Customer} with `expand[]=customer`. */
16
+ customer: string | Customer | null;
17
+ /** The `pm_…` it saved, once `succeeded`. */
18
+ payment_method: string | PaymentMethod | null;
19
+ /** Possession of it lets the payer's browser confirm this SetupIntent. */
20
+ client_secret: string;
21
+ next_action: {
22
+ type: 'use_peable_sdk';
23
+ } | null;
24
+ last_setup_error: PaymentIntentLastPaymentError | null;
25
+ cancellation_reason: SetupIntentCancellationReason | null;
26
+ description: string | null;
27
+ metadata: Record<string, string>;
28
+ created: number;
29
+ }
30
+ export interface CreateSetupIntentParams {
31
+ customer?: string;
32
+ usage?: SetupIntentUsage;
33
+ payment_method_types?: ['card'];
34
+ description?: string;
35
+ metadata?: Record<string, string>;
36
+ }
37
+ export interface CancelSetupIntentParams {
38
+ cancellation_reason?: SetupIntentCancellationReason;
39
+ }
@@ -2,52 +2,35 @@ import type { WebhookEventType } from './event';
2
2
  /**
3
3
  * Where one webhook delivery stands (ADR 0001 D7).
4
4
  *
5
- * Was `'delivered' | 'failed'` — the two outcomes an inline, best-effort
6
- * delivery could report once it had already given up. Delivery is now a durable
7
- * outbox, so a delivery exists BEFORE any attempt and this set has to say so.
8
- *
9
- * - `pending` — enqueued and will be attempted. `nextAttemptAt` says when.
5
+ * - `pending` — will be attempted. `next_attempt_at` says when.
10
6
  * - `delivered` — terminal success.
11
- * - `failed` — the endpoint REFUSED it (a 4xx, a blocked address). No retry
7
+ * - `failed` — the endpoint REFUSED it (a 4xx, a blocked address), or the
8
+ * endpoint was disabled before it could be attempted. No retry
12
9
  * can fix that, so none is made.
13
- * - `dead` — every attempt failed transiently and the budget ran out. The
14
- * envelope is kept and can be redelivered by hand.
15
- *
16
- * ⚠️ Widening a published union is a breaking change for a consumer that
17
- * switches exhaustively on it. It is the honest shape: the previous one could
18
- * not describe a delivery that had not been attempted yet, which is now the
19
- * state every delivery starts in.
10
+ * - `dead` — every attempt failed transiently over three days and the
11
+ * budget ran out. It can be redelivered by hand.
20
12
  */
21
13
  export type WebhookDeliveryStatus = 'pending' | 'delivered' | 'failed' | 'dead';
14
+ /**
15
+ * One event sent to one webhook endpoint (ADR 0014 D4). An event that matches
16
+ * three endpoints is three deliveries, each with its own attempts.
17
+ */
22
18
  export interface WebhookDelivery {
23
19
  id: string;
24
20
  object: 'webhook_delivery';
25
- /**
26
- * Which of the provider's two worlds this object belongs to (ADR 0017):
27
- * `true` for a `production` merchant, `false` for every other environment.
28
- */
21
+ /** The mode of the merchant it was sent for (ADR 0017). */
29
22
  livemode: boolean;
30
- merchant_id: string;
31
- /**
32
- * The `pi_…` this delivery is about — `null` when the event is not about a
33
- * payment.
34
- *
35
- * Nullable since `connected_account.updated`, which is about a SELLER. The
36
- * alternative was to name an unrelated payment to keep the field a string,
37
- * which is worse than a null: a consumer correlating on it would attach a
38
- * seller's readiness change to a payment that has nothing to do with it.
39
- */
40
- intent_id: string | null;
23
+ /** The `evt_…` delivered — `GET /v1/events/:id` returns it. */
41
24
  event_id: string;
42
25
  event_type: WebhookEventType;
26
+ /** The `we_…` it was sent to. */
27
+ webhook_endpoint_id: string;
28
+ /** The URL actually POSTed to, as of the latest attempt. */
43
29
  url: string;
44
30
  attempts: number;
45
31
  delivered: boolean;
46
32
  last_status: WebhookDeliveryStatus;
47
- /**
48
- * Why the last attempt did not succeed — operator-facing, never a secret.
49
- * Absent while pending with no attempts, and after a success.
50
- */
33
+ /** Why the last attempt did not succeed — operator-facing, never a secret. */
51
34
  last_error?: string;
52
35
  /** When the next attempt is due, Unix seconds. Absent once the delivery is terminal. */
53
36
  next_attempt_at?: number;
@@ -0,0 +1,81 @@
1
+ import type { WebhookEventType } from './event';
2
+ /**
3
+ * A webhook endpoint (`we_…`) — one URL a merchant receives events at, with
4
+ * the event types it wants and the secret deliveries to it are signed with
5
+ * (ADR 0014 D4).
6
+ *
7
+ * A merchant may hold several; every event fans out to each ENABLED endpoint
8
+ * whose `enabled_events` matches it, as one delivery per endpoint.
9
+ */
10
+ /** `'*'` subscribes to every event type, including ones added later. */
11
+ export type WebhookEnabledEvent = WebhookEventType | '*';
12
+ export declare const WEBHOOK_ENDPOINT_STATUSES: readonly ["enabled", "disabled"];
13
+ export type WebhookEndpointStatus = (typeof WEBHOOK_ENDPOINT_STATUSES)[number];
14
+ /**
15
+ * Why an endpoint is `disabled`.
16
+ *
17
+ * - `disabled_by_merchant` — its owner set `disabled: true`.
18
+ * - `delivery_failures` — every delivery to it failed for three days
19
+ * straight, so Peable stopped sending. Re-enabling it (`disabled: false`)
20
+ * starts the clock again.
21
+ */
22
+ export declare const WEBHOOK_ENDPOINT_DISABLED_REASONS: readonly ["disabled_by_merchant", "delivery_failures"];
23
+ export type WebhookEndpointDisabledReason = (typeof WEBHOOK_ENDPOINT_DISABLED_REASONS)[number];
24
+ /** At most this many endpoints per merchant (per mode). */
25
+ export declare const WEBHOOK_ENDPOINTS_MAX_PER_MERCHANT = 16;
26
+ /** The longest a rolled secret may keep signing beside its replacement. */
27
+ export declare const WEBHOOK_SECRET_ROLL_MAX_EXPIRES_IN_SECONDS: number;
28
+ export interface WebhookEndpoint {
29
+ id: string;
30
+ object: 'webhook_endpoint';
31
+ /** The mode of the merchant that owns it (ADR 0017). */
32
+ livemode: boolean;
33
+ url: string;
34
+ description: string | null;
35
+ enabled_events: WebhookEnabledEvent[];
36
+ status: WebhookEndpointStatus;
37
+ /** `null` exactly when `status` is `enabled`. */
38
+ disabled_reason: WebhookEndpointDisabledReason | null;
39
+ /** The wire version events are rendered with for this endpoint. */
40
+ api_version: string;
41
+ metadata: Record<string, string>;
42
+ /**
43
+ * The signing secret (`whsec_…`). Present ONLY on the response that
44
+ * created it — the create, and a `roll_secret`. Peable never shows it again;
45
+ * a lost secret is rolled, not retrieved.
46
+ */
47
+ secret?: string;
48
+ created: number;
49
+ updated_at: number;
50
+ }
51
+ /** What `DELETE /v1/webhook_endpoints/:id` answers. */
52
+ export interface DeletedWebhookEndpoint {
53
+ id: string;
54
+ object: 'webhook_endpoint';
55
+ deleted: true;
56
+ }
57
+ export interface CreateWebhookEndpointParams {
58
+ /** An `https` URL (a test-mode endpoint may also use `http`), ports 80/443 only. */
59
+ url: string;
60
+ enabled_events: WebhookEnabledEvent[];
61
+ description?: string;
62
+ metadata?: Record<string, string>;
63
+ }
64
+ export interface UpdateWebhookEndpointParams {
65
+ url?: string;
66
+ enabled_events?: WebhookEnabledEvent[];
67
+ /** `true` stops deliveries; `false` re-enables an endpoint, whatever disabled it. */
68
+ disabled?: boolean;
69
+ /** `''` clears it. */
70
+ description?: string;
71
+ /** Merged: named keys are set, a key sent as `''` is removed. */
72
+ metadata?: Record<string, string>;
73
+ }
74
+ export interface RollWebhookEndpointSecretParams {
75
+ /**
76
+ * How long the PREVIOUS secret keeps signing beside the new one, in seconds
77
+ * (0–86400, default 0). While both are live every delivery carries two
78
+ * `v1=` signatures, so a receiver can switch secrets without a gap.
79
+ */
80
+ expires_in_seconds?: number;
81
+ }
@@ -1,12 +1,20 @@
1
1
  /**
2
2
  * Produce a Stripe-style signature header for an outbound webhook:
3
- * `t=${timestamp},v1=${hexHmacSha256}`.
3
+ * `t=${timestamp},v1=${hex}` — with one `v1=` per secret.
4
+ *
5
+ * Several secrets are what make a rotation gapless: while an endpoint's rolled
6
+ * secret is still inside its overlap window, every delivery is signed with
7
+ * BOTH, so a receiver verifying with either one accepts it.
4
8
  */
5
- export declare function signWebhook(secret: string, rawBody: string, timestamp: number): string;
9
+ export declare function signWebhook(secret: string | readonly string[], rawBody: string, timestamp: number): string;
6
10
  /**
7
11
  * Verify a webhook signature header against the raw body.
8
- * - Parses `t` and `v1` from the header.
12
+ *
13
+ * - Parses `t` and EVERY `v1` from the header.
9
14
  * - Rejects if the timestamp drifts more than `toleranceSec` from `nowSec`.
10
- * - Recomputes the expected signature and compares it in constant time.
15
+ * - Accepts when any `v1` matches the signature computed with any of
16
+ * `secret` — so a receiver mid-rotation can pass both of its secrets, and a
17
+ * delivery signed during the sender's overlap window verifies against
18
+ * either.
11
19
  */
12
- export declare function verifyWebhook(secret: string, rawBody: string, header: string, toleranceSec: number, nowSec: number): boolean;
20
+ export declare function verifyWebhook(secret: string | readonly string[], rawBody: string, header: string, toleranceSec: number, nowSec: number): boolean;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@peable.to/shared-types",
3
- "version": "0.10.0",
3
+ "version": "0.12.0",
4
4
  "description": "Shared TypeScript types for the Peable Gateway — the wire contract shared by the backend, SDK, and frontend.",
5
5
  "main": "dist/cjs/index.js",
6
6
  "module": "dist/esm/index.js",