@vaam-apps/vpay-sdk 0.2.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.
Files changed (42) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +732 -0
  3. package/dist/auth.d.ts +180 -0
  4. package/dist/auth.js +297 -0
  5. package/dist/client.d.ts +75 -0
  6. package/dist/client.js +79 -0
  7. package/dist/errors.d.ts +91 -0
  8. package/dist/errors.js +112 -0
  9. package/dist/form.d.ts +47 -0
  10. package/dist/form.js +127 -0
  11. package/dist/http.d.ts +33 -0
  12. package/dist/http.js +115 -0
  13. package/dist/index.d.ts +15 -0
  14. package/dist/index.js +14 -0
  15. package/dist/resources/account-holders.d.ts +36 -0
  16. package/dist/resources/account-holders.js +46 -0
  17. package/dist/resources/balance.d.ts +8 -0
  18. package/dist/resources/balance.js +10 -0
  19. package/dist/resources/checkout-sessions.d.ts +55 -0
  20. package/dist/resources/checkout-sessions.js +127 -0
  21. package/dist/resources/customers.d.ts +97 -0
  22. package/dist/resources/customers.js +157 -0
  23. package/dist/resources/events.d.ts +8 -0
  24. package/dist/resources/events.js +10 -0
  25. package/dist/resources/invoices.d.ts +193 -0
  26. package/dist/resources/invoices.js +275 -0
  27. package/dist/resources/payment-intents.d.ts +12 -0
  28. package/dist/resources/payment-intents.js +44 -0
  29. package/dist/resources/refunds.d.ts +92 -0
  30. package/dist/resources/refunds.js +106 -0
  31. package/dist/stripe-auth.d.ts +177 -0
  32. package/dist/stripe-auth.js +192 -0
  33. package/dist/types.d.ts +1268 -0
  34. package/dist/types.js +48 -0
  35. package/dist/validate.d.ts +16 -0
  36. package/dist/validate.js +20 -0
  37. package/dist/version.d.ts +12 -0
  38. package/dist/version.js +12 -0
  39. package/dist/webhooks.d.ts +22 -0
  40. package/dist/webhooks.js +94 -0
  41. package/package.json +63 -0
  42. package/scripts/mint-assertion.mjs +73 -0
