@rekey.dev/node 2.0.0-rc.3 → 2.0.0-rc.4

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/index.js CHANGED
@@ -20,8 +20,20 @@
20
20
  * ```
21
21
  */
22
22
  // The canonical error class lives in shared-types; import it for internal use
23
- // and re-export below so @rekey.dev/node's public surface is unchanged.
24
- import { RekeyError } from '@rekey.dev/shared-types';
23
+ // and re-export below so @rekey.dev/node's public surface is unchanged. The
24
+ // `/error` subpath is the zod-free module the class actually lives in — same
25
+ // class object the barrel re-exports, so `instanceof` is identical.
26
+ import { RekeyError } from '@rekey.dev/shared-types/error';
27
+ /**
28
+ * Default per-request deadline, in milliseconds. Matches the timeout the Rekey
29
+ * API itself uses when it POSTs your outbound webhooks.
30
+ *
31
+ * Without a deadline the effective timeout is undici's `headersTimeout` — five
32
+ * minutes — so a single unreachable Rekey deployment can pin one of your
33
+ * request handlers for that long. Ten seconds is long enough for any endpoint
34
+ * this SDK calls and short enough to fail a page instead of hanging it.
35
+ */
36
+ export const DEFAULT_TIMEOUT_MS = 10_000;
25
37
  // RekeyError is the shared class (imported above) — re-exported so the public
26
38
  // API name is preserved and `instanceof` is consistent with @rekey.dev/react.
27
39
  export { RekeyError };
@@ -42,6 +54,25 @@ export { RekeyError };
42
54
  * ```
43
55
  */
44
56
  export { WEBHOOK_EVENTS, KNOWN_WEBHOOK_EVENTS, isKnownWebhookEvent } from '@rekey.dev/shared-types';
57
+ /**
58
+ * Would `cancelSubscription`'s default (`atPeriodEnd: true`) actually leave
59
+ * this subscriber the rest of the period they paid for?
60
+ *
61
+ * Exported because a cancel confirmation has to say which outcome the customer
62
+ * is about to get, and it has to say so BEFORE the call — there is no response
63
+ * to read it off. It is the same function the API decides from, not a
64
+ * description of it, so a UI built on it cannot promise a behaviour the server
65
+ * does not have. See its docblock for the cases that still end immediately.
66
+ *
67
+ * @example
68
+ * ```ts
69
+ * const sub = await rekey.billing.getSubscription(token);
70
+ * const message = sub && cancelsAtPeriodEnd(sub)
71
+ * ? `You keep access until ${sub.currentPeriodEnd}.`
72
+ * : 'Cancelling takes effect straight away.';
73
+ * ```
74
+ */
75
+ export { cancelsAtPeriodEnd } from '@rekey.dev/shared-types';
45
76
  /**
46
77
  * Top-level Rekey client. Auth and billing live as namespaces
47
78
  * (`rekey.applications`, `rekey.auth`, `rekey.billing`) so an agent
@@ -51,6 +82,9 @@ export class Rekey {
51
82
  apiUrl;
52
83
  secretKey;
53
84
  fetchImpl;
85
+ timeoutMs;
86
+ signal;
87
+ config;
54
88
  /** Operations on the calling Application itself. */
55
89
  applications;
56
90
  /** Auth operations — sign-in, sign-up, sessions, passkeys, magic-link. */
@@ -82,9 +116,12 @@ export class Rekey {
82
116
  fix: 'Get a key from the Rekey panel under Application → API Keys, then pass it as `secretKey`.',
83
117
  });
84
118
  }
119
+ this.config = config;
85
120
  this.apiUrl = config.apiUrl.replace(/\/$/, '');
86
121
  this.secretKey = config.secretKey;
87
122
  this.fetchImpl = config.fetch ?? fetch;
123
+ this.timeoutMs = config.timeoutMs ?? DEFAULT_TIMEOUT_MS;
124
+ this.signal = config.signal;
88
125
  this.applications = new ApplicationsClient(this);
89
126
  this.auth = new AuthClient(this);
90
127
  this.billing = new BillingClient(this);
@@ -94,9 +131,75 @@ export class Rekey {
94
131
  this.credits = new CreditsClient(this);
95
132
  this.mcp = new McpClient(this);
96
133
  }
