@vaam-apps/vpay-sdk 0.9.0 → 0.9.1
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 +22 -0
- package/dist/resources/checkout-sessions.js +3 -2
- package/dist/resources/customers.js +4 -3
- package/dist/resources/invoices.js +9 -9
- package/dist/resources/payment-intents.js +4 -4
- package/dist/resources/refunds.js +4 -4
- package/dist/validate.d.ts +19 -0
- package/dist/validate.js +32 -0
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -591,6 +591,22 @@ VPAY_AUDIENCE=https://api.vpay.example/v1/oauth/token \
|
|
|
591
591
|
|
|
592
592
|
## Status
|
|
593
593
|
|
|
594
|
+
**Behaviour change, 2026-10-21: an empty or all-dots `id` is refused before
|
|
595
|
+
any request.** Every one of the twenty-one methods that put an `id` in the
|
|
596
|
+
path now rejects with a `TypeError` for `""`, `.`, `..` or any longer run of
|
|
597
|
+
dots, without minting a token or sending anything. No signature changed —
|
|
598
|
+
each already returned a `Promise` — but the outcome for those inputs did.
|
|
599
|
+
They used to be sent, and `fetch`'s URL parser resolved them before the
|
|
600
|
+
request left, so `customers.del("..")` was a `DELETE /v1/` and
|
|
601
|
+
`paymentIntents.retrieve("")` a `GET /v1/payment_intents/`; escaping cannot
|
|
602
|
+
help, because the parser reads `%2E%2E` as `..` too. What vpay answered for
|
|
603
|
+
each is on
|
|
604
|
+
[the dated verification page](../../docs/status/verification/2026-10-21-sdk-dot-segment-ids.md).
|
|
605
|
+
A caller catching `VpayApiError` for those inputs now gets a `TypeError`,
|
|
606
|
+
and so does one narrowing on `VpayError`: like the amount refusal, it is not
|
|
607
|
+
one (a dated gap in [docs/sdks/parity.md](../../docs/sdks/parity.md)).
|
|
608
|
+
`sdks/rust` refuses the same ids with `Error::InvalidParams`.
|
|
609
|
+
|
|
594
610
|
**Corrected 2026-09-03.** This section used to say "what it does **not**
|
|
595
611
|
have is a single `/v1` resource route" — true when it was written on
|
|
596
612
|
2026-09-02, and false since Step 2 landed the next day.
|
|
@@ -673,6 +689,12 @@ none of them touches the network:
|
|
|
673
689
|
`util.inspect` while `JSON.stringify` stays faithful.
|
|
674
690
|
- The form encoder's nested-object/array/boolean/percent-encoding rules, and
|
|
675
691
|
that a non-integer `amount` throws `TypeError` before any request is sent.
|
|
692
|
+
- That an empty or all-dots `id` throws `TypeError` on all twenty-one
|
|
693
|
+
id-taking methods before any request, the token request included, with one
|
|
694
|
+
fixed message that never quotes it; that an id with dots in it that is not
|
|
695
|
+
only dots (`a..b`, `.a`, `%2E%2E`) is still sent as exactly one segment;
|
|
696
|
+
and, as the premise, that WHATWG `URL` resolves `..`, `%2E%2E` and `.%2e`
|
|
697
|
+
alike.
|
|
676
698
|
- Error mapping for a Stripe-shaped `400`, an unexpected `502` HTML body, a
|
|
677
699
|
connection failure, and a token-endpoint `401` (which is never retried).
|
|
678
700
|
- Webhook verification: valid/invalid signatures, timestamp tolerance,
|
|
@@ -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
|
}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { assertIntegerAmount } from "../validate.js";
|
|
1
|
+
import { assertIntegerAmount, pathSegment } from "../validate.js";
|
|
2
2
|
/**
|
|
3
3
|
* One patch field, in the wire's own three-state encoding.
|
|
4
4
|
*
|
|
@@ -61,7 +61,7 @@ export class InvoicesResource {
|
|
|
61
61
|
* never existed.
|
|
62
62
|
*/
|
|
63
63
|
async retrieve(id) {
|
|
64
|
-
return this.#http.request("GET", `/invoices/${
|
|
64
|
+
return this.#http.request("GET", `/invoices/${pathSegment(id)}`);
|
|
65
65
|
}
|
|
66
66
|
/**
|
|
67
67
|
* `POST /v1/invoices/{id}` — patches a **draft**.
|
|
@@ -88,7 +88,7 @@ export class InvoicesResource {
|
|
|
88
88
|
if (params.metadata !== undefined) {
|
|
89
89
|
body["metadata"] = params.metadata;
|
|
90
90
|
}
|
|
91
|
-
return this.#http.request("POST", `/invoices/${
|
|
91
|
+
return this.#http.request("POST", `/invoices/${pathSegment(id)}`, body, options);
|
|
92
92
|
}
|
|
93
93
|
/** `GET /v1/invoices` — newest first. */
|
|
94
94
|
async list(params) {
|
|
@@ -107,7 +107,7 @@ export class InvoicesResource {
|
|
|
107
107
|
* delete answer the original `{deleted: true}` rather than a `404`.
|
|
108
108
|
*/
|
|
109
109
|
async del(id, options) {
|
|
110
|
-
return this.#http.request("DELETE", `/invoices/${
|
|
110
|
+
return this.#http.request("DELETE", `/invoices/${pathSegment(id)}`, undefined, options);
|
|
111
111
|
}
|
|
112
112
|
/**
|
|
113
113
|
* `POST /v1/invoices/{id}/finalize` — issues the document. **Draft only.**
|
|
@@ -216,7 +216,7 @@ export class InvoicesResource {
|
|
|
216
216
|
},
|
|
217
217
|
}
|
|
218
218
|
: { success_url: params.success_url, cancel_url: params.cancel_url };
|
|
219
|
-
return this.#http.request("POST", `/invoices/${
|
|
219
|
+
return this.#http.request("POST", `/invoices/${pathSegment(id)}/pay`, body, options);
|
|
220
220
|
}
|
|
221
221
|
/**
|
|
222
222
|
* The three transitions that take no parameters, which are one shape:
|
|
@@ -228,7 +228,7 @@ export class InvoicesResource {
|
|
|
228
228
|
* that is easy to get wrong in only one of them.
|
|
229
229
|
*/
|
|
230
230
|
async #transition(id, name, options) {
|
|
231
|
-
return this.#http.request("POST", `/invoices/${
|
|
231
|
+
return this.#http.request("POST", `/invoices/${pathSegment(id)}/${name}`, {}, options);
|
|
232
232
|
}
|
|
233
233
|
}
|
|
234
234
|
/**
|
|
@@ -271,7 +271,7 @@ export class InvoiceItemsResource {
|
|
|
271
271
|
}
|
|
272
272
|
/** `GET /v1/invoice_items/{id}` — readable whatever the parent's status. */
|
|
273
273
|
async retrieve(id) {
|
|
274
|
-
return this.#http.request("GET", `/invoice_items/${
|
|
274
|
+
return this.#http.request("GET", `/invoice_items/${pathSegment(id)}`);
|
|
275
275
|
}
|
|
276
276
|
/**
|
|
277
277
|
* `POST /v1/invoice_items/{id}` — **draft parent only**, with `amount`
|
|
@@ -294,13 +294,13 @@ export class InvoiceItemsResource {
|
|
|
294
294
|
if (params.unit_amount !== undefined) {
|
|
295
295
|
body["unit_amount"] = params.unit_amount;
|
|
296
296
|
}
|
|
297
|
-
return this.#http.request("POST", `/invoice_items/${
|
|
297
|
+
return this.#http.request("POST", `/invoice_items/${pathSegment(id)}`, body, options);
|
|
298
298
|
}
|
|
299
299
|
/**
|
|
300
300
|
* `DELETE /v1/invoice_items/{id}` — **draft parent only**, and the invoice
|
|
301
301
|
* re-totals.
|
|
302
302
|
*/
|
|
303
303
|
async del(id, options) {
|
|
304
|
-
return this.#http.request("DELETE", `/invoice_items/${
|
|
304
|
+
return this.#http.request("DELETE", `/invoice_items/${pathSegment(id)}`, undefined, options);
|
|
305
305
|
}
|
|
306
306
|
}
|
|
@@ -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/validate.d.ts
CHANGED
|
@@ -14,3 +14,22 @@
|
|
|
14
14
|
* **safe** integer or nothing goes to the wire.
|
|
15
15
|
*/
|
|
16
16
|
export declare function assertIntegerAmount(amount: number, field?: string): void;
|
|
17
|
+
/**
|
|
18
|
+
* Percent-encodes one merchant-supplied id for a URL path, and refuses the
|
|
19
|
+
* ids escaping cannot keep in place.
|
|
20
|
+
*
|
|
21
|
+
* `encodeURIComponent` keeps a `/`, `?` or `#` inside its segment, but it
|
|
22
|
+
* leaves `.` alone, so `""`, `.` and `..` pass through, and as whole segments
|
|
23
|
+
* `fetch`'s URL parser resolves them before the request leaves:
|
|
24
|
+
* `customers.del("..")` was a `DELETE /v1/`. Escaping the dots would not
|
|
25
|
+
* help, because WHATWG parsing reads `%2E%2E` as `..` too. So an empty or
|
|
26
|
+
* all-dots id throws before anything is sent, the token request included —
|
|
27
|
+
* `sdks/rust`'s `path_segment` refuses the same ids with
|
|
28
|
+
* `Error::InvalidParams`. `...` is refused by decision rather than by the
|
|
29
|
+
* standard: `docs/reference/rails.md` § "A path segment the caller chose"
|
|
30
|
+
* says why.
|
|
31
|
+
*
|
|
32
|
+
* A `TypeError`, like {@link assertIntegerAmount}'s refusal, and not a
|
|
33
|
+
* `VpayError` — the dated gap `docs/sdks/parity.md` records for both.
|
|
34
|
+
*/
|
|
35
|
+
export declare function pathSegment(id: string): string;
|
package/dist/validate.js
CHANGED
|
@@ -18,3 +18,35 @@ export function assertIntegerAmount(amount, field = "amount") {
|
|
|
18
18
|
throw new TypeError(`${field} must be a non-negative safe integer in minor units, got ${amount}`);
|
|
19
19
|
}
|
|
20
20
|
}
|
|
21
|
+
/**
|
|
22
|
+
* The one message {@link pathSegment}'s refusal carries, whatever the id.
|
|
23
|
+
*
|
|
24
|
+
* Fixed so that it cannot quote the id, and byte-identical to `sdks/rust`'s
|
|
25
|
+
* `DOT_SEGMENT_REFUSAL` behind the same `invalid request parameter id: `
|
|
26
|
+
* prefix that crate's `Error::InvalidParams` displays.
|
|
27
|
+
*/
|
|
28
|
+
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";
|
|
29
|
+
/**
|
|
30
|
+
* Percent-encodes one merchant-supplied id for a URL path, and refuses the
|
|
31
|
+
* ids escaping cannot keep in place.
|
|
32
|
+
*
|
|
33
|
+
* `encodeURIComponent` keeps a `/`, `?` or `#` inside its segment, but it
|
|
34
|
+
* leaves `.` alone, so `""`, `.` and `..` pass through, and as whole segments
|
|
35
|
+
* `fetch`'s URL parser resolves them before the request leaves:
|
|
36
|
+
* `customers.del("..")` was a `DELETE /v1/`. Escaping the dots would not
|
|
37
|
+
* help, because WHATWG parsing reads `%2E%2E` as `..` too. So an empty or
|
|
38
|
+
* all-dots id throws before anything is sent, the token request included —
|
|
39
|
+
* `sdks/rust`'s `path_segment` refuses the same ids with
|
|
40
|
+
* `Error::InvalidParams`. `...` is refused by decision rather than by the
|
|
41
|
+
* standard: `docs/reference/rails.md` § "A path segment the caller chose"
|
|
42
|
+
* says why.
|
|
43
|
+
*
|
|
44
|
+
* A `TypeError`, like {@link assertIntegerAmount}'s refusal, and not a
|
|
45
|
+
* `VpayError` — the dated gap `docs/sdks/parity.md` records for both.
|
|
46
|
+
*/
|
|
47
|
+
export function pathSegment(id) {
|
|
48
|
+
if (/^\.*$/u.test(id)) {
|
|
49
|
+
throw new TypeError(`invalid request parameter id: ${DOT_SEGMENT_REFUSAL}`);
|
|
50
|
+
}
|
|
51
|
+
return encodeURIComponent(id);
|
|
52
|
+
}
|
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.9.
|
|
12
|
+
export const SDK_VERSION = "0.9.1"; // x-release-please-version
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@vaam-apps/vpay-sdk",
|
|
3
|
-
"version": "0.9.
|
|
3
|
+
"version": "0.9.1",
|
|
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": {
|