package/dist/auth.d.ts ADDED
@@ -0,0 +1,180 @@
1
+ /**
2
+ * `private_key_jwt` client assertion minting and access-token management,
3
+ * per docs/flows/merchant-auth.md.
4
+ */
5
+ import { type KeyObject } from "node:crypto";
6
+ /** RFC 7523's fixed URN for this grant's `client_assertion_type`. */
7
+ export declare const CLIENT_ASSERTION_TYPE_JWT_BEARER = "urn:ietf:params:oauth:client-assertion-type:jwt-bearer";
8
+ /** `authkestra-op`'s ceiling on how far in the future `exp` may sit (`MAX_CLIENT_ASSERTION_LIFETIME_SECS`). */
9
+ export declare const MAX_ASSERTION_LIFETIME_SECONDS = 300;
10
+ /**
11
+ * Default client-assertion lifetime, in seconds. Well inside the OP's
12
+ * `1..=300` bound — see the README's note on why raising it is a trap.
13
+ */
14
+ export declare const DEFAULT_ASSERTION_LIFETIME_SECONDS = 60;
15
+ /**
16
+ * Default OAuth2 `audience` request parameter. Server-side the same string is
17
+ * `vpay_config::MERCHANT_AUDIENCE`; this package keeps its own copy, so the
18
+ * two must change together (docs/flows/merchant-auth.md, "The token request").
19
+ */
20
+ export declare const DEFAULT_AUDIENCE = "vpay:v1";
21
+ /** Default timeout for the token exchange and every resource call, in milliseconds. */
22
+ export declare const DEFAULT_TIMEOUT_MS = 30000;
23
+ /** Validates a configured assertion lifetime against the OP's `1..=300` bound. */
24
+ export declare function validateAssertionLifetimeSeconds(lifetimeSeconds: number): void;
25
+ export interface MintClientAssertionOptions {
26
+ clientId: string;
27
+ privateKey: string | KeyObject;
28
+ /**
29
+ * The assertion's `aud` claim: the OP's own token endpoint URL, or its
30
+ * issuer identifier — both are accepted by `authenticate_client`
31
+ * (docs/flows/merchant-auth.md). Not necessarily the URL the caller POSTs
32
+ * to; see `MerchantAuthOptions.assertionAudience`.
33
+ */
34
+ audience: string;
35
+ kid?: string | undefined;
36
+ /** Default 60. Must be an integer in `1..=300`. */
37
+ lifetimeSeconds?: number | undefined;
38
+ /** Unix seconds. Defaults to `Date.now() / 1000`, injectable for tests. */
39
+ now?: number | undefined;
40
+ }
41
+ /**
42
+ * Mints an RS256 `private_key_jwt` client assertion
43
+ * (docs/flows/merchant-auth.md, "The client assertion" table).
44
+ *
45
+ * Signed with `crypto.sign('sha256', ..., rsaKey)`, which for an RSA key
46
+ * without an explicit padding option produces PKCS#1 v1.5 — exactly RS256.
47
+ */
48
+ export declare function mintClientAssertion(options: MintClientAssertionOptions): string;
49
+ export interface TokenManagerOptions {
50
+ clientId: string;
51
+ /**
52
+ * The signing key as a parsed {@link KeyObject}, never PEM text.
53
+ *
54
+ * {@link resolveMerchantAuth} parses it **once**, at construction: a PEM
55
+ * that `createPrivateKey` cannot read is a `VpayConfigError` the merchant
56
+ * sees at startup rather than a `crypto` exception thrown out of the first
57
+ * token exchange, and every later assertion signs against the same already
58
+ * parsed key instead of re-parsing the same text per mint.
59
+ */
60
+ privateKey: KeyObject;
61
+ kid: string | undefined;
62
+ /** Where the token request is POSTed — reachable from *this* process. */
63
+ tokenEndpoint: string;
64
+ /**
65
+ * The client assertion's `aud` claim — what the OP calls itself. Resolved
66
+ * by {@link resolveMerchantAuth}; defaults to {@link tokenEndpoint}, which
67
+ * is right only when this process reaches vpay at the same URL vpay
68
+ * publishes as its own.
69
+ */
70
+ assertionAudience: string;
71
+ /** The OAuth2 `audience` **request parameter** (`vpay:v1`). Not the `aud` claim. */
72
+ audience: string;
73
+ scope: string | undefined;
74
+ assertionLifetimeSeconds: number;
75
+ timeoutMs: number;
76
+ fetchImpl: typeof fetch;
77
+ userAgent: string;
78
+ }
79
+ /**
80
+ * Caches the merchant's `/v1` access token and mints a fresh one only when
81
+ * needed, per docs/flows/merchant-auth.md's "Using the token" section:
82
+ *
83
+ * - Cached until `expires_in − margin` has elapsed (margin: 30s, or half of
84
+ * `expires_in` for short TTLs — integer arithmetic only).
85
+ * - Concurrent callers share one in-flight token request (a single `jti`
86
+ * spent per refresh, not one per caller).
87
+ * - {@link TokenManager.invalidate} lets the caller force a re-auth on a 401.
88
+ */
89
+ export declare class TokenManager {
90
+ #private;
91
+ constructor(options: TokenManagerOptions);
92
+ getToken(): Promise<string>;
93
+ /** Discards the cached token, forcing the next {@link getToken} call to re-authenticate. */
94
+ invalidate(): void;
95
+ }
96
+ /**
97
+ * The subset of an entry point's constructor options that decides how this
98
+ * package authenticates: everything the {@link TokenManager} needs, plus the
99
+ * `baseUrl` the defaults are derived from.
100
+ *
101
+ * Shared by {@link VpayClient} and `createStripeAuthenticator` on purpose.
102
+ * The two entry points mint the *same* assertion against the *same* endpoint
103
+ * with the *same* defaults; resolving those defaults in one place is what
104
+ * stops them from drifting apart as one of them is edited.
105
+ *
106
+ * Every optional property is `?: T | undefined` for the
107
+ * `exactOptionalPropertyTypes` reason spelled out on `VpayClientOptions`.
108
+ */
109
+ export interface MerchantAuthOptions {
110
+ /** vpay's base URL, e.g. `https://api.vpay.example`. */
111
+ baseUrl: string;
112
+ /** This merchant's registered OAuth2 `client_id`. */
113
+ clientId: string;
114
+ /** The merchant's RSA private key — PEM text or a `crypto.KeyObject`. */
115
+ privateKey: string | KeyObject;
116
+ /** Required only if the merchant registered more than one JWK. */
117
+ kid?: string | undefined;
118
+ /** Default `${baseUrl}/v1/oauth`. */
119
+ issuer?: string | undefined;
120
+ /**
121
+ * The URL this package POSTs the token request to. Default
122
+ * `${issuer}/token`.
123
+ *
124
+ * This is a *reachability* setting: it must resolve from wherever your
125
+ * server runs — a compose service name, a private DNS name, a mesh
126
+ * address. It is **not** necessarily what the assertion is addressed to;
127
+ * see {@link assertionAudience}.
128
+ */
129
+ tokenEndpoint?: string | undefined;
130
+ /**
131
+ * The OP's own token endpoint (or issuer) as vpay is configured publicly —
132
+ * `{deployment.public_base_url}/v1/oauth/token`, or
133
+ * `{deployment.public_base_url}/v1/oauth`. This is what the client
134
+ * assertion's `aud` claim is signed as. **Set it when your server reaches
135
+ * vpay by a different URL than payers do.**
136
+ *
137
+ * Default: {@link tokenEndpoint}, which is correct only when the two are
138
+ * the same string. When they are not — a merchant server calling
139
+ * `http://vpay-server:8080` inside a compose network, or a private name in
140
+ * production — the OP's `authenticate_client` compares the `aud` claim
141
+ * against its own `{token endpoint, issuer}` pair, neither of which is the
142
+ * internal URL, and every token request answers `invalid_client` /
143
+ * `InvalidAudience`. Nothing else in the handshake is wrong, which is why
144
+ * the symptom carries no hint at the cause.
145
+ */
146
+ assertionAudience?: string | undefined;
147
+ /**
148
+ * The OAuth2 `audience` **request parameter** — a form field naming the
149
+ * resource server (`vpay:v1`), not the assertion's `aud` claim. Default
150
+ * {@link DEFAULT_AUDIENCE}.
151
+ */
152
+ audience?: string | undefined;
153
+ /** Omitted from the token request unless set. */
154
+ scope?: string | undefined;
155
+ /** Default {@link DEFAULT_ASSERTION_LIFETIME_SECONDS}. Must be an integer in `1..=300`. */
156
+ assertionLifetimeSeconds?: number | undefined;
157
+ /** Default {@link DEFAULT_TIMEOUT_MS}. */
158
+ timeoutMs?: number | undefined;
159
+ /** Injection point for tests and proxies. Defaults to the global `fetch`. */
160
+ fetch?: typeof fetch | undefined;
161
+ }
162
+ /** What {@link resolveMerchantAuth} hands back: a ready {@link TokenManager} and the settings it was built from. */
163
+ export interface ResolvedMerchantAuth {
164
+ /** `baseUrl` with any trailing slash removed. */
165
+ baseUrl: string;
166
+ clientId: string;
167
+ timeoutMs: number;
168
+ fetchImpl: typeof fetch;
169
+ userAgent: string;
170
+ tokenManager: TokenManager;
171
+ }
172
+ /**
173
+ * Validates {@link MerchantAuthOptions}, applies this package's defaults, and
174
+ * builds the {@link TokenManager} both entry points share.
175
+ *
176
+ * Every failure it can detect is detected here — at construction — rather
177
+ * than on the first request: a merchant with an out-of-range
178
+ * `assertionLifetimeSeconds` finds out at startup, not at 3am under load.
179
+ */
180
+ export declare function resolveMerchantAuth(options: MerchantAuthOptions): ResolvedMerchantAuth;
package/dist/auth.js ADDED
@@ -0,0 +1,297 @@
1
+ /**
2
+ * `private_key_jwt` client assertion minting and access-token management,
3
+ * per docs/flows/merchant-auth.md.
4
+ */
5
+ import { randomUUID, createPrivateKey, sign, } from "node:crypto";
6
+ import { VpayAuthError, VpayConfigError, VpayTransportError, VpayUnexpectedResponseError, boundedBodyPrefix, } from "./errors.js";
7
+ import { encodeForm } from "./form.js";
8
+ import { SDK_VERSION } from "./version.js";
9
+ /** RFC 7523's fixed URN for this grant's `client_assertion_type`. */
10
+ export const CLIENT_ASSERTION_TYPE_JWT_BEARER = "urn:ietf:params:oauth:client-assertion-type:jwt-bearer";
11
+ /** `authkestra-op`'s ceiling on how far in the future `exp` may sit (`MAX_CLIENT_ASSERTION_LIFETIME_SECS`). */
12
+ export const MAX_ASSERTION_LIFETIME_SECONDS = 300;
13
+ const MIN_ASSERTION_LIFETIME_SECONDS = 1;
14
+ /**
15
+ * Default client-assertion lifetime, in seconds. Well inside the OP's
16
+ * `1..=300` bound — see the README's note on why raising it is a trap.
17
+ */
18
+ export const DEFAULT_ASSERTION_LIFETIME_SECONDS = 60;
19
+ /**
20
+ * Default OAuth2 `audience` request parameter. Server-side the same string is
21
+ * `vpay_config::MERCHANT_AUDIENCE`; this package keeps its own copy, so the
22
+ * two must change together (docs/flows/merchant-auth.md, "The token request").
23
+ */
24
+ export const DEFAULT_AUDIENCE = "vpay:v1";
25
+ /** Default timeout for the token exchange and every resource call, in milliseconds. */
26
+ export const DEFAULT_TIMEOUT_MS = 30_000;
27
+ /** Validates a configured assertion lifetime against the OP's `1..=300` bound. */
28
+ export function validateAssertionLifetimeSeconds(lifetimeSeconds) {
29
+ if (!Number.isInteger(lifetimeSeconds) ||
30
+ lifetimeSeconds < MIN_ASSERTION_LIFETIME_SECONDS ||
31
+ lifetimeSeconds > MAX_ASSERTION_LIFETIME_SECONDS) {
32
+ throw new VpayConfigError(`assertionLifetimeSeconds must be an integer between ${MIN_ASSERTION_LIFETIME_SECONDS} and ` +
33
+ `${MAX_ASSERTION_LIFETIME_SECONDS}, got ${lifetimeSeconds}`);
34
+ }
35
+ }
36
+ function base64url(input) {
37
+ return input.toString("base64url");
38
+ }
39
+ /**
40
+ * Mints an RS256 `private_key_jwt` client assertion
41
+ * (docs/flows/merchant-auth.md, "The client assertion" table).
42
+ *
43
+ * Signed with `crypto.sign('sha256', ..., rsaKey)`, which for an RSA key
44
+ * without an explicit padding option produces PKCS#1 v1.5 — exactly RS256.
45
+ */
46
+ export function mintClientAssertion(options) {
47
+ const lifetimeSeconds = options.lifetimeSeconds ?? DEFAULT_ASSERTION_LIFETIME_SECONDS;
48
+ validateAssertionLifetimeSeconds(lifetimeSeconds);
49
+ const now = options.now ?? Math.floor(Date.now() / 1000);
50
+ const header = {
51
+ alg: "RS256",
52
+ typ: "JWT",
53
+ };
54
+ if (options.kid !== undefined) {
55
+ header.kid = options.kid;
56
+ }
57
+ const payload = {
58
+ iss: options.clientId,
59
+ sub: options.clientId,
60
+ aud: options.audience,
61
+ jti: randomUUID(),
62
+ exp: now + lifetimeSeconds,
63
+ iat: now,
64
+ };
65
+ const encodedHeader = base64url(Buffer.from(JSON.stringify(header), "utf8"));
66
+ const encodedPayload = base64url(Buffer.from(JSON.stringify(payload), "utf8"));
67
+ const signingInput = `${encodedHeader}.${encodedPayload}`;
68
+ const keyObject = typeof options.privateKey === "string"
69
+ ? createPrivateKey(options.privateKey)
70
+ : options.privateKey;
71
+ const signature = sign("sha256", Buffer.from(signingInput, "utf8"), keyObject);
72
+ return `${signingInput}.${base64url(signature)}`;
73
+ }
74
+ /**
75
+ * Recognises `authkestra_op::handlers::token::TokenResponse`.
76
+ *
77
+ * `token_type` must be `Bearer`, compared case-insensitively per RFC 6749
78
+ * §7.1 (the value is a case-insensitive token, and OPs differ on `Bearer` vs
79
+ * `bearer`). Anything else — a `MAC` or `DPoP` token, say — is a token this
80
+ * SDK does not know how to present: it only ever sends
81
+ * `Authorization: Bearer …`. Such a response is reported as a
82
+ * {@link VpayUnexpectedResponseError}, the same as any other 200 whose body
83
+ * is not the documented shape; it is not an authentication *failure*, so it
84
+ * is deliberately not a {@link VpayAuthError}.
85
+ */
86
+ function isTokenResponseBody(value) {
87
+ if (typeof value !== "object" || value === null) {
88
+ return false;
89
+ }
90
+ const record = value;
91
+ const tokenType = record["token_type"];
92
+ return (typeof record["access_token"] === "string" &&
93
+ typeof tokenType === "string" &&
94
+ tokenType.toLowerCase() === "bearer" &&
95
+ typeof record["expires_in"] === "number");
96
+ }
97
+ function isTokenErrorBody(value) {
98
+ return (typeof value === "object" &&
99
+ value !== null &&
100
+ typeof value["error"] === "string");
101
+ }
102
+ /**
103
+ * Caches the merchant's `/v1` access token and mints a fresh one only when
104
+ * needed, per docs/flows/merchant-auth.md's "Using the token" section:
105
+ *
106
+ * - Cached until `expires_in − margin` has elapsed (margin: 30s, or half of
107
+ * `expires_in` for short TTLs — integer arithmetic only).
108
+ * - Concurrent callers share one in-flight token request (a single `jti`
109
+ * spent per refresh, not one per caller).
110
+ * - {@link TokenManager.invalidate} lets the caller force a re-auth on a 401.
111
+ */
112
+ export class TokenManager {
113
+ #options;
114
+ #cached;
115
+ #inflight;
116
+ constructor(options) {
117
+ this.#options = options;
118
+ }
119
+ async getToken() {
120
+ const now = Date.now();
121
+ if (this.#cached && this.#cached.expiresAtMs > now) {
122
+ return this.#cached.accessToken;
123
+ }
124
+ if (!this.#inflight) {
125
+ this.#inflight = this.#fetchToken().finally(() => {
126
+ this.#inflight = undefined;
127
+ });
128
+ }
129
+ return this.#inflight;
130
+ }
131
+ /** Discards the cached token, forcing the next {@link getToken} call to re-authenticate. */
132
+ invalidate() {
133
+ this.#cached = undefined;
134
+ }
135
+ async #fetchToken() {
136
+ const assertion = mintClientAssertion({
137
+ clientId: this.#options.clientId,
138
+ privateKey: this.#options.privateKey,
139
+ // `aud` is the **OP's own token endpoint (or issuer)**, never the
140
+ // `audience` request parameter below (`vpay:v1`). RFC 7523 §3 and the
141
+ // OP's own `authenticate_client` both compare it against the endpoint
142
+ // as the OP names itself — `{deployment.public_base_url}/v1/oauth/token`
143
+ // or that issuer — which is not necessarily the URL this process
144
+ // POSTed to. The two are different values with different jobs
145
+ // (docs/flows/merchant-auth.md, "The client assertion").
146
+ audience: this.#options.assertionAudience,
147
+ kid: this.#options.kid,
148
+ lifetimeSeconds: this.#options.assertionLifetimeSeconds,
149
+ });
150
+ const formBody = {
151
+ grant_type: "client_credentials",
152
+ client_id: this.#options.clientId,
153
+ client_assertion_type: CLIENT_ASSERTION_TYPE_JWT_BEARER,
154
+ client_assertion: assertion,
155
+ audience: this.#options.audience,
156
+ };
157
+ if (this.#options.scope !== undefined) {
158
+ formBody["scope"] = this.#options.scope;
159
+ }
160
+ let status;
161
+ let ok;
162
+ let text;
163
+ try {
164
+ const response = await this.#options.fetchImpl(this.#options.tokenEndpoint, {
165
+ method: "POST",
166
+ headers: {
167
+ "Content-Type": "application/x-www-form-urlencoded",
168
+ Accept: "application/json",
169
+ "User-Agent": this.#options.userAgent,
170
+ },
171
+ body: encodeForm(formBody),
172
+ signal: AbortSignal.timeout(this.#options.timeoutMs),
173
+ });
174
+ status = response.status;
175
+ ok = response.ok;
176
+ // Read the body inside the same `try`: `fetch` resolves on headers, so
177
+ // a body that stalls (or the timeout firing mid-stream) rejects here.
178
+ // Outside this block it escapes as a raw `DOMException: TimeoutError`
179
+ // rather than a `VpayTransportError` carrying it as `cause`.
180
+ text = await response.text();
181
+ }
182
+ catch (err) {
183
+ throw new VpayTransportError("token request failed", { cause: err });
184
+ }
185
+ let parsed;
186
+ try {
187
+ parsed = text.length > 0 ? JSON.parse(text) : undefined;
188
+ }
189
+ catch {
190
+ parsed = undefined;
191
+ }
192
+ if (!ok) {
193
+ if (isTokenErrorBody(parsed)) {
194
+ throw new VpayAuthError(parsed.error, parsed.error_description);
195
+ }
196
+ throw new VpayUnexpectedResponseError(status, boundedBodyPrefix(text));
197
+ }
198
+ if (!isTokenResponseBody(parsed)) {
199
+ throw new VpayUnexpectedResponseError(status, boundedBodyPrefix(text));
200
+ }
201
+ const margin = Math.min(30, Math.floor(parsed.expires_in / 2));
202
+ const ttlSeconds = Math.max(parsed.expires_in - margin, 0);
203
+ this.#cached = {
204
+ accessToken: parsed.access_token,
205
+ expiresAtMs: Date.now() + ttlSeconds * 1000,
206
+ };
207
+ return parsed.access_token;
208
+ }
209
+ }
210
+ function requireNonEmptyString(value, field) {
211
+ if (typeof value !== "string" || value.length === 0) {
212
+ throw new VpayConfigError(`${field} is required`);
213
+ }
214
+ return value;
215
+ }
216
+ function stripTrailingSlash(url) {
217
+ return url.endsWith("/") ? url.slice(0, -1) : url;
218
+ }
219
+ /**
220
+ * Turns a configured signing key into a {@link KeyObject}, once.
221
+ *
222
+ * Two things this buys, and both are the reason it is here rather than in
223
+ * {@link mintClientAssertion}'s per-call path:
224
+ *
225
+ * - **A bad key is a startup failure.** `createPrivateKey('')`, or a PEM with
226
+ * a mangled header, throws an OpenSSL `Error` out of `crypto`. Left to the
227
+ * first mint, that surfaced as an unrecognisable exception from the middle
228
+ * of a token exchange — and, through `@vaam-apps/vpay-sdk/stripe`, as stripe-node's
229
+ * detached `unhandledRejection` (see `stripe-auth.ts`). Parsed here it is a
230
+ * {@link VpayConfigError} raised by the constructor the merchant wrote.
231
+ * - **One parse, not one per request.** RSA PEM decoding is not free, and
232
+ * nothing about it changes between mints.
233
+ *
234
+ * The message never quotes the key: the input is the merchant's private key,
235
+ * and a `VpayConfigError` is exactly the sort of thing that reaches a log.
236
+ * `cause` carries OpenSSL's own reason, which names the decoder that failed
237
+ * and no key material.
238
+ */
239
+ function parsePrivateKey(privateKey) {
240
+ if (typeof privateKey !== "string") {
241
+ return privateKey;
242
+ }
243
+ try {
244
+ return createPrivateKey(privateKey);
245
+ }
246
+ catch (err) {
247
+ throw new VpayConfigError("privateKey could not be read as a private key: expected PEM text (or a crypto.KeyObject)", { cause: err });
248
+ }
249
+ }
250
+ /**
251
+ * Validates {@link MerchantAuthOptions}, applies this package's defaults, and
252
+ * builds the {@link TokenManager} both entry points share.
253
+ *
254
+ * Every failure it can detect is detected here — at construction — rather
255
+ * than on the first request: a merchant with an out-of-range
256
+ * `assertionLifetimeSeconds` finds out at startup, not at 3am under load.
257
+ */
258
+ export function resolveMerchantAuth(options) {
259
+ const baseUrl = stripTrailingSlash(requireNonEmptyString(options.baseUrl, "baseUrl"));
260
+ const clientId = requireNonEmptyString(options.clientId, "clientId");
261
+ if (options.privateKey === undefined || options.privateKey === null) {
262
+ throw new VpayConfigError("privateKey is required");
263
+ }
264
+ const privateKey = parsePrivateKey(options.privateKey);
265
+ const assertionLifetimeSeconds = options.assertionLifetimeSeconds ?? DEFAULT_ASSERTION_LIFETIME_SECONDS;
266
+ validateAssertionLifetimeSeconds(assertionLifetimeSeconds);
267
+ const issuer = options.issuer ?? `${baseUrl}/v1/oauth`;
268
+ const tokenEndpoint = options.tokenEndpoint ?? `${issuer}/token`;
269
+ // Defaults to the URL we POST to — unchanged behaviour for every merchant
270
+ // whose server reaches vpay at the same URL vpay publishes as its own.
271
+ // A merchant reaching vpay by an internal name sets it explicitly.
272
+ const assertionAudience = options.assertionAudience ?? tokenEndpoint;
273
+ const audience = options.audience ?? DEFAULT_AUDIENCE;
274
+ const timeoutMs = options.timeoutMs ?? DEFAULT_TIMEOUT_MS;
275
+ const fetchImpl = options.fetch ?? fetch;
276
+ const userAgent = `vpay-sdk-node/${SDK_VERSION}`;
277
+ return {
278
+ baseUrl,
279
+ clientId,
280
+ timeoutMs,
281
+ fetchImpl,
282
+ userAgent,
283
+ tokenManager: new TokenManager({
284
+ clientId,
285
+ privateKey,
286
+ kid: options.kid,
287
+ tokenEndpoint,
288
+ assertionAudience,
289
+ audience,
290
+ scope: options.scope,
291
+ assertionLifetimeSeconds,
292
+ timeoutMs,
293
+ fetchImpl,
294
+ userAgent,
295
+ }),
296
+ };
297
+ }
@@ -0,0 +1,75 @@
1
+ import { type MerchantAuthOptions } from "./auth.js";
2
+ import { AccountHoldersResource } from "./resources/account-holders.js";
3
+ import { BalanceResource } from "./resources/balance.js";
4
+ import { CheckoutResource } from "./resources/checkout-sessions.js";
5
+ import { CustomersResource } from "./resources/customers.js";
6
+ import { EventsResource } from "./resources/events.js";
7
+ import { InvoiceItemsResource, InvoicesResource } from "./resources/invoices.js";
8
+ import { PaymentIntentsResource } from "./resources/payment-intents.js";
9
+ import { RefundsResource } from "./resources/refunds.js";
10
+ /**
11
+ * Constructor options for {@link VpayClient} — see the package README's
12
+ * configuration table.
13
+ *
14
+ * **This is {@link MerchantAuthOptions}**, which is also what
15
+ * `createStripeAuthenticator` takes. The two entry points authenticate the
16
+ * same way against the same endpoint with the same defaults
17
+ * ({@link resolveMerchantAuth} is the single implementation), so they must
18
+ * offer the same knobs — and an interface that merely *listed* the same
19
+ * eleven properties would drift the first time one of them gained a twelfth.
20
+ * Extending is what makes "the same options" checked rather than intended.
21
+ *
22
+ * Every optional property is declared `?: T | undefined`, not `?: T`. Under
23
+ * `exactOptionalPropertyTypes` (which this repo sets, and which consumers are
24
+ * likely to) the shorter form rejects `{ kid: maybeKid }` where `maybeKid` is
25
+ * `string | undefined` — forcing every consumer into conditional spreads for
26
+ * options that are, semantically, simply optional. `src/types.test.ts` pins
27
+ * this at the type level.
28
+ */
29
+ export interface VpayClientOptions extends MerchantAuthOptions {
30
+ /**
31
+ * Default 30000. Applies to **both** the token exchange and every resource
32
+ * call — the one place this client's reading of an inherited option is
33
+ * narrower than the base's, because `VpayClient` owns its resource
34
+ * transport and `createStripeAuthenticator` does not.
35
+ */
36
+ timeoutMs?: number | undefined;
37
+ }
38
+ declare const INSPECT_CUSTOM: unique symbol;
39
+ /**
40
+ * The vpay merchant SDK client. Mints `private_key_jwt` assertions, exchanges
41
+ * and caches `/v1` access tokens, and exposes the resource namespaces below.
42
+ *
43
+ * The private key never appears in `util.inspect(client)` or
44
+ * `JSON.stringify(client)` output — this class never holds it. It is parsed
45
+ * once by `resolveMerchantAuth` and kept by the {@link TokenManager} as a
46
+ * `crypto.KeyObject`, whose own inspection reveals no key material; the two
47
+ * redacted representations below are built by hand from the two fields that
48
+ * are safe to print.
49
+ */
50
+ export declare class VpayClient {
51
+ #private;
52
+ readonly paymentIntents: PaymentIntentsResource;
53
+ readonly checkout: CheckoutResource;
54
+ /** `/v1/customers` — the merchant-owned record of a payer (S4a). */
55
+ readonly customers: CustomersResource;
56
+ /** `/v1/invoices` — a merchant's bill to one customer (S4b). */
57
+ readonly invoices: InvoicesResource;
58
+ /** `/v1/invoice_items` — the lines of a draft invoice (S4b). */
59
+ readonly invoiceItems: InvoiceItemsResource;
60
+ readonly refunds: RefundsResource;
61
+ readonly events: EventsResource;
62
+ readonly balance: BalanceResource;
63
+ /** `/v1/account_holders` — issue #47. */
64
+ readonly accountHolders: AccountHoldersResource;
65
+ constructor(options: VpayClientOptions);
66
+ /** A safe, redacted representation — never the private key. */
67
+ [INSPECT_CUSTOM](): string;
68
+ /** A safe, redacted representation — never the private key. */
69
+ toJSON(): {
70
+ object: "vpay_client";
71
+ clientId: string;
72
+ baseUrl: string;
73
+ };
74
+ }
75
+ export {};
package/dist/client.js ADDED
@@ -0,0 +1,79 @@
1
+ import { inspect } from "node:util";
2
+ import { resolveMerchantAuth, } from "./auth.js";
3
+ import { HttpClient } from "./http.js";
4
+ import { AccountHoldersResource } from "./resources/account-holders.js";
5
+ import { BalanceResource } from "./resources/balance.js";
6
+ import { CheckoutResource } from "./resources/checkout-sessions.js";
7
+ import { CustomersResource } from "./resources/customers.js";
8
+ import { EventsResource } from "./resources/events.js";
9
+ import { InvoiceItemsResource, InvoicesResource, } from "./resources/invoices.js";
10
+ import { PaymentIntentsResource } from "./resources/payment-intents.js";
11
+ import { RefundsResource } from "./resources/refunds.js";
12
+ const INSPECT_CUSTOM = Symbol.for("nodejs.util.inspect.custom");
13
+ /**
14
+ * The vpay merchant SDK client. Mints `private_key_jwt` assertions, exchanges
15
+ * and caches `/v1` access tokens, and exposes the resource namespaces below.
16
+ *
17
+ * The private key never appears in `util.inspect(client)` or
18
+ * `JSON.stringify(client)` output — this class never holds it. It is parsed
19
+ * once by `resolveMerchantAuth` and kept by the {@link TokenManager} as a
20
+ * `crypto.KeyObject`, whose own inspection reveals no key material; the two
21
+ * redacted representations below are built by hand from the two fields that
22
+ * are safe to print.
23
+ */
24
+ export class VpayClient {
25
+ #clientId;
26
+ #baseUrl;
27
+ #tokenManager;
28
+ paymentIntents;
29
+ checkout;
30
+ /** `/v1/customers` — the merchant-owned record of a payer (S4a). */
31
+ customers;
32
+ /** `/v1/invoices` — a merchant's bill to one customer (S4b). */
33
+ invoices;
34
+ /** `/v1/invoice_items` — the lines of a draft invoice (S4b). */
35
+ invoiceItems;
36
+ refunds;
37
+ events;
38
+ balance;
39
+ /** `/v1/account_holders` — issue #47. */
40
+ accountHolders;
41
+ constructor(options) {
42
+ // Validation, defaulting and the `TokenManager` itself live in
43
+ // `resolveMerchantAuth`, shared with `@vaam-apps/vpay-sdk/stripe`'s authenticator:
44
+ // both entry points must mint the same assertion against the same
45
+ // endpoint with the same defaults, and one copy is how that stays true.
46
+ const auth = resolveMerchantAuth(options);
47
+ this.#baseUrl = auth.baseUrl;
48
+ this.#clientId = auth.clientId;
49
+ this.#tokenManager = auth.tokenManager;
50
+ const httpClient = new HttpClient({
51
+ baseUrl: `${this.#baseUrl}/v1`,
52
+ tokenManager: this.#tokenManager,
53
+ fetchImpl: auth.fetchImpl,
54
+ timeoutMs: auth.timeoutMs,
55
+ userAgent: auth.userAgent,
56
+ });
57
+ this.paymentIntents = new PaymentIntentsResource(httpClient);
58
+ this.checkout = new CheckoutResource(httpClient);
59
+ this.customers = new CustomersResource(httpClient);
60
+ this.invoices = new InvoicesResource(httpClient);
61
+ this.invoiceItems = new InvoiceItemsResource(httpClient);
62
+ this.refunds = new RefundsResource(httpClient);
63
+ this.events = new EventsResource(httpClient);
64
+ this.balance = new BalanceResource(httpClient);
65
+ this.accountHolders = new AccountHoldersResource(httpClient);
66
+ }
67
+ /** A safe, redacted representation — never the private key. */
68
+ [INSPECT_CUSTOM]() {
69
+ return `VpayClient ${inspect({ clientId: this.#clientId, baseUrl: this.#baseUrl }, { colors: false })}`;
70
+ }
71
+ /** A safe, redacted representation — never the private key. */
72
+ toJSON() {
73
+ return {
74
+ object: "vpay_client",
75
+ clientId: this.#clientId,
76
+ baseUrl: this.#baseUrl,
77
+ };
78
+ }
79
+ }