97
- /** @internal */
98
- async request(method, path, body, extraHeaders) {
99
- const res = await this.fetchImpl(`${this.apiUrl}${path}`, {
134
+ /**
135
+ * A clone of this client with different call options — the per-call knob for
136
+ * every wrapped method.
137
+ *
138
+ * Each namespace method (`billing.getPlans()`, `auth.signIn()`, …) has a
139
+ * fixed signature, so scoping one call is done by scoping the client rather
140
+ * than by threading an options argument through sixty-odd methods:
141
+ *
142
+ * @example Give one call a tighter deadline
143
+ * ```ts
144
+ * const plans = await rekey.with({ timeoutMs: 2_000 }).billing.getPlans();
145
+ * ```
146
+ *
147
+ * @example Tie every Rekey call to an inbound request's lifetime
148
+ * ```ts
149
+ * app.get('/me', async (req, res) => {
150
+ * const scoped = rekey.with({ signal: AbortSignal.any([req.signal]) });
151
+ * res.json(await scoped.auth.getCurrentUser(token));
152
+ * });
153
+ * ```
154
+ *
155
+ * Cheap — it rebuilds the namespace objects, holds no connections, and
156
+ * shares the same `fetch`.
157
+ */
158
+ with(options) {
159
+ const signal = options.signal && this.signal
160
+ ? AbortSignal.any([this.signal, options.signal])
161
+ : (options.signal ?? this.signal);
162
+ return new Rekey({
163
+ ...this.config,
164
+ timeoutMs: options.timeoutMs ?? this.config.timeoutMs,
165
+ ...(signal !== undefined && { signal }),
166
+ });
167
+ }
168
+ /**
169
+ * Call a Rekey endpoint this SDK does not wrap yet.
170
+ *
171
+ * This is a **supported** escape hatch, not an internal: when the API grows a
172
+ * route before the SDK does, use this instead of hand-rolling `fetch` — you
173
+ * keep the auth header, the `{ success, data }` unwrapping, the `RekeyError`
174
+ * mapping (including transport failures) and the deadline. It takes an
175
+ * options object precisely so a future knob does not need a new overload.
176
+ *
177
+ * Prefer a namespace method when one exists; those carry the endpoint's real
178
+ * types, this returns whatever `T` you claim.
179
+ *
180
+ * @example
181
+ * ```ts
182
+ * const seats = await rekey.request<{ used: number }>('GET', '/api/v1/seats');
183
+ *
184
+ * await rekey.request('POST', '/api/v1/seats', {
185
+ * body: { count: 5 },
186
+ * timeoutMs: 30_000,
187
+ * });
188
+ * ```
189
+ *
190
+ * @throws {RekeyError} the server's error envelope, or `REQUEST_TIMEOUT` /
191
+ * `REQUEST_ABORTED` / `NETWORK_ERROR` when the request never got an answer.
192
+ */
193
+ request(method, path, options) {
194
+ return this.send(method, path, options?.body, options?.headers, options);
195
+ }
196
+ /**
197
+ * @internal Positional workhorse behind {@link request}. Every wrapped method
198
+ * calls this; it stays positional because it is not part of the published
199
+ * surface (see `stripInternal` in tsconfig).
200
+ */
201
+ async send(method, path, body, extraHeaders, options) {
202
+ const res = await this.fetchWithDeadline(`${this.apiUrl}${path}`, {
100
203
  method,
101
204
  headers: {
102
205
  Authorization: `Bearer ${this.secretKey}`,
@@ -104,8 +207,8 @@ export class Rekey {
104
207
  ...extraHeaders,
105
208
  },
106
209
  ...(body !== undefined ? { body: JSON.stringify(body) } : {}),
107
- });
108
- const json = (await res.json().catch(() => ({})));
210
+ }, method, path, options);
211
+ const json = (await this.readJson(res, method, path, options));
109
212
  if (!res.ok || ('success' in json && json.success === false)) {
110
213
  const requestId = res.headers.get('x-request-id') ?? undefined;
111
214
  const err = 'error' in json
@@ -130,16 +233,16 @@ export class Rekey {
130
233
  * `{ success, data }` envelope). Throws `RekeyError` on non-2xx, mapping
131
234
  * the OAuth `{ error, error_description }` body when present.
132
235
  */
133
- async requestRaw(method, path, body, auth = true) {
134
- const res = await this.fetchImpl(`${this.apiUrl}${path}`, {
236
+ async requestRaw(method, path, body, auth = true, options) {
237
+ const res = await this.fetchWithDeadline(`${this.apiUrl}${path}`, {
135
238
  method,
136
239
  headers: {
137
240
  ...(auth ? { Authorization: `Bearer ${this.secretKey}` } : {}),
138
241
  ...(body !== undefined ? { 'Content-Type': 'application/json' } : {}),
139
242
  },
140
243
  ...(body !== undefined ? { body: JSON.stringify(body) } : {}),
141
- });
142
- const json = (await res.json().catch(() => ({})));
244
+ }, method, path, options);
245
+ const json = (await this.readJson(res, method, path, options));
143
246
  if (!res.ok) {
144
247
  const code = typeof json.error === 'string' ? json.error : `HTTP_${res.status}`;
145
248
  const message = typeof json.error_description === 'string'
@@ -149,6 +252,82 @@ export class Rekey {
149
252
  }
150
253
  return json;
151
254
  }
255
+ /**
256
+ * @internal The one place `fetch` is called. Applies the deadline, composes
257
+ * the caller's signals, and turns anything the transport throws into a
258
+ * `RekeyError` — without this, `ECONNREFUSED` escaped as a bare `TypeError`
259
+ * and slipped straight through the documented
260
+ * `catch (e) { if (e instanceof RekeyError) … }` pattern.
261
+ */
262
+ async fetchWithDeadline(url, init, method, path, options) {
263
+ const deadline = createDeadline(options?.timeoutMs ?? this.timeoutMs, this.signal, options?.signal);
264
+ try {
265
+ return await this.fetchImpl(url, {
266
+ ...init,
267
+ ...(deadline.signal ? { signal: deadline.signal } : {}),
268
+ });
269
+ }
270
+ catch (cause) {
271
+ throw transportError(cause, deadline, method, path);
272
+ }
273
+ }
274
+ /**
275
+ * @internal Read the JSON body under the same deadline. A body that never
276
+ * finishes streaming is just as hanging as headers that never arrive, and a
277
+ * non-JSON body still degrades to `{}` the way it always did.
278
+ */
279
+ async readJson(res, method, path, options) {
280
+ try {
281
+ return await res.json();
282
+ }
283
+ catch (cause) {
284
+ if (isAbortError(cause)) {
285
+ throw transportError(cause, createDeadline(options?.timeoutMs ?? this.timeoutMs, this.signal, options?.signal), method, path);
286
+ }
287
+ return {};
288
+ }
289
+ }
290
+ }
291
+ function createDeadline(timeoutMs, clientSignal, callSignal) {
292
+ const callerSignals = [clientSignal, callSignal].filter((s) => s !== undefined);
293
+ const timer = timeoutMs > 0 ? AbortSignal.timeout(timeoutMs) : undefined;
294
+ const all = timer ? [timer, ...callerSignals] : callerSignals;
295
+ const signal = all.length === 0 ? undefined : all.length === 1 ? all[0] : AbortSignal.any(all);
296
+ return { signal, timeoutMs, timer, callerSignals };
297
+ }
298
+ function isAbortError(err) {
299
+ return (err instanceof Error && (err.name === 'AbortError' || err.name === 'TimeoutError'));
300
+ }
301
+ /**
302
+ * Map whatever `fetch` rejected with onto a `RekeyError`. Three codes, because
303
+ * three different things go wrong and they want different responses:
304
+ * `REQUEST_ABORTED` (you asked), `REQUEST_TIMEOUT` (retry / raise the limit),
305
+ * `NETWORK_ERROR` (check the URL, DNS, TLS).
306
+ */
307
+ function transportError(cause, deadline, method, path) {
308
+ const where = `${method} ${path}`;
309
+ if (deadline.callerSignals.some((s) => s.aborted)) {
310
+ return new RekeyError({
311
+ code: 'REQUEST_ABORTED',
312
+ message: `${where} was aborted by the caller's AbortSignal.`,
313
+ fix: 'This is your own cancellation — swallow it, or check the signal you passed to `signal` / `Rekey.with({ signal })`.',
314
+ cause,
315
+ });
316
+ }
317
+ if (deadline.timer?.aborted || (isAbortError(cause) && deadline.timer !== undefined)) {
318
+ return new RekeyError({
319
+ code: 'REQUEST_TIMEOUT',
320
+ message: `${where} exceeded the ${deadline.timeoutMs}ms request deadline.`,
321
+ fix: 'Retry, or raise `timeoutMs` on the client / this call if the endpoint is legitimately slow. If it never responds, check `apiUrl` points at a reachable Rekey deployment.',
322
+ cause,
323
+ });
324
+ }
325
+ return new RekeyError({
326
+ code: 'NETWORK_ERROR',
327
+ message: `${where} failed before the server answered: ${cause instanceof Error ? cause.message : String(cause)}`,
328
+ fix: 'Check `apiUrl`, DNS, and that the Rekey deployment is reachable from this host. The underlying error is on `error.cause`.',
329
+ cause,
330
+ });
152
331
  }
153
332
  /**
154
333
  * MCP helpers for customers running their OWN MCP server behind Rekey auth.
@@ -165,7 +344,7 @@ class McpClient {
165
344
  async slug() {
166
345
  if (this.slugCache)
167
346
  return this.slugCache;
168
- const app = await this.client.request('GET', '/api/v1/me/');
347
+ const app = await this.client.send('GET', '/api/v1/me/');
169
348
  this.slugCache = app.slug;
170
349
  return app.slug;
171
350
  }
@@ -208,7 +387,7 @@ class ApplicationsClient {
208
387
  * @throws {RekeyError} with `code: "API_KEY_INVALID"` if the key is wrong/revoked/expired.
209
388
  */
210
389
  me() {
211
- return this.client.request('GET', '/api/v1/me/');
390
+ return this.client.send('GET', '/api/v1/me/');
212
391
  }
213
392
  }
214
393
  class AuthClient {
@@ -239,7 +418,7 @@ class AuthClient {
239
418
  * @throws {RekeyError} `AUTH_METHOD_DISABLED` (400) if the Application doesn't have `"password"` enabled.
240
419
  */
241
420
  signUp(input) {
242
- return this.client.request('POST', '/api/v1/auth/sign-up', input);
421
+ return this.client.send('POST', '/api/v1/auth/sign-up', input);
243
422
  }
244
423
  /**
245
424
  * Authenticate an existing end-user with email + password.
@@ -261,7 +440,7 @@ class AuthClient {
261
440
  * `sendVerificationEmail`), not for the password again.
262
441
  */
263
442
  signIn(input) {
264
- return this.client.request('POST', '/api/v1/auth/sign-in', input);
443
+ return this.client.send('POST', '/api/v1/auth/sign-in', input);
265
444
  }
266
445
  /**
267
446
  * Exchange an MFA challenge token + TOTP/backup code for a real session.
@@ -275,7 +454,7 @@ class AuthClient {
275
454
  * verify against the user's TOTP secret or remaining backup codes.
276
455
  */
277
456
  mfaVerify(input) {
278
- return this.client.request('POST', '/api/v1/auth/mfa-verify', input);
457
+ return this.client.send('POST', '/api/v1/auth/mfa-verify', input);
279
458
  }
280
459
  /**
281
460
  * Request a magic-link sign-in email. Enumeration-safe: same response
@@ -284,7 +463,7 @@ class AuthClient {
284
463
  * is null; otherwise the raw token is returned for you to forward.
285
464
  */
286
465
  requestMagicLink(input) {
287
- return this.client.request('POST', '/api/v1/auth/magic-link/request', input);
466
+ return this.client.send('POST', '/api/v1/auth/magic-link/request', input);
288
467
  }
289
468
  /**
290
469
  * Consume a magic-link token. Returns `SignInOutcome` — branch on
@@ -293,7 +472,7 @@ class AuthClient {
293
472
  * `mfaVerify(...)`.
294
473
  */
295
474
  verifyMagicLink(input) {
296
- return this.client.request('POST', '/api/v1/auth/magic-link/verify', input);
475
+ return this.client.send('POST', '/api/v1/auth/magic-link/verify', input);
297
476
  }
298
477
  /**
299
478
  * Begin a passkey authentication ceremony. Returns the WebAuthn options
@@ -302,7 +481,7 @@ class AuthClient {
302
481
  * pass both back via `verifyPasskeyAuthentication(...)`.
303
482
  */
304
483
  startPasskeyAuthentication(input) {
305
- return this.client.request('POST', '/api/v1/auth/passkey/authenticate/start', input ?? {});
484
+ return this.client.send('POST', '/api/v1/auth/passkey/authenticate/start', input ?? {});
306
485
  }
307
486
  /**
308
487
  * Complete a passkey authentication. Returns the same `SignInOutcome`
@@ -310,7 +489,7 @@ class AuthClient {
310
489
  * `mfaRequired` will always be `false` in practice.
311
490
  */
312
491
  verifyPasskeyAuthentication(input) {
313
- return this.client.request('POST', '/api/v1/auth/passkey/authenticate/complete', input);
492
+ return this.client.send('POST', '/api/v1/auth/passkey/authenticate/complete', input);
314
493
  }
315
494
  /**
316
495
  * Begin a passkey registration ceremony for an authenticated user.
@@ -319,24 +498,29 @@ class AuthClient {
319
498
  * `verifyPasskeyRegistration(...)`.
320
499
  */
321
500
  startPasskeyRegistration(accessToken) {
322
- return this.client.request('POST', '/api/v1/auth/passkey/register/start', undefined, {
501
+ return this.client.send('POST', '/api/v1/auth/passkey/register/start', undefined, {
323
502
  'X-Rekey-User-Token': accessToken,
324
503
  });
325
504
  }
326
505
  verifyPasskeyRegistration(accessToken, input) {
327
- return this.client.request('POST', '/api/v1/auth/passkey/register/complete', input, {
506
+ return this.client.send('POST', '/api/v1/auth/passkey/register/complete', input, {
328
507
  'X-Rekey-User-Token': accessToken,
329
508
  });
330
509
  }
331
- /** List the user's registered passkeys. */
332
- listPasskeys(accessToken) {
333
- return this.client.request('GET', '/api/v1/auth/passkeys', undefined, {
510
+ /**
511
+ * List the user's registered passkeys, newest first.
512
+ *
513
+ * Returns `{items, page}` — `page.total` is the number of passkeys the user
514
+ * has, independent of the window served.
515
+ */
516
+ listPasskeys(accessToken, page) {
517
+ return this.client.send('GET', `/api/v1/auth/passkeys${listQuery(page)}`, undefined, {
334
518
  'X-Rekey-User-Token': accessToken,
335
519
  });
336
520
  }
337
521
  /** Remove a passkey. Returns `{deleted: false}` if the row doesn't belong to this user. */
338
522
  deletePasskey(accessToken, credentialRowId) {
339
- return this.client.request('DELETE', `/api/v1/auth/passkeys/${encodeURIComponent(credentialRowId)}`, undefined, { 'X-Rekey-User-Token': accessToken });
523
+ return this.client.send('DELETE', `/api/v1/auth/passkeys/${encodeURIComponent(credentialRowId)}`, undefined, { 'X-Rekey-User-Token': accessToken });
340
524
  }
341
525
  // End-user organization / team methods live on `rekey.organizations.*`
342
526
  // (OrganizationsClient) — the canonical, fuller surface. The earlier
@@ -351,7 +535,7 @@ class AuthClient {
351
535
  * by a different Application than the calling secret key represents.
352
536
  */
353
537
  getCurrentUser(accessToken) {
354
- return this.client.request('GET', '/api/v1/users/me/', undefined, {
538
+ return this.client.send('GET', '/api/v1/users/me/', undefined, {
355
539
  'X-Rekey-User-Token': accessToken,
356
540
  });
357
541
  }
@@ -383,7 +567,7 @@ class AuthClient {
383
567
  * @throws {RekeyError} `USER_TOKEN_INVALID` (401) if expired/forged/wrong-secret.
384
568
  */
385
569
  updateCurrentUser(accessToken, input) {
386
- return this.client.request('PATCH', '/api/v1/users/me/', input, {
570
+ return this.client.send('PATCH', '/api/v1/users/me/', input, {
387
571
  'X-Rekey-User-Token': accessToken,
388
572
  });
389
573
  }
@@ -400,7 +584,7 @@ class AuthClient {
400
584
  // /auth/refresh returns the same shape as /auth/mfa-verify — always a
401
585
  // full session (refresh requires a prior MFA-verified session by
402
586
  // definition).
403
- return this.client.request('POST', '/api/v1/auth/refresh', { refreshToken });
587
+ return this.client.send('POST', '/api/v1/auth/refresh', { refreshToken });
404
588
  }
405
589
  /**
406
590
  * Revoke a refresh token. Idempotent — no-op for unknown tokens. The
@@ -409,7 +593,7 @@ class AuthClient {
409
593
  * the access token from your client.
410
594
  */
411
595
  signOut(refreshToken) {
412
- return this.client.request('POST', '/api/v1/auth/sign-out', { refreshToken });
596
+ return this.client.send('POST', '/api/v1/auth/sign-out', { refreshToken });
413
597
  }
414
598
  /**
415
599
  * Request a password reset for an email. Always succeeds — never tells you
@@ -431,7 +615,7 @@ class AuthClient {
431
615
  * ```
432
616
  */
433
617
  requestPasswordReset(input) {
434
- return this.client.request('POST', '/api/v1/auth/forgot-password', input);
618
+ return this.client.send('POST', '/api/v1/auth/forgot-password', input);
435
619
  }
436
620
  /**
437
621
  * Consume a reset token + set a new password. Single-use. On success,
@@ -441,7 +625,7 @@ class AuthClient {
441
625
  * @throws {RekeyError} `PASSWORD_TOO_SHORT` if below the Application's `passwordMinLength`
442
626
  */
443
627
  resetPassword(input) {
444
- return this.client.request('POST', '/api/v1/auth/reset-password', input);
628
+ return this.client.send('POST', '/api/v1/auth/reset-password', input);
445
629
  }
446
630
  /**
447
631
  * Authenticated password change. Pass the user's *current* access token.
@@ -449,7 +633,7 @@ class AuthClient {
449
633
  * are signed out.
450
634
  */
451
635
  changePassword(accessToken, input) {
452
- return this.client.request('POST', '/api/v1/auth/change-password', input, {
636
+ return this.client.send('POST', '/api/v1/auth/change-password', input, {
453
637
  'X-Rekey-User-Token': accessToken,
454
638
  });
455
639
  }
@@ -459,7 +643,7 @@ class AuthClient {
459
643
  * — clear it client-side for full logout.
460
644
  */
461
645
  signOutEverywhere(accessToken) {
462
- return this.client.request('POST', '/api/v1/auth/sign-out-everywhere', undefined, { 'X-Rekey-User-Token': accessToken });
646
+ return this.client.send('POST', '/api/v1/auth/sign-out-everywhere', undefined, { 'X-Rekey-User-Token': accessToken });
463
647
  }
464
648
  /**
465
649
  * Send (or re-send) an email-verification link to the current user.
@@ -471,7 +655,7 @@ class AuthClient {
471
655
  * (e.g. `https://app.example.com/verify?t={token}`).
472
656
  */
473
657
  sendVerificationEmail(accessToken, input) {
474
- return this.client.request('POST', '/api/v1/auth/send-verification', input ?? {}, {
658
+ return this.client.send('POST', '/api/v1/auth/send-verification', input ?? {}, {
475
659
  'X-Rekey-User-Token': accessToken,
476
660
  });
477
661
  }
@@ -509,7 +693,7 @@ class AuthClient {
509
693
  * ```
510
694
  */
511
695
  resendVerificationEmail(input) {
512
- return this.client.request('POST', '/api/v1/auth/resend-verification', input);
696
+ return this.client.send('POST', '/api/v1/auth/resend-verification', input);
513
697
  }
514
698
  /**
515
699
  * Consume an email-verification token. Single-use, 24-hour lifetime.
@@ -517,7 +701,7 @@ class AuthClient {
517
701
  * tokens are refused with `EMAIL_VERIFICATION_TOKEN_WRONG_APPLICATION`.
518
702
  */
519
703
  verifyEmail(input) {
520
- return this.client.request('POST', '/api/v1/auth/verify-email', input);
704
+ return this.client.send('POST', '/api/v1/auth/verify-email', input);
521
705
  }
522
706
  // ---------- Active sessions ----------
523
707
  /**
@@ -525,14 +709,14 @@ class AuthClient {
525
709
  * first. Each carries the User-Agent + IP captured at issue time and an
526
710
  * `id` you can pass to `revokeSession(...)`.
527
711
  */
528
- listSessions(accessToken) {
529
- return this.client.request('GET', '/api/v1/auth/sessions', undefined, {
712
+ listSessions(accessToken, page) {
713
+ return this.client.send('GET', `/api/v1/auth/sessions${listQuery(page)}`, undefined, {
530
714
  'X-Rekey-User-Token': accessToken,
531
715
  });
532
716
  }
533
717
  /** Revoke one session by id. Idempotent — `{ revoked: false }` if it isn't this user's. */
534
718
  revokeSession(accessToken, sessionId) {
535
- return this.client.request('DELETE', `/api/v1/auth/sessions/${encodeURIComponent(sessionId)}`, undefined, { 'X-Rekey-User-Token': accessToken });
719
+ return this.client.send('DELETE', `/api/v1/auth/sessions/${encodeURIComponent(sessionId)}`, undefined, { 'X-Rekey-User-Token': accessToken });
536
720
  }
537
721
  // ---------- MFA enrollment / management ----------
538
722
  //
@@ -542,7 +726,7 @@ class AuthClient {
542
726
  // (403) when the policy is "off".
543
727
  /** MFA enrollment status for the current user, plus the Application's policy. */
544
728
  mfaStatus(accessToken) {
545
- return this.client.request('GET', '/api/v1/auth/mfa/status', undefined, {
729
+ return this.client.send('GET', '/api/v1/auth/mfa/status', undefined, {
546
730
  'X-Rekey-User-Token': accessToken,
547
731
  });
548
732
  }
@@ -552,13 +736,13 @@ class AuthClient {
552
736
  * Only SHA-256 hashes of the backup codes are stored — show them once.
553
737
  */
554
738
  mfaSetup(accessToken) {
555
- return this.client.request('POST', '/api/v1/auth/mfa/setup', undefined, {
739
+ return this.client.send('POST', '/api/v1/auth/mfa/setup', undefined, {
556
740
  'X-Rekey-User-Token': accessToken,
557
741
  });
558
742
  }
559
743
  /** Confirm enrollment by submitting the current 6-digit TOTP code. */
560
744
  confirmMfaSetup(accessToken, code) {
561
- return this.client.request('POST', '/api/v1/auth/mfa/setup-confirm', { code }, {
745
+ return this.client.send('POST', '/api/v1/auth/mfa/setup-confirm', { code }, {
562
746
  'X-Rekey-User-Token': accessToken,
563
747
  });
564
748
  }
@@ -567,13 +751,13 @@ class AuthClient {
567
751
  * Backup codes are single-use — consumed on success. Returns `{ ok }`.
568
752
  */
569
753
  mfaChallenge(accessToken, code) {
570
- return this.client.request('POST', '/api/v1/auth/mfa/challenge', { code }, {
754
+ return this.client.send('POST', '/api/v1/auth/mfa/challenge', { code }, {
571
755
  'X-Rekey-User-Token': accessToken,
572
756
  });
573
757
  }
574
758
  /** Disable MFA for the current user. */
575
759
  disableMfa(accessToken) {
576
- return this.client.request('POST', '/api/v1/auth/mfa/disable', undefined, {
760
+ return this.client.send('POST', '/api/v1/auth/mfa/disable', undefined, {
577
761
  'X-Rekey-User-Token': accessToken,
578
762
  });
579
763
  }
@@ -587,7 +771,7 @@ class AuthClient {
587
771
  * unguessable `state` and verify it on return before calling `completeOAuth`.
588
772
  */
589
773
  startOAuth(provider, state) {
590
- return this.client.request('POST', `/api/v1/auth/oauth/${encodeURIComponent(provider)}/start`, { state });
774
+ return this.client.send('POST', `/api/v1/auth/oauth/${encodeURIComponent(provider)}/start`, { state });
591
775
  }
592
776
  /**
593
777
  * Exchange the provider `code` for a Rekey session. Returns a
@@ -595,17 +779,17 @@ class AuthClient {
595
779
  * Verify the `state` CSRF value yourself before calling.
596
780
  */
597
781
  completeOAuth(provider, code) {
598
- return this.client.request('POST', `/api/v1/auth/oauth/${encodeURIComponent(provider)}/callback`, { code });
782
+ return this.client.send('POST', `/api/v1/auth/oauth/${encodeURIComponent(provider)}/callback`, { code });
599
783
  }
600
784
  /** List the OAuth providers linked to the current user. */
601
785
  listOAuthIdentities(accessToken) {
602
- return this.client.request('GET', '/api/v1/auth/oauth/identities', undefined, {
786
+ return this.client.send('GET', '/api/v1/auth/oauth/identities', undefined, {
603
787
  'X-Rekey-User-Token': accessToken,
604
788
  });
605
789
  }
606
790
  /** Begin linking a provider to the *currently authenticated* user. */
607
791
  startOAuthLink(accessToken, provider, state) {
608
- return this.client.request('POST', `/api/v1/auth/oauth/${encodeURIComponent(provider)}/link/start`, { state }, { 'X-Rekey-User-Token': accessToken });
792
+ return this.client.send('POST', `/api/v1/auth/oauth/${encodeURIComponent(provider)}/link/start`, { state }, { 'X-Rekey-User-Token': accessToken });
609
793
  }
610
794
  /**
611
795
  * Complete an OAuth link — attaches the provider identity to the current
@@ -613,14 +797,14 @@ class AuthClient {
613
797
  * when the provider account already belongs to a different user.
614
798
  */
615
799
  completeOAuthLink(accessToken, provider, code) {
616
- return this.client.request('POST', `/api/v1/auth/oauth/${encodeURIComponent(provider)}/link/complete`, { code }, { 'X-Rekey-User-Token': accessToken });
800
+ return this.client.send('POST', `/api/v1/auth/oauth/${encodeURIComponent(provider)}/link/complete`, { code }, { 'X-Rekey-User-Token': accessToken });
617
801
  }
618
802
  /**
619
803
  * Remove a linked provider. Refuses with `OAUTH_UNLINK_WOULD_LOCK_OUT` (409)
620
804
  * if it would leave the account with no way to sign in.
621
805
  */
622
806
  unlinkOAuth(accessToken, provider) {
623
- return this.client.request('DELETE', `/api/v1/auth/oauth/${encodeURIComponent(provider)}`, undefined, { 'X-Rekey-User-Token': accessToken });
807
+ return this.client.send('DELETE', `/api/v1/auth/oauth/${encodeURIComponent(provider)}`, undefined, { 'X-Rekey-User-Token': accessToken });
624
808
  }
625
809
  }
626
810
  function listQuery(page) {
@@ -641,47 +825,55 @@ class OrganizationsClient {
641
825
  }
642
826
  /** Create an organization; the calling user becomes the OWNER. */
643
827
  create(accessToken, input) {
644
- return this.client.request('POST', '/api/v1/users/me/organizations/', input, {
828
+ return this.client.send('POST', '/api/v1/users/me/organizations/', input, {
645
829
  'X-Rekey-User-Token': accessToken,
646
830
  });
647
831
  }
648
- /** List organizations the calling user belongs to, with their role. The
649
- * result is paginated (default 50, max 100); pass `page.offset` for more. */
832
+ /**
833
+ * List organizations the calling user belongs to, with their role.
834
+ *
835
+ * Paginated (default 50, max 100). Read `page.hasMore` / `page.total` from
836
+ * the result rather than guessing from `items.length`.
837
+ */
650
838
  listMine(accessToken, page) {
651
- return this.client.request('GET', `/api/v1/users/me/organizations/${listQuery(page)}`, undefined, {
839
+ return this.client.send('GET', `/api/v1/users/me/organizations/${listQuery(page)}`, undefined, {
652
840
  'X-Rekey-User-Token': accessToken,
653
841
  });
654
842
  }
655
843
  /** Fetch one organization the caller belongs to. */
656
844
  get(accessToken, organizationId) {
657
- return this.client.request('GET', `/api/v1/users/me/organizations/${encodeURIComponent(organizationId)}`, undefined, { 'X-Rekey-User-Token': accessToken });
845
+ return this.client.send('GET', `/api/v1/users/me/organizations/${encodeURIComponent(organizationId)}`, undefined, { 'X-Rekey-User-Token': accessToken });
658
846
  }
659
847
  /** Update org name / metadata. OWNER + ADMIN only. */
660
848
  update(accessToken, organizationId, input) {
661
- return this.client.request('PATCH', `/api/v1/users/me/organizations/${encodeURIComponent(organizationId)}`, input, { 'X-Rekey-User-Token': accessToken });
849
+ return this.client.send('PATCH', `/api/v1/users/me/organizations/${encodeURIComponent(organizationId)}`, input, { 'X-Rekey-User-Token': accessToken });
662
850
  }
663
- /** List members of an organization the caller belongs to. Paginated
664
- * (default 50, max 100); pass `page.offset` to page beyond the first window. */
851
+ /**
852
+ * List members of an organization the caller belongs to.
853
+ *
854
+ * Paginated (default 50, max 100). `page.total` is the org's member count,
855
+ * so you do not need a second call to render "3 of 40".
856
+ */
665
857
  listMembers(accessToken, organizationId, page) {
666
- return this.client.request('GET', `/api/v1/users/me/organizations/${encodeURIComponent(organizationId)}/members${listQuery(page)}`, undefined, { 'X-Rekey-User-Token': accessToken });
858
+ return this.client.send('GET', `/api/v1/users/me/organizations/${encodeURIComponent(organizationId)}/members${listQuery(page)}`, undefined, { 'X-Rekey-User-Token': accessToken });
667
859
  }
668
860
  /**
669
861
  * Invite a user. Returns the raw token ONCE — surface via your own
670
862
  * email/share channel. OWNER + ADMIN only.
671
863
  */
672
864
  invite(accessToken, organizationId, input) {
673
- return this.client.request('POST', `/api/v1/users/me/organizations/${encodeURIComponent(organizationId)}/invitations`, input, { 'X-Rekey-User-Token': accessToken });
865
+ return this.client.send('POST', `/api/v1/users/me/organizations/${encodeURIComponent(organizationId)}/invitations`, input, { 'X-Rekey-User-Token': accessToken });
674
866
  }
675
867
  /** Revoke a pending invitation. OWNER + ADMIN only. Idempotent. */
676
868
  revokeInvitation(accessToken, organizationId, invitationId) {
677
- return this.client.request('POST', `/api/v1/users/me/organizations/${encodeURIComponent(organizationId)}/invitations/${encodeURIComponent(invitationId)}/revoke`, undefined, { 'X-Rekey-User-Token': accessToken });
869
+ return this.client.send('POST', `/api/v1/users/me/organizations/${encodeURIComponent(organizationId)}/invitations/${encodeURIComponent(invitationId)}/revoke`, undefined, { 'X-Rekey-User-Token': accessToken });
678
870
  }
679
871
  /**
680
872
  * Change a member's role. OWNER manages anyone; ADMIN manages MEMBER
681
873
  * only. Last-OWNER guard refuses demoting the only OWNER.
682
874
  */
683
875
  setMemberRole(accessToken, organizationId, targetEndUserId, input) {
684
- return this.client.request('PATCH', `/api/v1/users/me/organizations/${encodeURIComponent(organizationId)}/members/${encodeURIComponent(targetEndUserId)}`, input, { 'X-Rekey-User-Token': accessToken });
876
+ return this.client.send('PATCH', `/api/v1/users/me/organizations/${encodeURIComponent(organizationId)}/members/${encodeURIComponent(targetEndUserId)}`, input, { 'X-Rekey-User-Token': accessToken });
685
877
  }
686
878
  /**
687
879
  * Remove a member (or self). Refuses removing the last OWNER.
@@ -691,7 +883,7 @@ class OrganizationsClient {
691
883
  * rather than assuming it is always `true`.
692
884
  */
693
885
  removeMember(accessToken, organizationId, targetEndUserId) {
694
- return this.client.request('DELETE', `/api/v1/users/me/organizations/${encodeURIComponent(organizationId)}/members/${encodeURIComponent(targetEndUserId)}`, undefined, { 'X-Rekey-User-Token': accessToken });
886
+ return this.client.send('DELETE', `/api/v1/users/me/organizations/${encodeURIComponent(organizationId)}/members/${encodeURIComponent(targetEndUserId)}`, undefined, { 'X-Rekey-User-Token': accessToken });
695
887
  }
696
888
  /**
697
889
  * Self-leave. An OWNER cannot leave (payment + benefits are tied to the
@@ -699,14 +891,14 @@ class OrganizationsClient {
699
891
  * first, or demote yourself to ADMIN if there is another OWNER.
700
892
  */
701
893
  leave(accessToken, organizationId) {
702
- return this.client.request('POST', `/api/v1/users/me/organizations/${encodeURIComponent(organizationId)}/leave`, undefined, { 'X-Rekey-User-Token': accessToken });
894
+ return this.client.send('POST', `/api/v1/users/me/organizations/${encodeURIComponent(organizationId)}/leave`, undefined, { 'X-Rekey-User-Token': accessToken });
703
895
  }
704
896
  /**
705
897
  * Accept an organization invitation by raw token. Refuses cross-
706
898
  * Application invitations. Idempotent if the caller is already a member.
707
899
  */
708
900
  acceptInvitation(accessToken, input) {
709
- return this.client.request('POST', '/api/v1/auth/organizations/accept-invitation', input, { 'X-Rekey-User-Token': accessToken });
901
+ return this.client.send('POST', '/api/v1/auth/organizations/accept-invitation', input, { 'X-Rekey-User-Token': accessToken });
710
902
  }
711
903
  /**
712
904
  * Make `organizationId` the active org for this session (member-only).
@@ -717,14 +909,14 @@ class OrganizationsClient {
717
909
  * you switch again, clear it, or leave the org.
718
910
  */
719
911
  switch(accessToken, organizationId) {
720
- return this.client.request('POST', `/api/v1/users/me/organizations/${encodeURIComponent(organizationId)}/switch`, undefined, { 'X-Rekey-User-Token': accessToken });
912
+ return this.client.send('POST', `/api/v1/users/me/organizations/${encodeURIComponent(organizationId)}/switch`, undefined, { 'X-Rekey-User-Token': accessToken });
721
913
  }
722
914
  /**
723
915
  * Clear the active org — switch the session back to the personal pool.
724
916
  * Returns a fresh token pair (no active org); **store both**.
725
917
  */
726
918
  clearActive(accessToken) {
727
- return this.client.request('POST', '/api/v1/users/me/organizations/clear-active-organization', undefined, { 'X-Rekey-User-Token': accessToken });
919
+ return this.client.send('POST', '/api/v1/users/me/organizations/clear-active-organization', undefined, { 'X-Rekey-User-Token': accessToken });
728
920
  }
729
921
  }
730
922
  class LicensesClient {
@@ -753,7 +945,7 @@ class LicensesClient {
753
945
  * ```
754
946
  */
755
947
  verify(input) {
756
- return this.client.request('POST', '/api/v1/licenses/verify', input);
948
+ return this.client.send('POST', '/api/v1/licenses/verify', input);
757
949
  }
758
950
  }
759
951
  class UsageClient {
@@ -767,7 +959,7 @@ class UsageClient {
767
959
  * server time; pass an ISO string when ingesting historical events.
768
960
  */
769
961
  record(input) {
770
- return this.client.request('POST', '/api/v1/usage/record', input);
962
+ return this.client.send('POST', '/api/v1/usage/record', input);
771
963
  }
772
964
  /**
773
965
  * Sum recorded quantity for a meter, optionally bounded by a time window
@@ -785,7 +977,7 @@ class UsageClient {
785
977
  params.set('endUserId', input.endUserId);
786
978
  if (input.organizationId)
787
979
  params.set('organizationId', input.organizationId);
788
- return this.client.request('GET', `/api/v1/usage/aggregate?${params.toString()}`);
980
+ return this.client.send('GET', `/api/v1/usage/aggregate?${params.toString()}`);
789
981
  }
790
982
  }
791
983
  function creditSubjectQuery(subject) {
@@ -811,7 +1003,7 @@ class CreditsClient {
811
1003
  }
812
1004
  /** Current spendable balance for a subject (end-user or org); 0 if none. */
813
1005
  getBalance(subject) {
814
- return this.client.request('GET', `/api/v1/credits/balance?${creditSubjectQuery(subject)}`);
1006
+ return this.client.send('GET', `/api/v1/credits/balance?${creditSubjectQuery(subject)}`);
815
1007
  }
816
1008
  /**
817
1009
  * Deduct credits from a subject (end-user or org pool). Throws `RekeyError`
@@ -821,7 +1013,7 @@ class CreditsClient {
821
1013
  * double-charges — a repeat returns the original result with `applied: false`.
822
1014
  */
823
1015
  consume(input) {
824
- return this.client.request('POST', '/api/v1/credits/consume', input);
1016
+ return this.client.send('POST', '/api/v1/credits/consume', input);
825
1017
  }
826
1018
  /**
827
1019
  * Ledger entries for a subject, newest first. Pass `offset` to page back
@@ -834,7 +1026,7 @@ class CreditsClient {
834
1026
  params.set('limit', String(limit));
835
1027
  if (offset !== undefined)
836
1028
  params.set('offset', String(offset));
837
- return this.client.request('GET', `/api/v1/credits/ledger?${params.toString()}`);
1029
+ return this.client.send('GET', `/api/v1/credits/ledger?${params.toString()}`);
838
1030
  }
839
1031
  }
840
1032
  /**
@@ -949,7 +1141,14 @@ async function loadJwks(options, forceRefetch) {
949
1141
  if (cached && !forceRefetch && Date.now() - cached.fetchedAt <= ttl)
950
1142
  return cached.jwks;
951
1143
  const fetchImpl = options.fetch ?? fetch;
952
- const res = await fetchImpl(url);
1144
+ const deadline = createDeadline(options.timeoutMs ?? DEFAULT_TIMEOUT_MS, undefined, options.signal);
1145
+ let res;
1146
+ try {
1147
+ res = await fetchImpl(url, deadline.signal ? { signal: deadline.signal } : {});
1148
+ }
1149
+ catch (cause) {
1150
+ throw transportError(cause, deadline, 'GET', url);
1151
+ }
953
1152
  if (!res.ok) {
954
1153
  throw new RekeyError({
955
1154
  code: 'JWKS_FETCH_FAILED',
@@ -958,7 +1157,20 @@ async function loadJwks(options, forceRefetch) {
958
1157
  statusCode: res.status,
959
1158
  });
960
1159
  }
961
- const jwks = (await res.json());
1160
+ let jwks;
1161
+ try {
1162
+ jwks = (await res.json());
1163
+ }
1164
+ catch (cause) {
1165
+ if (isAbortError(cause))
1166
+ throw transportError(cause, deadline, 'GET', url);
1167
+ throw new RekeyError({
1168
+ code: 'JWKS_FETCH_FAILED',
1169
+ message: `The JWKS endpoint at ${url} did not return JSON.`,
1170
+ fix: 'Check the URL points at /.well-known/jwks.json, not an HTML error page.',
1171
+ cause,
1172
+ });
1173
+ }
962
1174
  if (!jwks || !Array.isArray(jwks.keys)) {
963
1175
  throw new RekeyError({
964
1176
  code: 'JWKS_FETCH_FAILED',
@@ -995,12 +1207,20 @@ async function loadJwks(options, forceRefetch) {
995
1207
  * import { verifyAccessToken } from '@rekey.dev/node';
996
1208
  *
997
1209
  * const claims = await verifyAccessToken(req.headers['x-rekey-user-token'], {
1210
+ * applicationId: MY_APP_ID,
998
1211
  * jwksUrl: 'https://rekey.example.com/.well-known/jwks.json',
999
1212
  * });
1000
- * if (claims.applicationId !== MY_APP_ID) throw new Error('wrong app');
1001
1213
  * req.userId = claims.sub;
1002
1214
  * ```
1003
1215
  *
1216
+ * `applicationId` is required and checked inside this function. The RS256
1217
+ * keypair is deployment-wide and `eu_access` tokens carry no `iss`/`aud`, so
1218
+ * without it a token minted for any other Application on the same deployment
1219
+ * verifies here with a perfectly valid signature. This example used to show
1220
+ * the comparison being done by the caller afterwards, which is precisely why
1221
+ * it moved inside: the shortest correct path should not be the one nobody
1222
+ * takes.
1223
+ *
1004
1224
  * @throws {RekeyError} `TOKEN_ALG_NOT_RS256` — token is HS256 (app hasn't opted in) or another alg.
1005
1225
  * @throws {RekeyError} `TOKEN_KID_UNKNOWN` — `kid` not in the JWKS (forged, or key deleted).
1006
1226
  * @throws {RekeyError} `USER_TOKEN_EXPIRED` — `exp` passed; refresh the session.
@@ -1076,6 +1296,16 @@ export async function verifyAccessToken(token, options) {
1076
1296
  if (typeof payload.sub !== 'string' || typeof payload.applicationId !== 'string') {
1077
1297
  throw invalid('Token is missing the sub/applicationId claims.');
1078
1298
  }
1299
+ // Bind the token to ONE Application. The signing key is deployment-wide and
1300
+ // `eu_access` carries no `iss`/`aud`, so a token minted for a different
1301
+ // Application on the same deployment is cryptographically valid here — on a
1302
+ // multi-app self-host that means accepting someone else's end-user as your
1303
+ // own. The API does compare this server-side; the SDK left it to the caller
1304
+ // and documented it as a follow-up step, which made the shortest correct
1305
+ // path the one nobody takes.
1306
+ if (payload.applicationId !== options.applicationId) {
1307
+ throw invalid('Token was issued for a different Application.');
1308
+ }
1079
1309
  const nowSec = Math.floor((options.now ? options.now() : Date.now()) / 1000);
1080
1310
  if (typeof payload.exp !== 'number' || payload.exp <= nowSec) {
1081
1311
  throw new RekeyError({
@@ -1100,17 +1330,35 @@ class BillingClient {
1100
1330
  * `amount` is in the smallest currency unit (cents/paise/sen) — never
1101
1331
  * a float. Format on display: `${amount / 100} ${currency}`.
1102
1332
  */
1103
- getPlans() {
1104
- return this.client.request('GET', '/api/v1/billing/plans');
1333
+ getPlans(page) {
1334
+ return this.client.send('GET', `/api/v1/billing/plans${listQuery(page)}`);
1105
1335
  }
1106
1336
  /**
1107
1337
  * Fetch the current end-user's active subscription, or `null` if they
1108
1338
  * have none. Returns the most recent ACTIVE / PENDING / PAST_DUE row.
1109
1339
  *
1110
1340
  * Pass the user's access token (the SDK puts it in `X-Rekey-User-Token`).
1341
+ *
1342
+ * `opts.includeEnded` falls back to the most recent CANCELED/EXPIRED
1343
+ * subscription **only when the answer would otherwise be null** — for a
1344
+ * billing page that has to tell a former subscriber what they were on and
1345
+ * when it ended, rather than showing them the same blank state as somebody
1346
+ * who never subscribed. It can never replace a live subscription, so it is
1347
+ * safe to add to an existing call; it is off by default all the same,
1348
+ * because an entitlement check wants the strict question.
1349
+ *
1350
+ * `opts.organizationId` reads an organization's subscription instead of the
1351
+ * user's own on an org-billed app. The caller must be a member.
1111
1352
  */
1112
- getSubscription(accessToken) {
1113
- return this.client.request('GET', '/api/v1/billing/subscription', undefined, {
1353
+ getSubscription(accessToken, opts) {
1354
+ const qs = new URLSearchParams();
1355
+ if (opts?.organizationId)
1356
+ qs.set('organizationId', opts.organizationId);
1357
+ if (opts?.includeEnded)
1358
+ qs.set('includeEnded', 'true');
1359
+ const query = qs.toString();
1360
+ const suffix = query ? `?${query}` : '';
1361
+ return this.client.send('GET', `/api/v1/billing/subscription${suffix}`, undefined, {
1114
1362
  'X-Rekey-User-Token': accessToken,
1115
1363
  });
1116
1364
  }
@@ -1139,7 +1387,7 @@ class BillingClient {
1139
1387
  * ```
1140
1388
  */
1141
1389
  createCheckout(accessToken, input) {
1142
- return this.client.request('POST', '/api/v1/billing/checkout', input, {
1390
+ return this.client.send('POST', '/api/v1/billing/checkout', input, {
1143
1391
  'X-Rekey-User-Token': accessToken,
1144
1392
  });
1145
1393
  }
@@ -1153,7 +1401,7 @@ class BillingClient {
1153
1401
  * `COUPON_USER_LIMIT_REACHED`. Surface the message + fix to the user.
1154
1402
  */
1155
1403
  validateCoupon(accessToken, input) {
1156
- return this.client.request('POST', '/api/v1/billing/coupons/validate', input, {
1404
+ return this.client.send('POST', '/api/v1/billing/coupons/validate', input, {
1157
1405
  'X-Rekey-User-Token': accessToken,
1158
1406
  });
1159
1407
  }
@@ -1171,7 +1419,7 @@ class BillingClient {
1171
1419
  const headers = {};
1172
1420
  if (country)
1173
1421
  headers['x-country'] = country.toUpperCase();
1174
- return this.client.request('GET', '/api/v1/billing/providers', undefined, headers);
1422
+ return this.client.send('GET', '/api/v1/billing/providers', undefined, headers);
1175
1423
  }
1176
1424
  /**
1177
1425
  * Resolve the calling end-user's current entitlements — feature flags +
@@ -1190,7 +1438,7 @@ class BillingClient {
1190
1438
  const qs = opts?.organizationId
1191
1439
  ? `?organizationId=${encodeURIComponent(opts.organizationId)}`
1192
1440
  : '';
1193
- return this.client.request('GET', `/api/v1/billing/entitlements${qs}`, undefined, {
1441
+ return this.client.send('GET', `/api/v1/billing/entitlements${qs}`, undefined, {
1194
1442
  'X-Rekey-User-Token': accessToken,
1195
1443
  });
1196
1444
  }
@@ -1219,7 +1467,7 @@ class BillingClient {
1219
1467
  * ```
1220
1468
  */
1221
1469
  cancelSubscription(accessToken, input) {
1222
- return this.client.request('POST', '/api/v1/billing/subscription/cancel', {
1470
+ return this.client.send('POST', '/api/v1/billing/subscription/cancel', {
1223
1471
  // Omitted rather than sent as undefined so the API applies its own
1224
1472
  // default (at period end) instead of parsing a null-ish field.
1225
1473
  ...(input?.atPeriodEnd !== undefined && { atPeriodEnd: input.atPeriodEnd }),