@peable.to/shared-types 0.10.0 → 0.11.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.
- package/dist/cjs/charge.js +2 -0
- package/dist/cjs/customer.js +2 -0
- package/dist/cjs/event.js +7 -0
- package/dist/cjs/index.js +19 -8
- package/dist/cjs/paymentIntent.js +90 -188
- package/dist/cjs/paymentMethod.js +2 -0
- package/dist/cjs/setupIntent.js +11 -0
- package/dist/cjs/webhookEndpoint.js +20 -0
- package/dist/cjs/webhookSigner.js +35 -12
- package/dist/esm/charge.js +1 -0
- package/dist/esm/customer.js +1 -0
- package/dist/esm/event.js +6 -1
- package/dist/esm/index.js +4 -1
- package/dist/esm/paymentIntent.js +89 -186
- package/dist/esm/paymentMethod.js +1 -0
- package/dist/esm/setupIntent.js +8 -0
- package/dist/esm/webhookEndpoint.js +17 -0
- package/dist/esm/webhookSigner.js +35 -12
- package/dist/types/charge.d.ts +56 -0
- package/dist/types/customer.d.ts +39 -0
- package/dist/types/dispute.d.ts +2 -2
- package/dist/types/event.d.ts +99 -89
- package/dist/types/index.d.ts +7 -2
- package/dist/types/merchant.d.ts +0 -1
- package/dist/types/paymentIntent.d.ts +215 -84
- package/dist/types/paymentMethod.d.ts +33 -0
- package/dist/types/settlement.d.ts +0 -9
- package/dist/types/setupIntent.d.ts +39 -0
- package/dist/types/webhookDelivery.d.ts +15 -32
- package/dist/types/webhookEndpoint.d.ts +81 -0
- package/dist/types/webhookSigner.d.ts +13 -5
- package/package.json +1 -1
|
@@ -1,224 +1,127 @@
|
|
|
1
1
|
/** {@link PaymentIntentRail} as the tuple the database CHECK reads. */
|
|
2
2
|
export const PAYMENT_INTENT_RAILS = ['faircoin', 'card'];
|
|
3
3
|
/**
|
|
4
|
-
*
|
|
4
|
+
* The statuses only a CARD payment can reach (ADR 0019).
|
|
5
5
|
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
11
|
-
* be demanding a txid from a payment that can never have one.
|
|
12
|
-
*
|
|
13
|
-
* `db/schema/payments.ts` renders these two sets into CHECK constraints, so the
|
|
14
|
-
* restriction holds against a write that skipped the application.
|
|
6
|
+
* A FairCoin payment has no payment method to require (the payer's wallet signs
|
|
7
|
+
* on their own device), nothing to confirm server-side, and no authorization
|
|
8
|
+
* to capture — coins either arrive or they do not. `db/schema/payments.ts`
|
|
9
|
+
* renders this set into a CHECK, so the restriction holds against a write that
|
|
10
|
+
* skipped the application.
|
|
15
11
|
*/
|
|
16
|
-
export const CHAIN_ONLY_STATUSES = [
|
|
17
|
-
'awaiting_approval',
|
|
18
|
-
'approved',
|
|
19
|
-
'broadcast',
|
|
20
|
-
'confirming',
|
|
21
|
-
];
|
|
22
12
|
export const CARD_ONLY_STATUSES = [
|
|
23
|
-
'
|
|
24
|
-
'
|
|
25
|
-
'
|
|
26
|
-
'partially_refunded',
|
|
13
|
+
'requires_payment_method',
|
|
14
|
+
'requires_confirmation',
|
|
15
|
+
'requires_capture',
|
|
27
16
|
];
|
|
28
17
|
/**
|
|
29
|
-
* Legal
|
|
30
|
-
* (refunded/expired/failed/rejected) allow no further transition here — the one
|
|
31
|
-
* documented exception, a reorg dropping `settled` back to `confirming`, is
|
|
32
|
-
* modelled by the backend state machine, not this happy-path table.
|
|
18
|
+
* Legal transitions of the PaymentIntent lifecycle.
|
|
33
19
|
*
|
|
34
|
-
* ONE table for both rails
|
|
35
|
-
*
|
|
36
|
-
*
|
|
37
|
-
*
|
|
20
|
+
* ONE table for both rails. It answers "is this a legal lifecycle edge at all";
|
|
21
|
+
* WHICH rail may reach a given status is the separate, structural question
|
|
22
|
+
* {@link CARD_ONLY_STATUSES} answers in the database, and which EVENT may walk
|
|
23
|
+
* an edge is `LEGAL_SOURCES` in the backend's `services/intentState.ts`.
|
|
38
24
|
*
|
|
39
25
|
* Self-transitions are absent because `applyEvent` short-circuits on
|
|
40
|
-
* `current === target`
|
|
41
|
-
*
|
|
42
|
-
*
|
|
26
|
+
* `current === target` — the idempotent re-poll of the settlement watcher.
|
|
27
|
+
*
|
|
28
|
+
* `succeeded` has no outgoing edge here. Its one exception — a chain reorg
|
|
29
|
+
* dropping a FairCoin payment back below its confirmation threshold,
|
|
30
|
+
* `succeeded → processing` — is modelled by the backend state machine, not this
|
|
31
|
+
* table. Refunds are not statuses: money coming back is `amount_refunded` and
|
|
32
|
+
* the refunds themselves (ADR 0019).
|
|
43
33
|
*/
|
|
44
34
|
const ALLOWED = {
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
'
|
|
55
|
-
'broadcast',
|
|
35
|
+
requires_payment_method: [
|
|
36
|
+
'requires_confirmation',
|
|
37
|
+
'requires_action',
|
|
38
|
+
'processing',
|
|
39
|
+
'requires_capture',
|
|
40
|
+
'succeeded',
|
|
41
|
+
'canceled',
|
|
42
|
+
],
|
|
43
|
+
requires_confirmation: [
|
|
44
|
+
'requires_payment_method',
|
|
56
45
|
'requires_action',
|
|
57
46
|
'processing',
|
|
58
|
-
'
|
|
59
|
-
'
|
|
60
|
-
'
|
|
61
|
-
'expired',
|
|
47
|
+
'requires_capture',
|
|
48
|
+
'succeeded',
|
|
49
|
+
'canceled',
|
|
62
50
|
],
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
* had been declined.
|
|
79
|
-
*
|
|
80
|
-
* On the chain rail `failed` means `underpaid`, which IS terminal — coins
|
|
81
|
-
* arrived and were not enough. Nothing here opens that door: `underpaid` is
|
|
82
|
-
* emitted only by the settlement watcher, and `LEGAL_SOURCES` in
|
|
83
|
-
* `services/intentState.ts` keeps every `card_*` event to card statuses and
|
|
84
|
-
* the chain's `confirmed` to `confirming`. This table answers "is the edge
|
|
85
|
-
* legal at all"; which rail may walk it is the separate question those two
|
|
86
|
-
* guards answer.
|
|
87
|
-
*/
|
|
88
|
-
failed: ['requires_action', 'processing', 'settled'],
|
|
89
|
-
// Money coming back requires money to have arrived, so both refund states are
|
|
90
|
-
// reachable only from `settled`.
|
|
91
|
-
settled: ['refunded', 'partially_refunded'],
|
|
92
|
-
/**
|
|
93
|
-
* ...and money coming back can be UNDONE, which is why these edges run both
|
|
94
|
-
* ways.
|
|
95
|
-
*
|
|
96
|
-
* A refund is not final when the provider accepts it: a bank can reject one
|
|
97
|
-
* days later, and the provider then says so (`refund.failed`). Without a path
|
|
98
|
-
* back, a payment whose only refund failed would claim `refunded` forever —
|
|
99
|
-
* the merchant's books saying money went back that is still with them, and no
|
|
100
|
-
* transition anywhere able to correct it.
|
|
101
|
-
*
|
|
102
|
-
* The target is always recomputed from the SUM of succeeded refunds
|
|
103
|
-
* (`applyRefundToIntent`), never stepped, so these edges describe where that
|
|
104
|
-
* sum can legally land rather than a sequence anyone walks.
|
|
105
|
-
*/
|
|
106
|
-
partially_refunded: ['refunded', 'settled'],
|
|
107
|
-
refunded: ['partially_refunded', 'settled'],
|
|
108
|
-
expired: [],
|
|
109
|
-
rejected: [],
|
|
51
|
+
// FairCoin: the payer broadcasts (→ processing), or the intent is cancelled
|
|
52
|
+
// or expires (→ canceled). Card: an SCA challenge resolved either way.
|
|
53
|
+
requires_action: [
|
|
54
|
+
'requires_payment_method',
|
|
55
|
+
'processing',
|
|
56
|
+
'requires_capture',
|
|
57
|
+
'succeeded',
|
|
58
|
+
'canceled',
|
|
59
|
+
],
|
|
60
|
+
// FairCoin: confirmed (→ succeeded) or underpaid (→ canceled). Card: an async
|
|
61
|
+
// method settled, was authorized, or failed back to `requires_payment_method`.
|
|
62
|
+
processing: ['requires_payment_method', 'requires_capture', 'succeeded', 'canceled'],
|
|
63
|
+
requires_capture: ['processing', 'succeeded', 'canceled'],
|
|
64
|
+
succeeded: [],
|
|
65
|
+
canceled: [],
|
|
110
66
|
};
|
|
111
67
|
export function isValidStatusTransition(from, to) {
|
|
112
68
|
return ALLOWED[from].includes(to);
|
|
113
69
|
}
|
|
114
|
-
/** Every
|
|
115
|
-
* single source of truth consumed by the Gateway's list-route status filter. */
|
|
70
|
+
/** Every `PaymentIntentStatus`, derived from `ALLOWED`'s keys. */
|
|
116
71
|
export const PAYMENT_INTENT_STATUSES = Object.keys(ALLOWED);
|
|
117
|
-
/**
|
|
118
|
-
|
|
119
|
-
*
|
|
120
|
-
* **Twice now this question has been answered by walking the transition table,
|
|
121
|
-
* and twice the table has moved underneath it.**
|
|
122
|
-
*
|
|
123
|
-
* The hosted checkout first asked leaf-ness — "has this status no outgoing
|
|
124
|
-
* edges" — which meant `settled` while `settled` had none. Adding the refund
|
|
125
|
-
* transitions (`settled → refunded | partially_refunded`) gave it edges, so a
|
|
126
|
-
* settled payment stopped looking finished, and the checkout began REUSING a
|
|
127
|
-
* remembered settled intent instead of minting a fresh one: a payer who had
|
|
128
|
-
* already paid saw their old receipt forever, with no way to pay the link
|
|
129
|
-
* again.
|
|
130
|
-
*
|
|
131
|
-
* The fix was reachability — "is `settled` still reachable" — and that held
|
|
132
|
-
* until refunds could be UNDONE. `refunded → settled` (a bank rejecting a
|
|
133
|
-
* refund days later) makes `settled` reachable from `settled` in two steps, so
|
|
134
|
-
* reachability answers `true` for a payment that is finished, and the original
|
|
135
|
-
* bug returns unchanged.
|
|
136
|
-
*
|
|
137
|
-
* So the set is stated, not derived. It is exactly the statuses in which the
|
|
138
|
-
* gateway is still waiting on the PAYER — which is what the question means, in
|
|
139
|
-
* the payer's own terms, and is the one formulation no edge added elsewhere can
|
|
140
|
-
* silently change. `paymentIntent.test.ts` pins it against the table: every
|
|
141
|
-
* member must still be able to reach `settled` without passing through a
|
|
142
|
-
* post-settlement status, and no non-member may.
|
|
143
|
-
*/
|
|
144
|
-
const PAYABLE_STATUSES = new Set([
|
|
145
|
-
'created',
|
|
146
|
-
'awaiting_approval',
|
|
147
|
-
'approved',
|
|
148
|
-
'broadcast',
|
|
149
|
-
'confirming',
|
|
150
|
-
'requires_action',
|
|
151
|
-
'processing',
|
|
152
|
-
]);
|
|
72
|
+
/** The statuses that end a payment: nothing leaves them (bar the reorg exception). */
|
|
73
|
+
export const TERMINAL_STATUSES = ['succeeded', 'canceled'];
|
|
153
74
|
/**
|
|
154
75
|
* Whether a payer can still complete a payment against an intent in this
|
|
155
|
-
* status.
|
|
156
|
-
*
|
|
157
|
-
* `settled` answers `false`: it is where paying ENDS. `partially_refunded`
|
|
158
|
-
* answers `false` too — money can still move, but not from the payer.
|
|
76
|
+
* status — every non-terminal one. Stated, not derived from reachability:
|
|
77
|
+
* `paymentIntent.test.ts` pins it against the table.
|
|
159
78
|
*/
|
|
160
79
|
export function canStillBePaid(status) {
|
|
161
|
-
return
|
|
162
|
-
}
|
|
163
|
-
/**
|
|
164
|
-
* The statuses a payment reaches only AFTER the payer has finished.
|
|
165
|
-
*
|
|
166
|
-
* Exported for the gate below rather than for production use: the property that
|
|
167
|
-
* makes `PAYABLE_STATUSES` checkable is that a payable status reaches `settled`
|
|
168
|
-
* without going through one of these.
|
|
169
|
-
*/
|
|
170
|
-
export const POST_SETTLEMENT_STATUSES = [
|
|
171
|
-
'settled',
|
|
172
|
-
'refunded',
|
|
173
|
-
'partially_refunded',
|
|
174
|
-
];
|
|
175
|
-
/**
|
|
176
|
-
* Whether `settled` is reachable from `status` without passing through a
|
|
177
|
-
* post-settlement status. The derivation `canStillBePaid` used to BE, kept as
|
|
178
|
-
* the thing that checks it rather than as the thing that answers it.
|
|
179
|
-
*/
|
|
180
|
-
export function reachesSettlementFromPayer(status) {
|
|
181
|
-
const blocked = new Set(POST_SETTLEMENT_STATUSES);
|
|
182
|
-
// A payment that has ALREADY settled is one the payer has finished with,
|
|
183
|
-
// whatever edges lead out of it. Checked before the walk rather than inside
|
|
184
|
-
// it, because `partially_refunded → settled` is a direct edge and the walk
|
|
185
|
-
// would find it in one step — and that edge is a refund being undone, not a
|
|
186
|
-
// payer paying.
|
|
187
|
-
if (blocked.has(status))
|
|
188
|
-
return false;
|
|
189
|
-
const seen = new Set();
|
|
190
|
-
const queue = [...ALLOWED[status]];
|
|
191
|
-
while (queue.length > 0) {
|
|
192
|
-
const next = queue.shift();
|
|
193
|
-
if (next === 'settled')
|
|
194
|
-
return true;
|
|
195
|
-
if (seen.has(next) || blocked.has(next))
|
|
196
|
-
continue;
|
|
197
|
-
seen.add(next);
|
|
198
|
-
queue.push(...ALLOWED[next]);
|
|
199
|
-
}
|
|
200
|
-
return false;
|
|
80
|
+
return !TERMINAL_STATUSES.includes(status);
|
|
201
81
|
}
|
|
202
82
|
/**
|
|
203
83
|
* The statuses an intent may still EXPIRE from (ADR 0012 D9).
|
|
204
84
|
*
|
|
205
|
-
*
|
|
206
|
-
*
|
|
207
|
-
*
|
|
208
|
-
* intent NEVER expires, whatever `
|
|
209
|
-
* settlement watcher owns the row.
|
|
210
|
-
*
|
|
211
|
-
*
|
|
85
|
+
* The gateway is waiting on a PERSON who has not paid: a FairCoin payer who has
|
|
86
|
+
* not broadcast (`requires_action`), or a card payer who has not completed a
|
|
87
|
+
* payment method or a challenge. Once a FairCoin payer has reported a
|
|
88
|
+
* transaction (`processing`) the intent NEVER expires, whatever `expires_at`
|
|
89
|
+
* says — coins are moving and the settlement watcher owns the row. An
|
|
90
|
+
* authorization held for capture (`requires_capture`) is the merchant's to
|
|
91
|
+
* capture or cancel and does not expire either.
|
|
212
92
|
*
|
|
213
|
-
*
|
|
214
|
-
*
|
|
93
|
+
* Expiry is a cancellation with `cancellation_reason: 'abandoned'`: a platform
|
|
94
|
+
* releases a reservation on `payment_intent.canceled`.
|
|
215
95
|
*/
|
|
216
96
|
export const EXPIRABLE_STATUSES = [
|
|
217
|
-
'
|
|
218
|
-
'
|
|
97
|
+
'requires_payment_method',
|
|
98
|
+
'requires_confirmation',
|
|
219
99
|
'requires_action',
|
|
220
100
|
];
|
|
221
101
|
/** Whether an intent in this status can still expire. See {@link EXPIRABLE_STATUSES}. */
|
|
222
102
|
export function canExpire(status) {
|
|
223
103
|
return EXPIRABLE_STATUSES.includes(status);
|
|
224
104
|
}
|
|
105
|
+
export const PAYMENT_INTENT_CANCELLATION_REASONS = [
|
|
106
|
+
'abandoned',
|
|
107
|
+
'automatic',
|
|
108
|
+
'duplicate',
|
|
109
|
+
'fraudulent',
|
|
110
|
+
'requested_by_customer',
|
|
111
|
+
'underpaid',
|
|
112
|
+
];
|
|
113
|
+
/** The reasons a MERCHANT may give `POST /v1/payment_intents/:id/cancel`. */
|
|
114
|
+
export const MERCHANT_CANCELLATION_REASONS = [
|
|
115
|
+
'abandoned',
|
|
116
|
+
'duplicate',
|
|
117
|
+
'fraudulent',
|
|
118
|
+
'requested_by_customer',
|
|
119
|
+
];
|
|
120
|
+
export const PAYMENT_INTENT_CAPTURE_METHODS = [
|
|
121
|
+
'automatic',
|
|
122
|
+
'manual',
|
|
123
|
+
];
|
|
124
|
+
export const PAYMENT_INTENT_SETUP_FUTURE_USAGES = [
|
|
125
|
+
'on_session',
|
|
126
|
+
'off_session',
|
|
127
|
+
];
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
export const WEBHOOK_ENDPOINT_STATUSES = ['enabled', 'disabled'];
|
|
2
|
+
/**
|
|
3
|
+
* Why an endpoint is `disabled`.
|
|
4
|
+
*
|
|
5
|
+
* - `disabled_by_merchant` — its owner set `disabled: true`.
|
|
6
|
+
* - `delivery_failures` — every delivery to it failed for three days
|
|
7
|
+
* straight, so Peable stopped sending. Re-enabling it (`disabled: false`)
|
|
8
|
+
* starts the clock again.
|
|
9
|
+
*/
|
|
10
|
+
export const WEBHOOK_ENDPOINT_DISABLED_REASONS = [
|
|
11
|
+
'disabled_by_merchant',
|
|
12
|
+
'delivery_failures',
|
|
13
|
+
];
|
|
14
|
+
/** At most this many endpoints per merchant (per mode). */
|
|
15
|
+
export const WEBHOOK_ENDPOINTS_MAX_PER_MERCHANT = 16;
|
|
16
|
+
/** The longest a rolled secret may keep signing beside its replacement. */
|
|
17
|
+
export const WEBHOOK_SECRET_ROLL_MAX_EXPIRES_IN_SECONDS = 24 * 60 * 60;
|
|
@@ -9,32 +9,40 @@ function computeSignature(secret, rawBody, timestamp) {
|
|
|
9
9
|
}
|
|
10
10
|
/**
|
|
11
11
|
* Produce a Stripe-style signature header for an outbound webhook:
|
|
12
|
-
* `t=${timestamp},v1=${
|
|
12
|
+
* `t=${timestamp},v1=${hex}` — with one `v1=` per secret.
|
|
13
|
+
*
|
|
14
|
+
* Several secrets are what make a rotation gapless: while an endpoint's rolled
|
|
15
|
+
* secret is still inside its overlap window, every delivery is signed with
|
|
16
|
+
* BOTH, so a receiver verifying with either one accepts it.
|
|
13
17
|
*/
|
|
14
18
|
export function signWebhook(secret, rawBody, timestamp) {
|
|
15
|
-
|
|
19
|
+
const secrets = typeof secret === 'string' ? [secret] : secret;
|
|
20
|
+
if (secrets.length === 0)
|
|
21
|
+
throw new Error('signWebhook needs at least one secret');
|
|
22
|
+
const signatures = secrets.map((s) => `v1=${computeSignature(s, rawBody, timestamp)}`);
|
|
23
|
+
return `t=${timestamp},${signatures.join(',')}`;
|
|
16
24
|
}
|
|
17
25
|
function parseHeader(header) {
|
|
18
26
|
let timestamp = null;
|
|
19
|
-
|
|
27
|
+
const signatures = [];
|
|
20
28
|
for (const part of header.split(',')) {
|
|
21
29
|
const separator = part.indexOf('=');
|
|
22
30
|
if (separator === -1)
|
|
23
31
|
continue;
|
|
24
|
-
const key = part.slice(0, separator);
|
|
25
|
-
const value = part.slice(separator + 1);
|
|
32
|
+
const key = part.slice(0, separator).trim();
|
|
33
|
+
const value = part.slice(separator + 1).trim();
|
|
26
34
|
if (key === 't') {
|
|
27
35
|
const parsed = Number(value);
|
|
28
36
|
if (Number.isInteger(parsed))
|
|
29
37
|
timestamp = parsed;
|
|
30
38
|
}
|
|
31
39
|
else if (key === 'v1') {
|
|
32
|
-
|
|
40
|
+
signatures.push(value);
|
|
33
41
|
}
|
|
34
42
|
}
|
|
35
|
-
if (timestamp === null ||
|
|
43
|
+
if (timestamp === null || signatures.length === 0)
|
|
36
44
|
return null;
|
|
37
|
-
return { timestamp,
|
|
45
|
+
return { timestamp, signatures };
|
|
38
46
|
}
|
|
39
47
|
/**
|
|
40
48
|
* Constant-time hex-string comparison. Returns false for unequal-length inputs
|
|
@@ -49,9 +57,13 @@ function constantTimeEqualHex(a, b) {
|
|
|
49
57
|
}
|
|
50
58
|
/**
|
|
51
59
|
* Verify a webhook signature header against the raw body.
|
|
52
|
-
*
|
|
60
|
+
*
|
|
61
|
+
* - Parses `t` and EVERY `v1` from the header.
|
|
53
62
|
* - Rejects if the timestamp drifts more than `toleranceSec` from `nowSec`.
|
|
54
|
-
* -
|
|
63
|
+
* - Accepts when any `v1` matches the signature computed with any of
|
|
64
|
+
* `secret` — so a receiver mid-rotation can pass both of its secrets, and a
|
|
65
|
+
* delivery signed during the sender's overlap window verifies against
|
|
66
|
+
* either.
|
|
55
67
|
*/
|
|
56
68
|
export function verifyWebhook(secret, rawBody, header, toleranceSec, nowSec) {
|
|
57
69
|
const parsed = parseHeader(header);
|
|
@@ -59,6 +71,17 @@ export function verifyWebhook(secret, rawBody, header, toleranceSec, nowSec) {
|
|
|
59
71
|
return false;
|
|
60
72
|
if (Math.abs(nowSec - parsed.timestamp) > toleranceSec)
|
|
61
73
|
return false;
|
|
62
|
-
const
|
|
63
|
-
|
|
74
|
+
const secrets = typeof secret === 'string' ? [secret] : secret;
|
|
75
|
+
let matched = false;
|
|
76
|
+
// Every comparison runs, so the time taken does not say which one matched.
|
|
77
|
+
for (const candidate of secrets) {
|
|
78
|
+
if (candidate.length === 0)
|
|
79
|
+
continue;
|
|
80
|
+
const expected = computeSignature(candidate, rawBody, parsed.timestamp);
|
|
81
|
+
for (const signature of parsed.signatures) {
|
|
82
|
+
if (constantTimeEqualHex(expected, signature))
|
|
83
|
+
matched = true;
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
return matched;
|
|
64
87
|
}
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import type { CurrencyCode } from './money';
|
|
2
|
+
import type { PaymentIntent } from './paymentIntent';
|
|
3
|
+
/** Card details safe to show: never the number, never the CVC. */
|
|
4
|
+
export interface CardDetails {
|
|
5
|
+
brand: string | null;
|
|
6
|
+
last4: string | null;
|
|
7
|
+
exp_month: number | null;
|
|
8
|
+
exp_year: number | null;
|
|
9
|
+
/** ISO 3166-1 alpha-2 of the issuer. */
|
|
10
|
+
country: string | null;
|
|
11
|
+
/** `credit`, `debit`, `prepaid` or `unknown`. */
|
|
12
|
+
funding: string | null;
|
|
13
|
+
}
|
|
14
|
+
export interface ChargeOutcome {
|
|
15
|
+
/** `approved_by_network`, `declined_by_network`, `not_sent_to_network`, `reversed_after_approval`. */
|
|
16
|
+
network_status: string | null;
|
|
17
|
+
/** `normal`, `elevated`, `highest`, or `not_assessed`. */
|
|
18
|
+
risk_level: string | null;
|
|
19
|
+
/** `authorized`, `manual_review`, `issuer_declined`, `blocked`, `invalid`. */
|
|
20
|
+
type: string | null;
|
|
21
|
+
/** Peable's own one-line explanation for the merchant — never the provider's prose. */
|
|
22
|
+
seller_message: string | null;
|
|
23
|
+
}
|
|
24
|
+
export interface Charge {
|
|
25
|
+
id: string;
|
|
26
|
+
object: 'charge';
|
|
27
|
+
livemode: boolean;
|
|
28
|
+
/** `pi_…`, or the {@link PaymentIntent} with `expand[]=payment_intent`. */
|
|
29
|
+
payment_intent: string | PaymentIntent;
|
|
30
|
+
customer: string | null;
|
|
31
|
+
payment_method: string | null;
|
|
32
|
+
amount: string;
|
|
33
|
+
amount_captured: string;
|
|
34
|
+
amount_refunded: string;
|
|
35
|
+
currency: CurrencyCode;
|
|
36
|
+
/** Always `succeeded`: Peable records the authorization that succeeded, not failed attempts. */
|
|
37
|
+
status: 'succeeded';
|
|
38
|
+
paid: boolean;
|
|
39
|
+
captured: boolean;
|
|
40
|
+
/** All of it came back — by refunds, or by releasing an uncaptured authorization. */
|
|
41
|
+
refunded: boolean;
|
|
42
|
+
disputed: boolean;
|
|
43
|
+
outcome: ChargeOutcome | null;
|
|
44
|
+
payment_method_details: {
|
|
45
|
+
type: 'card';
|
|
46
|
+
card: CardDetails;
|
|
47
|
+
} | null;
|
|
48
|
+
/** Peable-hosted receipts land with Phase 3; the provider's own URL never reaches the wire. */
|
|
49
|
+
receipt_url: string | null;
|
|
50
|
+
/** A Peable balance transaction (`txn_…`) once the ledger exists (Phase 5). */
|
|
51
|
+
balance_transaction: string | null;
|
|
52
|
+
description: string | null;
|
|
53
|
+
receipt_email: string | null;
|
|
54
|
+
metadata: Record<string, string>;
|
|
55
|
+
created: number;
|
|
56
|
+
}
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import type { Address, AddressParams, Shipping, ShippingParams } from './paymentIntent';
|
|
2
|
+
import type { PaymentMethod } from './paymentMethod';
|
|
3
|
+
export interface Customer {
|
|
4
|
+
id: string;
|
|
5
|
+
object: 'customer';
|
|
6
|
+
livemode: boolean;
|
|
7
|
+
email: string | null;
|
|
8
|
+
name: string | null;
|
|
9
|
+
phone: string | null;
|
|
10
|
+
description: string | null;
|
|
11
|
+
address: Address | null;
|
|
12
|
+
shipping: Shipping | null;
|
|
13
|
+
invoice_settings: {
|
|
14
|
+
/** `pm_…`, or the {@link PaymentMethod} with `expand[]=invoice_settings.default_payment_method`. */
|
|
15
|
+
default_payment_method: string | PaymentMethod | null;
|
|
16
|
+
};
|
|
17
|
+
metadata: Record<string, string>;
|
|
18
|
+
created: number;
|
|
19
|
+
}
|
|
20
|
+
/** A deleted customer, as `DELETE /v1/customers/:id` answers. */
|
|
21
|
+
export interface DeletedCustomer {
|
|
22
|
+
id: string;
|
|
23
|
+
object: 'customer';
|
|
24
|
+
deleted: true;
|
|
25
|
+
}
|
|
26
|
+
export interface CreateCustomerParams {
|
|
27
|
+
email?: string;
|
|
28
|
+
name?: string;
|
|
29
|
+
phone?: string;
|
|
30
|
+
description?: string;
|
|
31
|
+
address?: AddressParams;
|
|
32
|
+
shipping?: ShippingParams;
|
|
33
|
+
invoice_settings?: {
|
|
34
|
+
default_payment_method?: string;
|
|
35
|
+
};
|
|
36
|
+
metadata?: Record<string, string>;
|
|
37
|
+
}
|
|
38
|
+
/** `POST /v1/customers/:id`. A field sent as `""` is cleared; `metadata` is a patch. */
|
|
39
|
+
export type UpdateCustomerParams = CreateCustomerParams;
|
package/dist/types/dispute.d.ts
CHANGED
|
@@ -3,8 +3,8 @@ import type { PaymentIntent } from './paymentIntent';
|
|
|
3
3
|
* A dispute — the card network holding a settled payment's money while a
|
|
4
4
|
* cardholder contests it.
|
|
5
5
|
*
|
|
6
|
-
* Published because `
|
|
7
|
-
* carry it as their `data.object`. It was route-local at first, with the webhook
|
|
6
|
+
* Published because `charge.dispute.created`, `charge.dispute.updated` and
|
|
7
|
+
* `charge.dispute.closed` carry it as their `data.object`. It was route-local at first, with the webhook
|
|
8
8
|
* carrying the PaymentIntent and the detail reachable only through
|
|
9
9
|
* `GET /v1/payment_intents/:id/disputes`. That was inconsistent with the reason
|
|
10
10
|
* those two events exist: `disputed` is a DEADLINE, and a merchant who has to
|