@vonpay/checkout-node 1.1.1 → 2.0.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/client.d.ts +124 -1
- package/dist/client.d.ts.map +1 -1
- package/dist/client.js +335 -6
- package/dist/client.js.map +1 -1
- package/dist/error-help.d.ts +15 -1
- package/dist/error-help.d.ts.map +1 -1
- package/dist/error-help.js +46 -6
- package/dist/error-help.js.map +1 -1
- package/dist/errors.d.ts.map +1 -1
- package/dist/errors.js +4 -5
- package/dist/errors.js.map +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js.map +1 -1
- package/dist/telemetry.js +1 -1
- package/dist/telemetry.js.map +1 -1
- package/dist/types.d.ts +249 -17
- package/dist/types.d.ts.map +1 -1
- package/dist/types.js.map +1 -1
- package/package.json +1 -1
package/dist/client.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { VonPayCheckoutConfig, CreateSessionParams, CheckoutSession, SessionStatus, DryRunResult, WebhookEvent, HealthStatus, RequestOptions, ReturnOutcome, ReturnParams, PaymentIntent, CreatePaymentIntentParams, CapturePaymentIntentParams, Capabilities, Refund, CreateRefundParams, Token, CreateTokenParams, WebhookSubscription, ListWebhookSubscriptionsParams, WebhookSubscriptionsList, WebhookEventRecord } from "./types.js";
|
|
1
|
+
import type { VonPayCheckoutConfig, CreateSessionParams, CheckoutSession, SessionStatus, DryRunResult, WebhookEvent, HealthStatus, RequestOptions, ReturnOutcome, ReturnParams, PaymentIntent, CreatePaymentIntentParams, CapturePaymentIntentParams, Capabilities, Refund, CreateRefundParams, Token, CreateTokenParams, WebhookSubscription, ListWebhookSubscriptionsParams, WebhookSubscriptionsList, WebhookEventRecord, Buyer, UpsertBuyerParams, UpdateBuyerParams, FindBuyerParams } from "./types.js";
|
|
2
2
|
export declare const KNOWN_RETURN_SCHEMES: readonly ["v1", "v2"];
|
|
3
3
|
export type ReturnScheme = (typeof KNOWN_RETURN_SCHEMES)[number];
|
|
4
4
|
export declare const DEFAULT_RETURN_SCHEMES: readonly ReturnScheme[];
|
|
@@ -165,6 +165,129 @@ export declare class VonPayCheckout {
|
|
|
165
165
|
* the rest of `/v1/*` on this engine); the SDK exposes responses
|
|
166
166
|
* pass-through with no key conversion.
|
|
167
167
|
*/
|
|
168
|
+
/**
|
|
169
|
+
* Stored shopper profiles. `vp_sk_*` Bearer required.
|
|
170
|
+
*
|
|
171
|
+
* The same profile the checkout flows write via `buyerId` / `buyerEmail`, so
|
|
172
|
+
* a buyer created here is the one a later payment attaches to.
|
|
173
|
+
*
|
|
174
|
+
* ⛔ **The erase is a separate method on purpose.** `PATCH /v1/buyers/:id`
|
|
175
|
+
* accepts `email: null` to permanently erase the address — irreversibly, and
|
|
176
|
+
* it removes the copy held at the payment provider too. The API's own docs
|
|
177
|
+
* warn that several common JSON libraries serialise absent optional fields as
|
|
178
|
+
* `null`, so an ordinary update can wipe a shopper's email with nobody
|
|
179
|
+
* intending it.
|
|
180
|
+
*
|
|
181
|
+
* This SDK refuses to expose that shape. `update()` cannot express `null` —
|
|
182
|
+
* it is a type error — and {@link eraseEmail} is the only path that sends it.
|
|
183
|
+
* An accidental erase is therefore not writable in TypeScript.
|
|
184
|
+
*/
|
|
185
|
+
/**
|
|
186
|
+
* ⚠️ **No idempotency key on this resource, deliberately.**
|
|
187
|
+
*
|
|
188
|
+
* The canonical spec declares `Idempotency-Key` on `POST /v1/sessions` and
|
|
189
|
+
* `POST /v1/refunds`, and documents the replay-mismatch contract for them. It
|
|
190
|
+
* declares NOTHING for either buyers route. An earlier draft of this resource
|
|
191
|
+
* offered the option anyway, and that was wrong: shipping an
|
|
192
|
+
* unconfirmed safety knob on an IRREVERSIBLE, provider-propagated deletion is
|
|
193
|
+
* the wrong default — a caller would reasonably read it as the same
|
|
194
|
+
* replay-safety guarantee the other endpoints give, and we cannot back that.
|
|
195
|
+
*
|
|
196
|
+
* Removed until server support is confirmed and the API contract declares
|
|
197
|
+
* it. If you need replay safety on these calls today, guard at your own call
|
|
198
|
+
* site.
|
|
199
|
+
*/
|
|
200
|
+
buyers: {
|
|
201
|
+
/**
|
|
202
|
+
* `POST /v1/buyers` — create or update a buyer.
|
|
203
|
+
*
|
|
204
|
+
* ⚠️ **This is an UPSERT.** The write is keyed on `externalId` when
|
|
205
|
+
* present, else on `email`. Calling it twice with the same key UPDATES;
|
|
206
|
+
* it does not fail with a duplicate error. The server answers `201` when it
|
|
207
|
+
* created and `200` when it updated, and the body is the full stored
|
|
208
|
+
* profile either way — so what you read back is what was persisted
|
|
209
|
+
* (including `metadata` after its append-merge).
|
|
210
|
+
*
|
|
211
|
+
* At least one of `externalId` / `email` is required: a profile with no
|
|
212
|
+
* lookup key could never be retrieved or charged against. That is checked
|
|
213
|
+
* here rather than spent on a round-trip to a `400`.
|
|
214
|
+
*/
|
|
215
|
+
upsert: (params: UpsertBuyerParams) => Promise<Buyer>;
|
|
216
|
+
/**
|
|
217
|
+
* `GET /v1/buyers/:id` — fetch a stored profile by id.
|
|
218
|
+
*
|
|
219
|
+
* A cross-merchant id returns an opaque 404, never a 403.
|
|
220
|
+
*/
|
|
221
|
+
retrieve: (buyerId: string) => Promise<Buyer>;
|
|
222
|
+
/**
|
|
223
|
+
* `GET /v1/buyers` — exact lookup by your reference or by email.
|
|
224
|
+
*
|
|
225
|
+
* Returns the profile or `null`. There is deliberately no unfiltered
|
|
226
|
+
* listing in v1, so at least one key is required; when both are supplied
|
|
227
|
+
* `externalId` wins, matching the upsert's identity-resolution order.
|
|
228
|
+
*
|
|
229
|
+
* Unwraps the server's `{ data: [...] }` envelope, which holds zero or one
|
|
230
|
+
* profile — handing back a one-element array to destructure would be a
|
|
231
|
+
* worse API for a lookup that can never return two.
|
|
232
|
+
*/
|
|
233
|
+
find: (params: FindBuyerParams) => Promise<Buyer | null>;
|
|
234
|
+
/**
|
|
235
|
+
* `PATCH /v1/buyers/:id` — update a stored profile.
|
|
236
|
+
*
|
|
237
|
+
* Omitted fields are left unchanged. `metadata` merges with append
|
|
238
|
+
* semantics. `externalId` is not patchable — the identity key is fixed at
|
|
239
|
+
* creation; upsert a new profile instead.
|
|
240
|
+
*
|
|
241
|
+
* ⛔ **This method cannot erase.** `email` is typed `string`, so passing
|
|
242
|
+
* `null` is a compile error, and the body is built with an explicit
|
|
243
|
+
* `!== undefined` test rather than a truthiness check — a truthy test would
|
|
244
|
+
* also drop a legitimate empty-string value and, worse, would let a `null`
|
|
245
|
+
* through unnoticed if the type were ever widened. Use
|
|
246
|
+
* {@link eraseEmail} when you actually mean to erase.
|
|
247
|
+
*/
|
|
248
|
+
update: (buyerId: string, params: UpdateBuyerParams) => Promise<Buyer>;
|
|
249
|
+
/**
|
|
250
|
+
* **Permanently erase this buyer's email address.** `PATCH /v1/buyers/:id`
|
|
251
|
+
* with `email: null`.
|
|
252
|
+
*
|
|
253
|
+
* It removes the address from the buyer profile. Afterwards the buyer
|
|
254
|
+
* cannot be found by email.
|
|
255
|
+
*
|
|
256
|
+
* ⚠️ **We also ASK your payment provider to drop their copy — but a
|
|
257
|
+
* successful response here does not confirm that happened.** That request
|
|
258
|
+
* is dispatched after we answer you, is never awaited, and reports no
|
|
259
|
+
* failure back: a provider error, a timeout, or an open circuit breaker
|
|
260
|
+
* all pass silently. And for a buyer we only ever knew by email, whose
|
|
261
|
+
* provider record we had not separately recorded, the retraction cannot be
|
|
262
|
+
* performed at all — the key we would need was derived from the address
|
|
263
|
+
* that was just erased. Do not tell a shopper the provider copy is gone on
|
|
264
|
+
* the strength of a `200`.
|
|
265
|
+
*
|
|
266
|
+
* ⛔ **This is NOT a complete deletion on its own, and must not be
|
|
267
|
+
* described to a shopper as one.** It clears the profile's address. Other
|
|
268
|
+
* copies live outside this record, under their own
|
|
269
|
+
* retention rules, and this call does not touch them — including the
|
|
270
|
+
* settlement record kept for financial audit, checkout sessions from the
|
|
271
|
+
* recent past, and any address already forwarded to a connected store,
|
|
272
|
+
* which is an external system with no retraction path.
|
|
273
|
+
*
|
|
274
|
+
* ⚠️ Read that as examples, **not as an inventory.** It is the set we have
|
|
275
|
+
* confirmed, not a completed audit of every place a copy can exist — your
|
|
276
|
+
* own systems may hold one we sent you earlier. Treat this call as the
|
|
277
|
+
* profile half of a deletion request, not the whole answer.
|
|
278
|
+
*
|
|
279
|
+
* ⚠️ **It cannot be undone**, and a buyer identified only by email becomes
|
|
280
|
+
* unreachable by lookup afterwards — the record and every payment attached
|
|
281
|
+
* to it survive, but `find({ email })` will no longer locate it. Keep your
|
|
282
|
+
* own reference in `externalId` if you need to find them again.
|
|
283
|
+
*
|
|
284
|
+
* No other profile field is erasable this way; `email` is the only one the
|
|
285
|
+
* API declares nullable.
|
|
286
|
+
*
|
|
287
|
+
* @returns the stored profile as it is after the erase, with `email: null`.
|
|
288
|
+
*/
|
|
289
|
+
eraseEmail: (buyerId: string) => Promise<Buyer>;
|
|
290
|
+
};
|
|
168
291
|
webhookSubscriptions: {
|
|
169
292
|
/**
|
|
170
293
|
* `GET /v1/webhook_subscriptions` — Stripe-shaped list envelope.
|
package/dist/client.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"client.d.ts","sourceRoot":"","sources":["../src/client.ts"],"names":[],"mappings":"AAIA,OAAO,KAAK,EACV,oBAAoB,EACpB,mBAAmB,EACnB,eAAe,EACf,aAAa,EACb,YAAY,EACZ,YAAY,EACZ,YAAY,EACZ,cAAc,EAGd,aAAa,EACb,YAAY,EAGZ,aAAa,EACb,yBAAyB,EACzB,0BAA0B,EAE1B,YAAY,EACZ,MAAM,EACN,kBAAkB,EAClB,KAAK,EACL,iBAAiB,EACjB,mBAAmB,EACnB,8BAA8B,EAC9B,wBAAwB,EACxB,kBAAkB,
|
|
1
|
+
{"version":3,"file":"client.d.ts","sourceRoot":"","sources":["../src/client.ts"],"names":[],"mappings":"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"}
|
package/dist/client.js
CHANGED
|
@@ -9,11 +9,34 @@ const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
|
9
9
|
const pkg = JSON.parse(readFileSync(join(__dirname, "..", "package.json"), "utf-8"));
|
|
10
10
|
const SDK_VERSION = pkg.version;
|
|
11
11
|
const DEFAULT_BASE_URL = "https://checkout.vonpay.com";
|
|
12
|
-
// Sent as `Von-Pay-Version` on every request.
|
|
13
|
-
//
|
|
14
|
-
//
|
|
15
|
-
//
|
|
16
|
-
|
|
12
|
+
// Sent as `Von-Pay-Version` on every request.
|
|
13
|
+
//
|
|
14
|
+
// ⛔ THIS WAS "2026-05-05" AND THE API HAS NEVER ACCEPTED THAT VALUE.
|
|
15
|
+
// The server keeps a one-member allowlist (`SUPPORTED_VERSIONS` in checkout's
|
|
16
|
+
// proxy) containing only "2026-04-14", and silently substitutes it for anything
|
|
17
|
+
// else — no error, no warning. So every request this SDK has ever sent was
|
|
18
|
+
// served as 2026-04-14 while announcing something different, and the response
|
|
19
|
+
// header said so.
|
|
20
|
+
//
|
|
21
|
+
// The old value was not a premature version. It was the DATE THE 0.6.x SDK
|
|
22
|
+
// SCAFFOLD WAS WRITTEN (commit 1eb30ee, 2026-05-05), recorded as though it were
|
|
23
|
+
// a contract. Nothing on the server ever implemented it; the only occurrences
|
|
24
|
+
// of that string in checkout are comment dates.
|
|
25
|
+
//
|
|
26
|
+
// ⚠️ WHY IT MATTERED EVEN THOUGH NOTHING BROKE: a merchant copying this default
|
|
27
|
+
// and pinning `Von-Pay-Version: 2026-05-05` explicitly believed they had pinned
|
|
28
|
+
// a NEWER contract. They had pinned nothing and silently got the older one.
|
|
29
|
+
// Version pinning that silently no-ops is the exact failure pinning exists to
|
|
30
|
+
// prevent. Reported by vonpay-docs, 2026-09-08.
|
|
31
|
+
//
|
|
32
|
+
// This correction is RUNTIME-NEUTRAL: the server was already serving
|
|
33
|
+
// 2026-04-14: only the announced value changes, to the one actually honoured.
|
|
34
|
+
//
|
|
35
|
+
// ⛔ Do NOT bump this to a new date because a new SDK surface shipped. It is not
|
|
36
|
+
// a changelog. Bump it only when checkout ADDS the value to SUPPORTED_VERSIONS,
|
|
37
|
+
// and keep it in lock-step with the Python constant
|
|
38
|
+
// (`sdk/api-version-bump-both-sdks`).
|
|
39
|
+
const DEFAULT_API_VERSION = "2026-04-14";
|
|
17
40
|
const DEFAULT_MAX_RETRIES = 2;
|
|
18
41
|
const DEFAULT_TIMEOUT = 30_000;
|
|
19
42
|
const RETRYABLE_STATUS_CODES = new Set([429, 500, 502, 503, 504]);
|
|
@@ -571,6 +594,35 @@ function paymentIntentFromWire(raw) {
|
|
|
571
594
|
metadata: metadata ?? {},
|
|
572
595
|
};
|
|
573
596
|
}
|
|
597
|
+
/**
|
|
598
|
+
* Parse a server buyer response into the camelCase {@link Buyer} type.
|
|
599
|
+
*
|
|
600
|
+
* ⛔ WITHOUT THIS, THE RESOURCE'S OWN PRIMARY FIELD IS UNDEFINED. `request()`
|
|
601
|
+
* returns `response.json()` raw — it does NOT convert case — and every other
|
|
602
|
+
* resource here calls a `*FromWire` helper on the way out. The first version of
|
|
603
|
+
* `buyers` called none, so `externalId`, `createdAt`, `lastSeenAt` and every
|
|
604
|
+
* nested `address.*` field came back `undefined` from a correct server
|
|
605
|
+
* response. `externalId` is the field this SDK's own erase docs tell an
|
|
606
|
+
* integrator to keep hold of.
|
|
607
|
+
*
|
|
608
|
+
* Worth recording WHY this was easy to miss:
|
|
609
|
+
* the one committed test that read a returned field read `id`, which is spelled
|
|
610
|
+
* identically in both conventions and therefore passed by coincidence. A test
|
|
611
|
+
* that only ever asserts a convention-invariant field cannot detect a
|
|
612
|
+
* conversion bug.
|
|
613
|
+
*
|
|
614
|
+
* `metadata` is passed through verbatim — integrator-defined keys must NEVER be
|
|
615
|
+
* case-converted — they are the merchant's own keys and must round-trip
|
|
616
|
+
* byte-for-byte.
|
|
617
|
+
*/
|
|
618
|
+
function buyerFromWire(raw) {
|
|
619
|
+
const { metadata, ...rest } = raw;
|
|
620
|
+
const camel = toCamelKeysDeep(rest);
|
|
621
|
+
return {
|
|
622
|
+
...camel,
|
|
623
|
+
metadata: metadata ?? {},
|
|
624
|
+
};
|
|
625
|
+
}
|
|
574
626
|
// Server-issued resource IDs match this shape (vps_*, vpi_*, vpr_*, vp_pmt_*,
|
|
575
627
|
// etc). encodeURIComponent alone is sufficient for path safety on Node fetch,
|
|
576
628
|
// but a strict allowlist closes the contract — runtime-dependent encoding of
|
|
@@ -1084,7 +1136,32 @@ export class VonPayCheckout {
|
|
|
1084
1136
|
capabilities = {
|
|
1085
1137
|
get: async () => {
|
|
1086
1138
|
const { data } = await this.request("GET", "/v1/capabilities", { reporterMethod: "capabilities.get" });
|
|
1087
|
-
|
|
1139
|
+
const caps = toCamelKeysDeep(data);
|
|
1140
|
+
// ⛔ NORMALISE `disputeReporting` — do NOT trust the pass-through here.
|
|
1141
|
+
//
|
|
1142
|
+
// Everything else on this response is converted generically, which is why
|
|
1143
|
+
// the value reached callers even before it was declared. But the type
|
|
1144
|
+
// promises a non-optional `"tracked" | "not_tracked"`, and a generic
|
|
1145
|
+
// conversion cannot deliver that: against a server older than the field
|
|
1146
|
+
// (or any future regression) the property is simply absent, and a caller
|
|
1147
|
+
// doing an exhaustive check on a value the type says is always present
|
|
1148
|
+
// gets `undefined`. Python already defaults this; Node promised the same
|
|
1149
|
+
// guarantee and did not enforce it (found 2026-08-29).
|
|
1150
|
+
//
|
|
1151
|
+
// Unknown or missing both resolve to `"not_tracked"`, which is the
|
|
1152
|
+
// fail-safe direction and the whole point of the field: it can only mean
|
|
1153
|
+
// "we cannot promise we would see a dispute", and a server that does not
|
|
1154
|
+
// send it genuinely cannot. Resolving toward `"tracked"` would invent a
|
|
1155
|
+
// reassurance nobody gave and let a merchant read a `succeeded` payment
|
|
1156
|
+
// as dispute-free while nothing is watching.
|
|
1157
|
+
const reported = caps.supportedOperations?.disputeReporting;
|
|
1158
|
+
return {
|
|
1159
|
+
...caps,
|
|
1160
|
+
supportedOperations: {
|
|
1161
|
+
...caps.supportedOperations,
|
|
1162
|
+
disputeReporting: reported === "tracked" ? "tracked" : "not_tracked",
|
|
1163
|
+
},
|
|
1164
|
+
};
|
|
1088
1165
|
},
|
|
1089
1166
|
};
|
|
1090
1167
|
sessions = {
|
|
@@ -1274,6 +1351,258 @@ export class VonPayCheckout {
|
|
|
1274
1351
|
* the rest of `/v1/*` on this engine); the SDK exposes responses
|
|
1275
1352
|
* pass-through with no key conversion.
|
|
1276
1353
|
*/
|
|
1354
|
+
/**
|
|
1355
|
+
* Stored shopper profiles. `vp_sk_*` Bearer required.
|
|
1356
|
+
*
|
|
1357
|
+
* The same profile the checkout flows write via `buyerId` / `buyerEmail`, so
|
|
1358
|
+
* a buyer created here is the one a later payment attaches to.
|
|
1359
|
+
*
|
|
1360
|
+
* ⛔ **The erase is a separate method on purpose.** `PATCH /v1/buyers/:id`
|
|
1361
|
+
* accepts `email: null` to permanently erase the address — irreversibly, and
|
|
1362
|
+
* it removes the copy held at the payment provider too. The API's own docs
|
|
1363
|
+
* warn that several common JSON libraries serialise absent optional fields as
|
|
1364
|
+
* `null`, so an ordinary update can wipe a shopper's email with nobody
|
|
1365
|
+
* intending it.
|
|
1366
|
+
*
|
|
1367
|
+
* This SDK refuses to expose that shape. `update()` cannot express `null` —
|
|
1368
|
+
* it is a type error — and {@link eraseEmail} is the only path that sends it.
|
|
1369
|
+
* An accidental erase is therefore not writable in TypeScript.
|
|
1370
|
+
*/
|
|
1371
|
+
/**
|
|
1372
|
+
* ⚠️ **No idempotency key on this resource, deliberately.**
|
|
1373
|
+
*
|
|
1374
|
+
* The canonical spec declares `Idempotency-Key` on `POST /v1/sessions` and
|
|
1375
|
+
* `POST /v1/refunds`, and documents the replay-mismatch contract for them. It
|
|
1376
|
+
* declares NOTHING for either buyers route. An earlier draft of this resource
|
|
1377
|
+
* offered the option anyway, and that was wrong: shipping an
|
|
1378
|
+
* unconfirmed safety knob on an IRREVERSIBLE, provider-propagated deletion is
|
|
1379
|
+
* the wrong default — a caller would reasonably read it as the same
|
|
1380
|
+
* replay-safety guarantee the other endpoints give, and we cannot back that.
|
|
1381
|
+
*
|
|
1382
|
+
* Removed until server support is confirmed and the API contract declares
|
|
1383
|
+
* it. If you need replay safety on these calls today, guard at your own call
|
|
1384
|
+
* site.
|
|
1385
|
+
*/
|
|
1386
|
+
buyers = {
|
|
1387
|
+
/**
|
|
1388
|
+
* `POST /v1/buyers` — create or update a buyer.
|
|
1389
|
+
*
|
|
1390
|
+
* ⚠️ **This is an UPSERT.** The write is keyed on `externalId` when
|
|
1391
|
+
* present, else on `email`. Calling it twice with the same key UPDATES;
|
|
1392
|
+
* it does not fail with a duplicate error. The server answers `201` when it
|
|
1393
|
+
* created and `200` when it updated, and the body is the full stored
|
|
1394
|
+
* profile either way — so what you read back is what was persisted
|
|
1395
|
+
* (including `metadata` after its append-merge).
|
|
1396
|
+
*
|
|
1397
|
+
* At least one of `externalId` / `email` is required: a profile with no
|
|
1398
|
+
* lookup key could never be retrieved or charged against. That is checked
|
|
1399
|
+
* here rather than spent on a round-trip to a `400`.
|
|
1400
|
+
*/
|
|
1401
|
+
upsert: async (params) => {
|
|
1402
|
+
// Snapshot before checking, for the same reason `update()` does — a
|
|
1403
|
+
// getter that answers differently on a second read must not be able to
|
|
1404
|
+
// change what we validated. Also means the body we send is exactly the
|
|
1405
|
+
// object we inspected.
|
|
1406
|
+
const snapshot = { ...(params ?? {}) };
|
|
1407
|
+
if (snapshot.externalId === undefined && snapshot.email === undefined) {
|
|
1408
|
+
throw new TypeError("buyers.upsert requires at least one of externalId or email — a buyer " +
|
|
1409
|
+
"with no lookup key can never be retrieved or charged against. " +
|
|
1410
|
+
"externalId is your own customer reference and is the preferred key.");
|
|
1411
|
+
}
|
|
1412
|
+
// ⚠️ Refuse a null email here too, even though only PATCH gives null the
|
|
1413
|
+
// erase meaning. The argument for `update()`'s runtime guard — a
|
|
1414
|
+
// JavaScript caller has no type system, and their serialiser may emit
|
|
1415
|
+
// null for a field they left out — applies identically to this call, so
|
|
1416
|
+
// guarding one and not the other would be arbitrary. The spec types
|
|
1417
|
+
// this field non-nullable, so the server would answer 400; failing here
|
|
1418
|
+
// with a message that names the right method is a better answer than a
|
|
1419
|
+
// round-trip to a validation error.
|
|
1420
|
+
if (snapshot.email === null) {
|
|
1421
|
+
throw new TypeError("buyers.upsert does not accept a null email. Omit the field to leave it " +
|
|
1422
|
+
"unset, or pass an address. To erase an existing buyer's email, call " +
|
|
1423
|
+
"buyers.eraseEmail(buyerId) — that is the only method that erases.");
|
|
1424
|
+
}
|
|
1425
|
+
const { data } = await this.request("POST", "/v1/buyers", {
|
|
1426
|
+
// buildSnakeBodyOpaqueMetadata, not toSnakeKeysDeep: the latter recurses
|
|
1427
|
+
// into `metadata` and rewrites the MERCHANT'S OWN keys — `orderId`
|
|
1428
|
+
// silently becomes `order_id`, which they can never match on again
|
|
1429
|
+
// (review rule sdk/metadata-keys-opaque). Every other resource in this
|
|
1430
|
+
// file uses the helper; buyers did not.
|
|
1431
|
+
body: buildSnakeBodyOpaqueMetadata(snapshot),
|
|
1432
|
+
reporterMethod: "buyers.upsert",
|
|
1433
|
+
});
|
|
1434
|
+
return buyerFromWire(data);
|
|
1435
|
+
},
|
|
1436
|
+
/**
|
|
1437
|
+
* `GET /v1/buyers/:id` — fetch a stored profile by id.
|
|
1438
|
+
*
|
|
1439
|
+
* A cross-merchant id returns an opaque 404, never a 403.
|
|
1440
|
+
*/
|
|
1441
|
+
retrieve: async (buyerId) => {
|
|
1442
|
+
assertResourceId(buyerId, "buyerId");
|
|
1443
|
+
const { data } = await this.request("GET", `/v1/buyers/${encodeURIComponent(buyerId)}`, { reporterMethod: "buyers.retrieve" });
|
|
1444
|
+
return buyerFromWire(data);
|
|
1445
|
+
},
|
|
1446
|
+
/**
|
|
1447
|
+
* `GET /v1/buyers` — exact lookup by your reference or by email.
|
|
1448
|
+
*
|
|
1449
|
+
* Returns the profile or `null`. There is deliberately no unfiltered
|
|
1450
|
+
* listing in v1, so at least one key is required; when both are supplied
|
|
1451
|
+
* `externalId` wins, matching the upsert's identity-resolution order.
|
|
1452
|
+
*
|
|
1453
|
+
* Unwraps the server's `{ data: [...] }` envelope, which holds zero or one
|
|
1454
|
+
* profile — handing back a one-element array to destructure would be a
|
|
1455
|
+
* worse API for a lookup that can never return two.
|
|
1456
|
+
*/
|
|
1457
|
+
find: async (params) => {
|
|
1458
|
+
if (params?.externalId === undefined && params?.email === undefined) {
|
|
1459
|
+
throw new TypeError("buyers.find requires at least one of externalId or email — there is " +
|
|
1460
|
+
"no unfiltered buyer listing in v1.");
|
|
1461
|
+
}
|
|
1462
|
+
const query = {};
|
|
1463
|
+
if (params.externalId !== undefined)
|
|
1464
|
+
query.external_id = params.externalId;
|
|
1465
|
+
if (params.email !== undefined)
|
|
1466
|
+
query.email = params.email;
|
|
1467
|
+
const { data } = await this.request("GET", "/v1/buyers", {
|
|
1468
|
+
query,
|
|
1469
|
+
reporterMethod: "buyers.find",
|
|
1470
|
+
});
|
|
1471
|
+
const first = data.data[0];
|
|
1472
|
+
return first ? buyerFromWire(first) : null;
|
|
1473
|
+
},
|
|
1474
|
+
/**
|
|
1475
|
+
* `PATCH /v1/buyers/:id` — update a stored profile.
|
|
1476
|
+
*
|
|
1477
|
+
* Omitted fields are left unchanged. `metadata` merges with append
|
|
1478
|
+
* semantics. `externalId` is not patchable — the identity key is fixed at
|
|
1479
|
+
* creation; upsert a new profile instead.
|
|
1480
|
+
*
|
|
1481
|
+
* ⛔ **This method cannot erase.** `email` is typed `string`, so passing
|
|
1482
|
+
* `null` is a compile error, and the body is built with an explicit
|
|
1483
|
+
* `!== undefined` test rather than a truthiness check — a truthy test would
|
|
1484
|
+
* also drop a legitimate empty-string value and, worse, would let a `null`
|
|
1485
|
+
* through unnoticed if the type were ever widened. Use
|
|
1486
|
+
* {@link eraseEmail} when you actually mean to erase.
|
|
1487
|
+
*/
|
|
1488
|
+
update: async (buyerId, params) => {
|
|
1489
|
+
assertResourceId(buyerId, "buyerId");
|
|
1490
|
+
// ⛔ REFUSE AN EXPLICIT NULL AT RUNTIME, not only in the type.
|
|
1491
|
+
//
|
|
1492
|
+
// The type makes this unwritable in TypeScript, and that is worth having
|
|
1493
|
+
// — but it protects exactly the callers who were never the risk. The
|
|
1494
|
+
// API's own warning is about JSON libraries that serialise an ABSENT
|
|
1495
|
+
// optional field as `null`, and a caller in plain JavaScript has no type
|
|
1496
|
+
// system at all. For both of them the type is not present at runtime and
|
|
1497
|
+
// the erase would go through on an ordinary update.
|
|
1498
|
+
//
|
|
1499
|
+
// So the guarantee is enforced where it is actually load-bearing. There
|
|
1500
|
+
// is exactly one way to erase in this SDK, and it says so in its name.
|
|
1501
|
+
// ⛔ READ EVERY FIELD EXACTLY ONCE, BEFORE ANY OF THEM IS CHECKED.
|
|
1502
|
+
//
|
|
1503
|
+
// The first version of this guard read `params.email` three times — once
|
|
1504
|
+
// to test for null, twice more to build the body. That is a
|
|
1505
|
+
// check-then-use gap: an object whose `email` is a GETTER (or a Proxy)
|
|
1506
|
+
// can return `undefined` to the check and `null` to the body-builder,
|
|
1507
|
+
// and the erase goes through `update()` with no exception at all.
|
|
1508
|
+
// Reproduced against the real code, so this is not theoretical.
|
|
1509
|
+
//
|
|
1510
|
+
// That is a check-then-use gap, and it defeats the entire point of the
|
|
1511
|
+
// guard. Objects with lazily-evaluated properties are ordinary — ORM
|
|
1512
|
+
// models, framework request binders, validation wrappers — so this needs
|
|
1513
|
+
// no adversary, just a caller passing something that is not a plain
|
|
1514
|
+
// object literal. Snapshot first; decide afterwards.
|
|
1515
|
+
const email = params?.email;
|
|
1516
|
+
const name = params?.name;
|
|
1517
|
+
const phone = params?.phone;
|
|
1518
|
+
const company = params?.company;
|
|
1519
|
+
const address = params?.address;
|
|
1520
|
+
const metadata = params?.metadata;
|
|
1521
|
+
if (email === null) {
|
|
1522
|
+
throw new TypeError("buyers.update cannot erase an email address. Sending `email: null` on " +
|
|
1523
|
+
"this endpoint permanently erases it — including the copy held at your " +
|
|
1524
|
+
"payment provider — and it cannot be undone. If you meant to erase it, " +
|
|
1525
|
+
"call buyers.eraseEmail(buyerId). If you did not, check whether your " +
|
|
1526
|
+
"JSON serialiser is emitting null for fields you simply left out; " +
|
|
1527
|
+
"several common libraries do that by default. To leave the address " +
|
|
1528
|
+
"unchanged, omit the field entirely.");
|
|
1529
|
+
}
|
|
1530
|
+
const patch = {};
|
|
1531
|
+
// Explicit `!== undefined`, never truthiness: "" is a legitimate value to
|
|
1532
|
+
// send for a name, and truthiness would silently drop it.
|
|
1533
|
+
if (email !== undefined)
|
|
1534
|
+
patch.email = email;
|
|
1535
|
+
if (name !== undefined)
|
|
1536
|
+
patch.name = name;
|
|
1537
|
+
if (phone !== undefined)
|
|
1538
|
+
patch.phone = phone;
|
|
1539
|
+
if (company !== undefined)
|
|
1540
|
+
patch.company = company;
|
|
1541
|
+
if (address !== undefined)
|
|
1542
|
+
patch.address = address;
|
|
1543
|
+
if (metadata !== undefined)
|
|
1544
|
+
patch.metadata = metadata;
|
|
1545
|
+
// Same helper as every other resource: snake-cases `address.addressLine1`
|
|
1546
|
+
// and friends while leaving the merchant's own metadata keys untouched.
|
|
1547
|
+
const body = buildSnakeBodyOpaqueMetadata(patch);
|
|
1548
|
+
const { data } = await this.request("PATCH", `/v1/buyers/${encodeURIComponent(buyerId)}`, {
|
|
1549
|
+
body,
|
|
1550
|
+
reporterMethod: "buyers.update",
|
|
1551
|
+
});
|
|
1552
|
+
return buyerFromWire(data);
|
|
1553
|
+
},
|
|
1554
|
+
/**
|
|
1555
|
+
* **Permanently erase this buyer's email address.** `PATCH /v1/buyers/:id`
|
|
1556
|
+
* with `email: null`.
|
|
1557
|
+
*
|
|
1558
|
+
* It removes the address from the buyer profile. Afterwards the buyer
|
|
1559
|
+
* cannot be found by email.
|
|
1560
|
+
*
|
|
1561
|
+
* ⚠️ **We also ASK your payment provider to drop their copy — but a
|
|
1562
|
+
* successful response here does not confirm that happened.** That request
|
|
1563
|
+
* is dispatched after we answer you, is never awaited, and reports no
|
|
1564
|
+
* failure back: a provider error, a timeout, or an open circuit breaker
|
|
1565
|
+
* all pass silently. And for a buyer we only ever knew by email, whose
|
|
1566
|
+
* provider record we had not separately recorded, the retraction cannot be
|
|
1567
|
+
* performed at all — the key we would need was derived from the address
|
|
1568
|
+
* that was just erased. Do not tell a shopper the provider copy is gone on
|
|
1569
|
+
* the strength of a `200`.
|
|
1570
|
+
*
|
|
1571
|
+
* ⛔ **This is NOT a complete deletion on its own, and must not be
|
|
1572
|
+
* described to a shopper as one.** It clears the profile's address. Other
|
|
1573
|
+
* copies live outside this record, under their own
|
|
1574
|
+
* retention rules, and this call does not touch them — including the
|
|
1575
|
+
* settlement record kept for financial audit, checkout sessions from the
|
|
1576
|
+
* recent past, and any address already forwarded to a connected store,
|
|
1577
|
+
* which is an external system with no retraction path.
|
|
1578
|
+
*
|
|
1579
|
+
* ⚠️ Read that as examples, **not as an inventory.** It is the set we have
|
|
1580
|
+
* confirmed, not a completed audit of every place a copy can exist — your
|
|
1581
|
+
* own systems may hold one we sent you earlier. Treat this call as the
|
|
1582
|
+
* profile half of a deletion request, not the whole answer.
|
|
1583
|
+
*
|
|
1584
|
+
* ⚠️ **It cannot be undone**, and a buyer identified only by email becomes
|
|
1585
|
+
* unreachable by lookup afterwards — the record and every payment attached
|
|
1586
|
+
* to it survive, but `find({ email })` will no longer locate it. Keep your
|
|
1587
|
+
* own reference in `externalId` if you need to find them again.
|
|
1588
|
+
*
|
|
1589
|
+
* No other profile field is erasable this way; `email` is the only one the
|
|
1590
|
+
* API declares nullable.
|
|
1591
|
+
*
|
|
1592
|
+
* @returns the stored profile as it is after the erase, with `email: null`.
|
|
1593
|
+
*/
|
|
1594
|
+
eraseEmail: async (buyerId) => {
|
|
1595
|
+
assertResourceId(buyerId, "buyerId");
|
|
1596
|
+
const { data } = await this.request("PATCH", `/v1/buyers/${encodeURIComponent(buyerId)}`, {
|
|
1597
|
+
// The one place in this SDK that sends an explicit null. Written as a
|
|
1598
|
+
// literal rather than routed through the params builder so it cannot
|
|
1599
|
+
// be reached by any other call path.
|
|
1600
|
+
body: { email: null },
|
|
1601
|
+
reporterMethod: "buyers.eraseEmail",
|
|
1602
|
+
});
|
|
1603
|
+
return buyerFromWire(data);
|
|
1604
|
+
},
|
|
1605
|
+
};
|
|
1277
1606
|
webhookSubscriptions = {
|
|
1278
1607
|
/**
|
|
1279
1608
|
* `GET /v1/webhook_subscriptions` — Stripe-shaped list envelope.
|