@vaam-apps/vpay-sdk 0.9.0 → 0.10.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/README.md +48 -3
- package/dist/errors.d.ts +26 -0
- package/dist/errors.js +29 -0
- package/dist/form.d.ts +3 -0
- package/dist/form.js +8 -4
- package/dist/index.d.ts +1 -1
- package/dist/index.js +1 -1
- package/dist/resources/checkout-sessions.js +3 -2
- package/dist/resources/customers.js +4 -3
- package/dist/resources/invoices.d.ts +4 -3
- package/dist/resources/invoices.js +15 -13
- package/dist/resources/payment-intents.js +4 -4
- package/dist/resources/refunds.js +4 -4
- package/dist/types.d.ts +6 -4
- package/dist/validate.d.ts +23 -0
- package/dist/validate.js +39 -1
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -225,6 +225,7 @@ works as a catch-all; narrow further with `instanceof` on a specific subclass:
|
|
|
225
225
|
| `VpayUnexpectedResponseError` | A response that isn't the shape this SDK expects — a proxy's HTML 502, an empty body, or a token-endpoint 200 that is not `{ access_token, token_type: "Bearer", expires_in }` (a non-`Bearer` `token_type` lands here, not in `VpayAuthError`: nothing failed to authenticate, the SDK simply cannot present that token). | `status`, `bodyPrefix` (first 500 bytes) |
|
|
226
226
|
| `VpayTransportError` | DNS, TLS, connection refused, timeout. | `cause` (the underlying error) |
|
|
227
227
|
| `VpayConfigError` | The SDK was misconfigured — caught at construction where possible. | — |
|
|
228
|
+
| `VpayInvalidParamsError` | A request parameter this SDK refuses to send, caught before anything is sent: an amount outside `0..=2^53-1`, a value the form encoder cannot put on the wire exactly (a non-safe-integer number, a `Date`, a `Map`), an empty or all-dots path `id`, a URL beside `outOfBand`. Not a `TypeError` (2026-10-21). | `param`, `message` |
|
|
228
229
|
| `WebhookSignatureError` | `verifyWebhook` rejected a signature. | — |
|
|
229
230
|
|
|
230
231
|
```ts
|
|
@@ -591,6 +592,40 @@ VPAY_AUDIENCE=https://api.vpay.example/v1/oauth/token \
|
|
|
591
592
|
|
|
592
593
|
## Status
|
|
593
594
|
|
|
595
|
+
**Breaking, 2026-10-21: every local refusal is a `VpayInvalidParamsError`,
|
|
596
|
+
not a `TypeError`.** An amount outside `0..=2^53-1`, a value the form
|
|
597
|
+
encoder cannot put on the wire exactly, an empty or all-dots path `id` and a
|
|
598
|
+
URL beside `outOfBand` on `invoices.pay` now reject with the new exported
|
|
599
|
+
`VpayInvalidParamsError`. It extends `VpayError`, so the README's
|
|
600
|
+
`instanceof VpayError` catch-all finally catches it, and its `param` names
|
|
601
|
+
the parameter, like `sdks/rust`'s `Error::InvalidParams { param, .. }`. Its
|
|
602
|
+
`message` is that variant's `Display` text,
|
|
603
|
+
`invalid request parameter {param}: {detail}`. **A caller that tested
|
|
604
|
+
`instanceof TypeError` for these no longer matches**, and cannot be made to:
|
|
605
|
+
a class has one superclass, and making `VpayError` a `TypeError` would make
|
|
606
|
+
every error here one, a network failure included. Switch to
|
|
607
|
+
`instanceof VpayInvalidParamsError`. No signature changed and nothing that
|
|
608
|
+
was sent before is refused now.
|
|
609
|
+
|
|
610
|
+
**Behaviour change, 2026-10-21: an empty or all-dots `id` is refused before
|
|
611
|
+
any request.** Every one of the twenty-one methods that put an `id` in the
|
|
612
|
+
path now rejects with a `TypeError` for `""`, `.`, `..` or any longer run of
|
|
613
|
+
dots, without minting a token or sending anything. No signature changed —
|
|
614
|
+
each already returned a `Promise` — but the outcome for those inputs did.
|
|
615
|
+
They used to be sent, and `fetch`'s URL parser resolved them before the
|
|
616
|
+
request left, so `customers.del("..")` was a `DELETE /v1/` and
|
|
617
|
+
`paymentIntents.retrieve("")` a `GET /v1/payment_intents/`; escaping cannot
|
|
618
|
+
help, because the parser reads `%2E%2E` as `..` too. What vpay answered for
|
|
619
|
+
each is on
|
|
620
|
+
[the dated verification page](../../docs/status/verification/2026-10-21-sdk-dot-segment-ids.md).
|
|
621
|
+
A caller catching `VpayApiError` for those inputs now gets a `TypeError`,
|
|
622
|
+
and so does one narrowing on `VpayError`: like the amount refusal, it is not
|
|
623
|
+
one (a dated gap in [docs/sdks/parity.md](../../docs/sdks/parity.md)).
|
|
624
|
+
`sdks/rust` refuses the same ids with `Error::InvalidParams`. _(Corrected
|
|
625
|
+
later the same day: the refusal is a `VpayInvalidParamsError` now, so a
|
|
626
|
+
`VpayError` narrowing catches it and the gap is closed — the paragraph
|
|
627
|
+
above.)_
|
|
628
|
+
|
|
594
629
|
**Corrected 2026-09-03.** This section used to say "what it does **not**
|
|
595
630
|
have is a single `/v1` resource route" — true when it was written on
|
|
596
631
|
2026-09-02, and false since Step 2 landed the next day.
|
|
@@ -672,7 +707,17 @@ none of them touches the network:
|
|
|
672
707
|
session's `url`, which carries the same value — are redacted from
|
|
673
708
|
`util.inspect` while `JSON.stringify` stays faithful.
|
|
674
709
|
- The form encoder's nested-object/array/boolean/percent-encoding rules, and
|
|
675
|
-
that a non-integer `amount` throws `
|
|
710
|
+
that a non-integer `amount` throws `VpayInvalidParamsError` before any
|
|
711
|
+
request is sent.
|
|
712
|
+
- That every kind of local refusal — an amount, an id, an out-of-band URL, a
|
|
713
|
+
`Date` in `metadata` — is a `VpayInvalidParamsError` naming its parameter,
|
|
714
|
+
caught by `instanceof VpayError` and not a `TypeError`, with nothing sent.
|
|
715
|
+
- That an empty or all-dots `id` throws `VpayInvalidParamsError` on all twenty-one
|
|
716
|
+
id-taking methods before any request, the token request included, with one
|
|
717
|
+
fixed message that never quotes it; that an id with dots in it that is not
|
|
718
|
+
only dots (`a..b`, `.a`, `%2E%2E`) is still sent as exactly one segment;
|
|
719
|
+
and, as the premise, that WHATWG `URL` resolves `..`, `%2E%2E` and `.%2e`
|
|
720
|
+
alike.
|
|
676
721
|
- Error mapping for a Stripe-shaped `400`, an unexpected `502` HTML body, a
|
|
677
722
|
connection failure, and a token-endpoint `401` (which is never retried).
|
|
678
723
|
- Webhook verification: valid/invalid signatures, timestamp tolerance,
|
|
@@ -697,8 +742,8 @@ none of them touches the network:
|
|
|
697
742
|
both URLs and answers an invoice still `"open"` with a `payment_intent` and
|
|
698
743
|
a `hosted_invoice_url`; the four `invoiceItems` methods, including that
|
|
699
744
|
neither `currency` nor `amount` is ever in the body; and that a
|
|
700
|
-
`unit_amount` outside the safe-integer range throws `
|
|
701
|
-
request. Every body string is the one `sdks/rust/tests/resources.rs` pins,
|
|
745
|
+
`unit_amount` outside the safe-integer range throws `VpayInvalidParamsError`
|
|
746
|
+
before any request. Every body string is the one `sdks/rust/tests/resources.rs` pins,
|
|
702
747
|
asserted as a literal on both sides.
|
|
703
748
|
- **Against a real `vpay-server`, not a stub** (added 2026-09-08):
|
|
704
749
|
`src/invoices.live.test.ts` creates a customer, drafts an invoice, bills two
|
package/dist/errors.d.ts
CHANGED
|
@@ -57,6 +57,32 @@ export declare class VpayUnexpectedResponseError extends VpayError {
|
|
|
57
57
|
readonly bodyPrefix: string;
|
|
58
58
|
constructor(status: number, bodyPrefix: string);
|
|
59
59
|
}
|
|
60
|
+
/**
|
|
61
|
+
* A request parameter this SDK refuses to put on the wire, caught before
|
|
62
|
+
* anything is sent — no token is minted and no request is built. The twin
|
|
63
|
+
* of `sdks/rust`'s `Error::InvalidParams { param, message }`, and its
|
|
64
|
+
* `message` is that variant's `Display` text: `invalid request parameter
|
|
65
|
+
* {param}: {detail}`.
|
|
66
|
+
*
|
|
67
|
+
* Thrown for an amount outside `0..=2^53-1`, a value the form encoder cannot
|
|
68
|
+
* put on the wire exactly (a non-safe-integer number, a `Date`, a `Map`, a
|
|
69
|
+
* class instance), a path `id` that is empty or only dots, and a URL beside
|
|
70
|
+
* `outOfBand` on `invoices.pay`.
|
|
71
|
+
*
|
|
72
|
+
* **Not a `TypeError`, since 2026-10-21.** Every one of these was a bare
|
|
73
|
+
* `TypeError` until then, which the one narrowing this package documents —
|
|
74
|
+
* `instanceof VpayError` — missed. It cannot be both: a class has one
|
|
75
|
+
* superclass, and making `VpayError` extend `TypeError` would make every
|
|
76
|
+
* error this package throws a `TypeError`, a network failure included.
|
|
77
|
+
*/
|
|
78
|
+
export declare class VpayInvalidParamsError extends VpayError {
|
|
79
|
+
/**
|
|
80
|
+
* The offending parameter's wire name — `amount`, `unit_amount`, `id`,
|
|
81
|
+
* `success_url`, or a nested form path such as `metadata[order_id]`.
|
|
82
|
+
*/
|
|
83
|
+
readonly param: string;
|
|
84
|
+
constructor(param: string, detail: string);
|
|
85
|
+
}
|
|
60
86
|
/** A transport-level failure — DNS, TLS, connection refused, timeout. */
|
|
61
87
|
export declare class VpayTransportError extends VpayError {
|
|
62
88
|
constructor(message: string, options?: ErrorOptions);
|
package/dist/errors.js
CHANGED
|
@@ -69,6 +69,35 @@ export class VpayUnexpectedResponseError extends VpayError {
|
|
|
69
69
|
this.bodyPrefix = bodyPrefix;
|
|
70
70
|
}
|
|
71
71
|
}
|
|
72
|
+
/**
|
|
73
|
+
* A request parameter this SDK refuses to put on the wire, caught before
|
|
74
|
+
* anything is sent — no token is minted and no request is built. The twin
|
|
75
|
+
* of `sdks/rust`'s `Error::InvalidParams { param, message }`, and its
|
|
76
|
+
* `message` is that variant's `Display` text: `invalid request parameter
|
|
77
|
+
* {param}: {detail}`.
|
|
78
|
+
*
|
|
79
|
+
* Thrown for an amount outside `0..=2^53-1`, a value the form encoder cannot
|
|
80
|
+
* put on the wire exactly (a non-safe-integer number, a `Date`, a `Map`, a
|
|
81
|
+
* class instance), a path `id` that is empty or only dots, and a URL beside
|
|
82
|
+
* `outOfBand` on `invoices.pay`.
|
|
83
|
+
*
|
|
84
|
+
* **Not a `TypeError`, since 2026-10-21.** Every one of these was a bare
|
|
85
|
+
* `TypeError` until then, which the one narrowing this package documents —
|
|
86
|
+
* `instanceof VpayError` — missed. It cannot be both: a class has one
|
|
87
|
+
* superclass, and making `VpayError` extend `TypeError` would make every
|
|
88
|
+
* error this package throws a `TypeError`, a network failure included.
|
|
89
|
+
*/
|
|
90
|
+
export class VpayInvalidParamsError extends VpayError {
|
|
91
|
+
/**
|
|
92
|
+
* The offending parameter's wire name — `amount`, `unit_amount`, `id`,
|
|
93
|
+
* `success_url`, or a nested form path such as `metadata[order_id]`.
|
|
94
|
+
*/
|
|
95
|
+
param;
|
|
96
|
+
constructor(param, detail) {
|
|
97
|
+
super(`invalid request parameter ${param}: ${detail}`);
|
|
98
|
+
this.param = param;
|
|
99
|
+
}
|
|
100
|
+
}
|
|
72
101
|
/** A transport-level failure — DNS, TLS, connection refused, timeout. */
|
|
73
102
|
export class VpayTransportError extends VpayError {
|
|
74
103
|
constructor(message, options) {
|
package/dist/form.d.ts
CHANGED
|
@@ -30,6 +30,9 @@
|
|
|
30
30
|
* is decimal integers with no separators (docs/flows/money.md bans floating
|
|
31
31
|
* point in the money path outright), so a float or a `1e21` is a bug in the
|
|
32
32
|
* caller, not a value to transmit approximately.
|
|
33
|
+
*
|
|
34
|
+
* Both refusals are a {@link VpayInvalidParamsError} naming the value's form
|
|
35
|
+
* path (`amount`, `metadata[order_id]`), thrown before anything is sent.
|
|
33
36
|
*/
|
|
34
37
|
/** A value the form encoder can flatten. `null` is treated like `undefined`: omitted. */
|
|
35
38
|
export type FormValue = string | number | boolean | undefined | null | readonly FormValue[] | {
|
package/dist/form.js
CHANGED
|
@@ -30,8 +30,12 @@
|
|
|
30
30
|
* is decimal integers with no separators (docs/flows/money.md bans floating
|
|
31
31
|
* point in the money path outright), so a float or a `1e21` is a bug in the
|
|
32
32
|
* caller, not a value to transmit approximately.
|
|
33
|
+
*
|
|
34
|
+
* Both refusals are a {@link VpayInvalidParamsError} naming the value's form
|
|
35
|
+
* path (`amount`, `metadata[order_id]`), thrown before anything is sent.
|
|
33
36
|
*/
|
|
34
|
-
|
|
37
|
+
import { VpayInvalidParamsError } from "./errors.js";
|
|
38
|
+
/** Renders a path back into the bracket syntax, for an error's `param` only. */
|
|
35
39
|
function describePath(path) {
|
|
36
40
|
const [head, ...rest] = path;
|
|
37
41
|
return `${head ?? ""}${rest.map((segment) => `[${segment}]`).join("")}`;
|
|
@@ -53,7 +57,7 @@ function isPlainObject(value) {
|
|
|
53
57
|
/** Renders a number for the wire, refusing anything that is not an exact decimal integer. */
|
|
54
58
|
function encodeNumber(value, path) {
|
|
55
59
|
if (!Number.isSafeInteger(value)) {
|
|
56
|
-
throw new
|
|
60
|
+
throw new VpayInvalidParamsError(describePath(path), `must be a safe integer, got ${value}`);
|
|
57
61
|
}
|
|
58
62
|
const text = String(value);
|
|
59
63
|
// Subsumed by the check above as the language stands today — JavaScript
|
|
@@ -63,7 +67,7 @@ function encodeNumber(value, path) {
|
|
|
63
67
|
// format actually requires: no test can kill this branch on its own, and
|
|
64
68
|
// the comment says so rather than implying it is covered.
|
|
65
69
|
if (text.includes("e") || text.includes("E")) {
|
|
66
|
-
throw new
|
|
70
|
+
throw new VpayInvalidParamsError(describePath(path), `must encode as a plain decimal integer, got ${text}`);
|
|
67
71
|
}
|
|
68
72
|
return text;
|
|
69
73
|
}
|
|
@@ -77,7 +81,7 @@ function flattenValue(path, value, out) {
|
|
|
77
81
|
}
|
|
78
82
|
if (typeof value === "object") {
|
|
79
83
|
if (!isPlainObject(value)) {
|
|
80
|
-
throw new
|
|
84
|
+
throw new VpayInvalidParamsError(describePath(path), `must be a plain object, array or scalar, got ${value.constructor?.name ?? "an object"}`);
|
|
81
85
|
}
|
|
82
86
|
for (const [childKey, childValue] of Object.entries(value)) {
|
|
83
87
|
flattenValue([...path, childKey], childValue, out);
|
package/dist/index.d.ts
CHANGED
|
@@ -10,6 +10,6 @@ export { mintClientAssertion, type MintClientAssertionOptions, } from "./auth.js
|
|
|
10
10
|
export { verifyWebhook, type VerifyWebhookOptions } from "./webhooks.js";
|
|
11
11
|
/** This package's own version, as sent in the `vpay-sdk-node/<version>` User-Agent. */
|
|
12
12
|
export { SDK_VERSION } from "./version.js";
|
|
13
|
-
export { VpayError, VpayApiError, VpayAuthError, VpayUnexpectedResponseError, VpayTransportError, VpayConfigError, WebhookSignatureError, } from "./errors.js";
|
|
13
|
+
export { VpayError, VpayApiError, VpayAuthError, VpayUnexpectedResponseError, VpayTransportError, VpayConfigError, VpayInvalidParamsError, WebhookSignatureError, } from "./errors.js";
|
|
14
14
|
export type { AccountHolder, RetrieveAccountHolderParams, CheckoutSession, CheckoutSessionStatus, CheckoutSessionUiMode, CheckoutPaymentStatus, CreateCheckoutSessionParams, ListCheckoutSessionsParams, Customer, DeletedCustomer, Address, AddressParams, CreateCustomerParams, UpdateCustomerParams, ListCustomersParams, Invoice, InvoiceLine, InvoiceStatus, InvoiceStatusTransitions, DeletedInvoice, DeletedInvoiceItem, CreateInvoiceParams, UpdateInvoiceParams, ListInvoicesParams, PayInvoiceParams, HostedPayInvoiceParams, OutOfBandPayInvoiceParams, OutOfBandParams, OutOfBandMethod, OutOfBandPayment, CreateInvoiceItemParams, UpdateInvoiceItemParams, PaymentIntent, PaymentIntentStatus, PaymentMethodType, NextAction, LastPaymentError, FailureCode, Refund, RefundStatus, Event, KnownEventType, Balance, BalanceAmount, List, ListParams, ListPaymentIntentsParams, ListEventsParams, CreatePaymentIntentParams, ConfirmPaymentIntentParams, MtnMomoPaymentMethodData, OrangeMoneyPaymentMethodData, CreateRefundParams, RefundDestination, UpdateRefundParams, ListRefundsParams, RequestOptions, } from "./types.js";
|
|
15
15
|
export { isCheckoutSessionEvent, isInvoiceEvent, isPaymentIntentEvent, isRefundEvent, } from "./types.js";
|
package/dist/index.js
CHANGED
|
@@ -10,5 +10,5 @@ export { mintClientAssertion, } from "./auth.js";
|
|
|
10
10
|
export { verifyWebhook } from "./webhooks.js";
|
|
11
11
|
/** This package's own version, as sent in the `vpay-sdk-node/<version>` User-Agent. */
|
|
12
12
|
export { SDK_VERSION } from "./version.js";
|
|
13
|
-
export { VpayError, VpayApiError, VpayAuthError, VpayUnexpectedResponseError, VpayTransportError, VpayConfigError, WebhookSignatureError, } from "./errors.js";
|
|
13
|
+
export { VpayError, VpayApiError, VpayAuthError, VpayUnexpectedResponseError, VpayTransportError, VpayConfigError, VpayInvalidParamsError, WebhookSignatureError, } from "./errors.js";
|
|
14
14
|
export { isCheckoutSessionEvent, isInvoiceEvent, isPaymentIntentEvent, isRefundEvent, } from "./types.js";
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { pathSegment } from "../validate.js";
|
|
1
2
|
const INSPECT_CUSTOM = Symbol.for("nodejs.util.inspect.custom");
|
|
2
3
|
/**
|
|
3
4
|
* The `url` of a hosted session with its fragment redacted.
|
|
@@ -89,7 +90,7 @@ export class CheckoutSessionsResource {
|
|
|
89
90
|
}
|
|
90
91
|
/** `GET /v1/checkout/sessions/{id}`. Answers the session with `client_secret`. */
|
|
91
92
|
async retrieve(id) {
|
|
92
|
-
return withRedactedInspect(await this.#http.request("GET", `/checkout/sessions/${
|
|
93
|
+
return withRedactedInspect(await this.#http.request("GET", `/checkout/sessions/${pathSegment(id)}`));
|
|
93
94
|
}
|
|
94
95
|
/** `GET /v1/checkout/sessions`. List items never carry `client_secret`. */
|
|
95
96
|
async list(params) {
|
|
@@ -106,7 +107,7 @@ export class CheckoutSessionsResource {
|
|
|
106
107
|
* whose intent already has a live charge is refused `409`.
|
|
107
108
|
*/
|
|
108
109
|
async expire(id, options) {
|
|
109
|
-
return withRedactedInspect(await this.#http.request("POST", `/checkout/sessions/${
|
|
110
|
+
return withRedactedInspect(await this.#http.request("POST", `/checkout/sessions/${pathSegment(id)}/expire`, {}, options));
|
|
110
111
|
}
|
|
111
112
|
}
|
|
112
113
|
/**
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { pathSegment } from "../validate.js";
|
|
1
2
|
/**
|
|
2
3
|
* One patch field, in the wire's own three-state encoding.
|
|
3
4
|
*
|
|
@@ -79,7 +80,7 @@ export class CustomersResource {
|
|
|
79
80
|
}
|
|
80
81
|
/** `GET /v1/customers/{id}`. */
|
|
81
82
|
async retrieve(id) {
|
|
82
|
-
return this.#http.request("GET", `/customers/${
|
|
83
|
+
return this.#http.request("GET", `/customers/${pathSegment(id)}`);
|
|
83
84
|
}
|
|
84
85
|
/**
|
|
85
86
|
* `POST /v1/customers/{id}` — the update.
|
|
@@ -116,7 +117,7 @@ export class CustomersResource {
|
|
|
116
117
|
if (params.metadata !== undefined) {
|
|
117
118
|
body["metadata"] = params.metadata;
|
|
118
119
|
}
|
|
119
|
-
return this.#http.request("POST", `/customers/${
|
|
120
|
+
return this.#http.request("POST", `/customers/${pathSegment(id)}`, body, options);
|
|
120
121
|
}
|
|
121
122
|
/** `GET /v1/customers`. */
|
|
122
123
|
async list(params) {
|
|
@@ -152,6 +153,6 @@ export class CustomersResource {
|
|
|
152
153
|
* than a `404`.
|
|
153
154
|
*/
|
|
154
155
|
async del(id, options) {
|
|
155
|
-
return this.#http.request("DELETE", `/customers/${
|
|
156
|
+
return this.#http.request("DELETE", `/customers/${pathSegment(id)}`, undefined, options);
|
|
156
157
|
}
|
|
157
158
|
}
|
|
@@ -153,7 +153,8 @@ export declare class InvoicesResource {
|
|
|
153
153
|
* `invoice.paid` emitted, no intent, no checkout, no URL, and nothing
|
|
154
154
|
* verified or posted to any ledger. While an intent that is not canceled is
|
|
155
155
|
* attached it is the same `409` as `void`; cancel the intent first. A URL
|
|
156
|
-
* beside `outOfBand` is refused with a `
|
|
156
|
+
* beside `outOfBand` is refused with a `VpayInvalidParamsError` naming it,
|
|
157
|
+
* before any request.
|
|
157
158
|
*/
|
|
158
159
|
pay(id: string, params: PayInvoiceParams, options?: RequestOptions): Promise<Invoice>;
|
|
159
160
|
}
|
|
@@ -177,7 +178,7 @@ export declare class InvoiceItemsResource {
|
|
|
177
178
|
* `POST /v1/invoice_items` — adds a line to a draft, and the invoice's
|
|
178
179
|
* `amount_due` moves with it.
|
|
179
180
|
*
|
|
180
|
-
* Throws `
|
|
181
|
+
* Throws `VpayInvalidParamsError` before any request when `unit_amount` is not a
|
|
181
182
|
* non-negative safe integer; a `400` naming `invoice` when it is not one of
|
|
182
183
|
* your drafts.
|
|
183
184
|
*/
|
|
@@ -188,7 +189,7 @@ export declare class InvoiceItemsResource {
|
|
|
188
189
|
* `POST /v1/invoice_items/{id}` — **draft parent only**, with `amount`
|
|
189
190
|
* recomputed.
|
|
190
191
|
*
|
|
191
|
-
* Throws `
|
|
192
|
+
* Throws `VpayInvalidParamsError` before any request for a `unit_amount` outside the
|
|
192
193
|
* bound; a `409` once the parent is no longer a draft.
|
|
193
194
|
*/
|
|
194
195
|
update(id: string, params: UpdateInvoiceItemParams, options?: RequestOptions): Promise<InvoiceLine>;
|
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { VpayInvalidParamsError } from "../errors.js";
|
|
2
|
+
import { assertIntegerAmount, pathSegment } from "../validate.js";
|
|
2
3
|
/**
|
|
3
4
|
* One patch field, in the wire's own three-state encoding.
|
|
4
5
|
*
|
|
@@ -61,7 +62,7 @@ export class InvoicesResource {
|
|
|
61
62
|
* never existed.
|
|
62
63
|
*/
|
|
63
64
|
async retrieve(id) {
|
|
64
|
-
return this.#http.request("GET", `/invoices/${
|
|
65
|
+
return this.#http.request("GET", `/invoices/${pathSegment(id)}`);
|
|
65
66
|
}
|
|
66
67
|
/**
|
|
67
68
|
* `POST /v1/invoices/{id}` — patches a **draft**.
|
|
@@ -88,7 +89,7 @@ export class InvoicesResource {
|
|
|
88
89
|
if (params.metadata !== undefined) {
|
|
89
90
|
body["metadata"] = params.metadata;
|
|
90
91
|
}
|
|
91
|
-
return this.#http.request("POST", `/invoices/${
|
|
92
|
+
return this.#http.request("POST", `/invoices/${pathSegment(id)}`, body, options);
|
|
92
93
|
}
|
|
93
94
|
/** `GET /v1/invoices` — newest first. */
|
|
94
95
|
async list(params) {
|
|
@@ -107,7 +108,7 @@ export class InvoicesResource {
|
|
|
107
108
|
* delete answer the original `{deleted: true}` rather than a `404`.
|
|
108
109
|
*/
|
|
109
110
|
async del(id, options) {
|
|
110
|
-
return this.#http.request("DELETE", `/invoices/${
|
|
111
|
+
return this.#http.request("DELETE", `/invoices/${pathSegment(id)}`, undefined, options);
|
|
111
112
|
}
|
|
112
113
|
/**
|
|
113
114
|
* `POST /v1/invoices/{id}/finalize` — issues the document. **Draft only.**
|
|
@@ -189,7 +190,8 @@ export class InvoicesResource {
|
|
|
189
190
|
* `invoice.paid` emitted, no intent, no checkout, no URL, and nothing
|
|
190
191
|
* verified or posted to any ledger. While an intent that is not canceled is
|
|
191
192
|
* attached it is the same `409` as `void`; cancel the intent first. A URL
|
|
192
|
-
* beside `outOfBand` is refused with a `
|
|
193
|
+
* beside `outOfBand` is refused with a `VpayInvalidParamsError` naming it,
|
|
194
|
+
* before any request.
|
|
193
195
|
*/
|
|
194
196
|
async pay(id, params, options) {
|
|
195
197
|
const outOfBand = params.outOfBand;
|
|
@@ -200,7 +202,7 @@ export class InvoicesResource {
|
|
|
200
202
|
for (const param of ["success_url", "cancel_url"]) {
|
|
201
203
|
const value = params[param];
|
|
202
204
|
if (value !== undefined && value !== "") {
|
|
203
|
-
throw new
|
|
205
|
+
throw new VpayInvalidParamsError(param, "has no meaning when recording a payment made outside vpay (outOfBand is set)");
|
|
204
206
|
}
|
|
205
207
|
}
|
|
206
208
|
}
|
|
@@ -216,7 +218,7 @@ export class InvoicesResource {
|
|
|
216
218
|
},
|
|
217
219
|
}
|
|
218
220
|
: { success_url: params.success_url, cancel_url: params.cancel_url };
|
|
219
|
-
return this.#http.request("POST", `/invoices/${
|
|
221
|
+
return this.#http.request("POST", `/invoices/${pathSegment(id)}/pay`, body, options);
|
|
220
222
|
}
|
|
221
223
|
/**
|
|
222
224
|
* The three transitions that take no parameters, which are one shape:
|
|
@@ -228,7 +230,7 @@ export class InvoicesResource {
|
|
|
228
230
|
* that is easy to get wrong in only one of them.
|
|
229
231
|
*/
|
|
230
232
|
async #transition(id, name, options) {
|
|
231
|
-
return this.#http.request("POST", `/invoices/${
|
|
233
|
+
return this.#http.request("POST", `/invoices/${pathSegment(id)}/${name}`, {}, options);
|
|
232
234
|
}
|
|
233
235
|
}
|
|
234
236
|
/**
|
|
@@ -253,7 +255,7 @@ export class InvoiceItemsResource {
|
|
|
253
255
|
* `POST /v1/invoice_items` — adds a line to a draft, and the invoice's
|
|
254
256
|
* `amount_due` moves with it.
|
|
255
257
|
*
|
|
256
|
-
* Throws `
|
|
258
|
+
* Throws `VpayInvalidParamsError` before any request when `unit_amount` is not a
|
|
257
259
|
* non-negative safe integer; a `400` naming `invoice` when it is not one of
|
|
258
260
|
* your drafts.
|
|
259
261
|
*/
|
|
@@ -271,13 +273,13 @@ export class InvoiceItemsResource {
|
|
|
271
273
|
}
|
|
272
274
|
/** `GET /v1/invoice_items/{id}` — readable whatever the parent's status. */
|
|
273
275
|
async retrieve(id) {
|
|
274
|
-
return this.#http.request("GET", `/invoice_items/${
|
|
276
|
+
return this.#http.request("GET", `/invoice_items/${pathSegment(id)}`);
|
|
275
277
|
}
|
|
276
278
|
/**
|
|
277
279
|
* `POST /v1/invoice_items/{id}` — **draft parent only**, with `amount`
|
|
278
280
|
* recomputed.
|
|
279
281
|
*
|
|
280
|
-
* Throws `
|
|
282
|
+
* Throws `VpayInvalidParamsError` before any request for a `unit_amount` outside the
|
|
281
283
|
* bound; a `409` once the parent is no longer a draft.
|
|
282
284
|
*/
|
|
283
285
|
async update(id, params, options) {
|
|
@@ -294,13 +296,13 @@ export class InvoiceItemsResource {
|
|
|
294
296
|
if (params.unit_amount !== undefined) {
|
|
295
297
|
body["unit_amount"] = params.unit_amount;
|
|
296
298
|
}
|
|
297
|
-
return this.#http.request("POST", `/invoice_items/${
|
|
299
|
+
return this.#http.request("POST", `/invoice_items/${pathSegment(id)}`, body, options);
|
|
298
300
|
}
|
|
299
301
|
/**
|
|
300
302
|
* `DELETE /v1/invoice_items/{id}` — **draft parent only**, and the invoice
|
|
301
303
|
* re-totals.
|
|
302
304
|
*/
|
|
303
305
|
async del(id, options) {
|
|
304
|
-
return this.#http.request("DELETE", `/invoice_items/${
|
|
306
|
+
return this.#http.request("DELETE", `/invoice_items/${pathSegment(id)}`, undefined, options);
|
|
305
307
|
}
|
|
306
308
|
}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { assertIntegerAmount } from "../validate.js";
|
|
1
|
+
import { assertIntegerAmount, pathSegment } from "../validate.js";
|
|
2
2
|
/** `/v1/payment_intents` — docs/flows/merchant-auth.md, "Resources". */
|
|
3
3
|
export class PaymentIntentsResource {
|
|
4
4
|
#http;
|
|
@@ -24,7 +24,7 @@ export class PaymentIntentsResource {
|
|
|
24
24
|
return this.#http.request("POST", "/payment_intents", body, options);
|
|
25
25
|
}
|
|
26
26
|
async retrieve(id) {
|
|
27
|
-
return this.#http.request("GET", `/payment_intents/${
|
|
27
|
+
return this.#http.request("GET", `/payment_intents/${pathSegment(id)}`);
|
|
28
28
|
}
|
|
29
29
|
async confirm(id, params, options) {
|
|
30
30
|
const body = {
|
|
@@ -33,10 +33,10 @@ export class PaymentIntentsResource {
|
|
|
33
33
|
if ("return_url" in params) {
|
|
34
34
|
body["return_url"] = params.return_url;
|
|
35
35
|
}
|
|
36
|
-
return this.#http.request("POST", `/payment_intents/${
|
|
36
|
+
return this.#http.request("POST", `/payment_intents/${pathSegment(id)}/confirm`, body, options);
|
|
37
37
|
}
|
|
38
38
|
async cancel(id, options) {
|
|
39
|
-
return this.#http.request("POST", `/payment_intents/${
|
|
39
|
+
return this.#http.request("POST", `/payment_intents/${pathSegment(id)}/cancel`, {}, options);
|
|
40
40
|
}
|
|
41
41
|
/** `GET /v1/payment_intents`, newest first; `customer` narrows it. */
|
|
42
42
|
async list(params) {
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { assertIntegerAmount } from "../validate.js";
|
|
1
|
+
import { assertIntegerAmount, pathSegment } from "../validate.js";
|
|
2
2
|
/**
|
|
3
3
|
* The rail-agnostic envelope, built around the rail-specific interior.
|
|
4
4
|
*
|
|
@@ -62,7 +62,7 @@ export class RefundsResource {
|
|
|
62
62
|
* order.
|
|
63
63
|
*/
|
|
64
64
|
async retrieve(id) {
|
|
65
|
-
return this.#http.request("GET", `/refunds/${
|
|
65
|
+
return this.#http.request("GET", `/refunds/${pathSegment(id)}`);
|
|
66
66
|
}
|
|
67
67
|
/**
|
|
68
68
|
* `POST /v1/refunds/{id}` — the **metadata** update, and nothing else.
|
|
@@ -82,7 +82,7 @@ export class RefundsResource {
|
|
|
82
82
|
if (params.metadata !== undefined) {
|
|
83
83
|
body["metadata"] = params.metadata;
|
|
84
84
|
}
|
|
85
|
-
return this.#http.request("POST", `/refunds/${
|
|
85
|
+
return this.#http.request("POST", `/refunds/${pathSegment(id)}`, body, options);
|
|
86
86
|
}
|
|
87
87
|
/** `GET /v1/refunds` — this merchant's refunds, newest first. */
|
|
88
88
|
async list(params) {
|
|
@@ -101,6 +101,6 @@ export class RefundsResource {
|
|
|
101
101
|
* rail was ever told to start. See the module doc.
|
|
102
102
|
*/
|
|
103
103
|
async cancel(id, options) {
|
|
104
|
-
return this.#http.request("POST", `/refunds/${
|
|
104
|
+
return this.#http.request("POST", `/refunds/${pathSegment(id)}/cancel`, {}, options);
|
|
105
105
|
}
|
|
106
106
|
}
|
package/dist/types.d.ts
CHANGED
|
@@ -1109,8 +1109,8 @@ export interface CreateInvoiceItemParams {
|
|
|
1109
1109
|
quantity?: number | undefined;
|
|
1110
1110
|
/**
|
|
1111
1111
|
* The price of one, in integer minor units. A non-integer, a negative, or
|
|
1112
|
-
* anything past `Number.MAX_SAFE_INTEGER` throws `
|
|
1113
|
-
* request — `sdks/rust` refuses the identical set.
|
|
1112
|
+
* anything past `Number.MAX_SAFE_INTEGER` throws `VpayInvalidParamsError`
|
|
1113
|
+
* before any request — `sdks/rust` refuses the identical set.
|
|
1114
1114
|
*/
|
|
1115
1115
|
unit_amount: number;
|
|
1116
1116
|
}
|
|
@@ -1134,7 +1134,8 @@ export interface UpdateInvoiceItemParams {
|
|
|
1134
1134
|
export interface CreatePaymentIntentParams {
|
|
1135
1135
|
/**
|
|
1136
1136
|
* Integer minor units. A non-integer, a negative, or anything past
|
|
1137
|
-
* `Number.MAX_SAFE_INTEGER` throws `
|
|
1137
|
+
* `Number.MAX_SAFE_INTEGER` throws `VpayInvalidParamsError` before any
|
|
1138
|
+
* request.
|
|
1138
1139
|
*/
|
|
1139
1140
|
amount: number;
|
|
1140
1141
|
currency: string;
|
|
@@ -1164,7 +1165,8 @@ export type OrangeMoneyPaymentMethodData = {
|
|
|
1164
1165
|
/**
|
|
1165
1166
|
* Notch Pay (RFC-0012, 2026-10-20): a push rail, so the same instrument as
|
|
1166
1167
|
* MTN's, under this rail's own code. The server's adapter is proven against a
|
|
1167
|
-
* stub
|
|
1168
|
+
* stub and took three test-mode payments through Notch Pay's real sandbox on
|
|
1169
|
+
* 2026-10-21 (RFC-0012 step 5); it has never called Notch Pay's production.
|
|
1168
1170
|
*/
|
|
1169
1171
|
export type NotchPayPaymentMethodData = {
|
|
1170
1172
|
type: "notchpay";
|
package/dist/validate.d.ts
CHANGED
|
@@ -1,5 +1,8 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Shared request-shape validation, run before anything reaches the wire.
|
|
3
|
+
*
|
|
4
|
+
* Every refusal here is a {@link VpayInvalidParamsError}, so it is caught by
|
|
5
|
+
* the same `instanceof VpayError` as every other error this package throws.
|
|
3
6
|
*/
|
|
4
7
|
/**
|
|
5
8
|
* Mirrors `@vpay/api-client`'s `formatAmount` integer check
|
|
@@ -12,5 +15,25 @@
|
|
|
12
15
|
* `Number.MAX_SAFE_INTEGER`, where the value the caller wrote and the value
|
|
13
16
|
* JavaScript holds have already diverged. An amount must be a non-negative
|
|
14
17
|
* **safe** integer or nothing goes to the wire.
|
|
18
|
+
*
|
|
19
|
+
* @throws {VpayInvalidParamsError} naming `field`.
|
|
15
20
|
*/
|
|
16
21
|
export declare function assertIntegerAmount(amount: number, field?: string): void;
|
|
22
|
+
/**
|
|
23
|
+
* Percent-encodes one merchant-supplied id for a URL path, and refuses the
|
|
24
|
+
* ids escaping cannot keep in place.
|
|
25
|
+
*
|
|
26
|
+
* `encodeURIComponent` keeps a `/`, `?` or `#` inside its segment, but it
|
|
27
|
+
* leaves `.` alone, so `""`, `.` and `..` pass through, and as whole segments
|
|
28
|
+
* `fetch`'s URL parser resolves them before the request leaves:
|
|
29
|
+
* `customers.del("..")` was a `DELETE /v1/`. Escaping the dots would not
|
|
30
|
+
* help, because WHATWG parsing reads `%2E%2E` as `..` too. So an empty or
|
|
31
|
+
* all-dots id throws before anything is sent, the token request included —
|
|
32
|
+
* `sdks/rust`'s `path_segment` refuses the same ids with
|
|
33
|
+
* `Error::InvalidParams`. `...` is refused by decision rather than by the
|
|
34
|
+
* standard: `docs/reference/rails.md` § "A path segment the caller chose"
|
|
35
|
+
* says why.
|
|
36
|
+
*
|
|
37
|
+
* @throws {VpayInvalidParamsError} naming `id`.
|
|
38
|
+
*/
|
|
39
|
+
export declare function pathSegment(id: string): string;
|
package/dist/validate.js
CHANGED
|
@@ -1,6 +1,10 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Shared request-shape validation, run before anything reaches the wire.
|
|
3
|
+
*
|
|
4
|
+
* Every refusal here is a {@link VpayInvalidParamsError}, so it is caught by
|
|
5
|
+
* the same `instanceof VpayError` as every other error this package throws.
|
|
3
6
|
*/
|
|
7
|
+
import { VpayInvalidParamsError } from "./errors.js";
|
|
4
8
|
/**
|
|
5
9
|
* Mirrors `@vpay/api-client`'s `formatAmount` integer check
|
|
6
10
|
* (frontends/packages/api-client/src/index.ts) and
|
|
@@ -12,9 +16,43 @@
|
|
|
12
16
|
* `Number.MAX_SAFE_INTEGER`, where the value the caller wrote and the value
|
|
13
17
|
* JavaScript holds have already diverged. An amount must be a non-negative
|
|
14
18
|
* **safe** integer or nothing goes to the wire.
|
|
19
|
+
*
|
|
20
|
+
* @throws {VpayInvalidParamsError} naming `field`.
|
|
15
21
|
*/
|
|
16
22
|
export function assertIntegerAmount(amount, field = "amount") {
|
|
17
23
|
if (!Number.isSafeInteger(amount) || amount < 0) {
|
|
18
|
-
throw new
|
|
24
|
+
throw new VpayInvalidParamsError(field, `must be a non-negative safe integer in minor units, got ${amount}`);
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
/**
|
|
28
|
+
* The one message {@link pathSegment}'s refusal carries, whatever the id.
|
|
29
|
+
*
|
|
30
|
+
* Fixed so that it cannot quote the id, and byte-identical to `sdks/rust`'s
|
|
31
|
+
* `DOT_SEGMENT_REFUSAL`. {@link VpayInvalidParamsError} puts the same
|
|
32
|
+
* `invalid request parameter id: ` prefix in front of it that crate's
|
|
33
|
+
* `Error::InvalidParams` displays.
|
|
34
|
+
*/
|
|
35
|
+
const DOT_SEGMENT_REFUSAL = "an id that is empty or only dots would address a different endpoint once the URL is parsed; nothing was sent";
|
|
36
|
+
/**
|
|
37
|
+
* Percent-encodes one merchant-supplied id for a URL path, and refuses the
|
|
38
|
+
* ids escaping cannot keep in place.
|
|
39
|
+
*
|
|
40
|
+
* `encodeURIComponent` keeps a `/`, `?` or `#` inside its segment, but it
|
|
41
|
+
* leaves `.` alone, so `""`, `.` and `..` pass through, and as whole segments
|
|
42
|
+
* `fetch`'s URL parser resolves them before the request leaves:
|
|
43
|
+
* `customers.del("..")` was a `DELETE /v1/`. Escaping the dots would not
|
|
44
|
+
* help, because WHATWG parsing reads `%2E%2E` as `..` too. So an empty or
|
|
45
|
+
* all-dots id throws before anything is sent, the token request included —
|
|
46
|
+
* `sdks/rust`'s `path_segment` refuses the same ids with
|
|
47
|
+
* `Error::InvalidParams`. `...` is refused by decision rather than by the
|
|
48
|
+
* standard: `docs/reference/rails.md` § "A path segment the caller chose"
|
|
49
|
+
* says why.
|
|
50
|
+
*
|
|
51
|
+
* @throws {VpayInvalidParamsError} naming `id`.
|
|
52
|
+
*/
|
|
53
|
+
export function pathSegment(id) {
|
|
54
|
+
if (/^\.*$/u.test(id)) {
|
|
55
|
+
throw new VpayInvalidParamsError("id", DOT_SEGMENT_REFUSAL);
|
|
19
56
|
}
|
|
57
|
+
return encodeURIComponent(id);
|
|
20
58
|
}
|
package/dist/version.d.ts
CHANGED
package/dist/version.js
CHANGED
|
@@ -9,4 +9,4 @@
|
|
|
9
9
|
* releases. A hand-written constant has no such landmine — it just needs to
|
|
10
10
|
* be bumped alongside `package.json`'s own `version` field.
|
|
11
11
|
*/
|
|
12
|
-
export const SDK_VERSION = "0.
|
|
12
|
+
export const SDK_VERSION = "0.10.0"; // x-release-please-version
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@vaam-apps/vpay-sdk",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.10.0",
|
|
4
4
|
"description": "The Node.js merchant SDK for vpay's /v1 API: OAuth2 private_key_jwt, payment intents, checkout sessions, refunds, events and webhook verification.",
|
|
5
5
|
"license": "Apache-2.0",
|
|
6
6
|
"publishConfig": {
|