@rekey.dev/node 2.0.0-rc.2 → 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,9 +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';
25
- import { createRequire } from 'node:module';
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;
26
37
  // RekeyError is the shared class (imported above) — re-exported so the public
27
38
  // API name is preserved and `instanceof` is consistent with @rekey.dev/react.
28
39
  export { RekeyError };
@@ -43,6 +54,25 @@ export { RekeyError };
43
54
  * ```
44
55
  */
45
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';
46
76
  /**
47
77
  * Top-level Rekey client. Auth and billing live as namespaces
48
78
  * (`rekey.applications`, `rekey.auth`, `rekey.billing`) so an agent
@@ -52,6 +82,9 @@ export class Rekey {
52
82
  apiUrl;
53
83
  secretKey;
54
84
  fetchImpl;
85
+ timeoutMs;
86
+ signal;
87
+ config;
55
88
  /** Operations on the calling Application itself. */
56
89
  applications;
57
90
  /** Auth operations — sign-in, sign-up, sessions, passkeys, magic-link. */
@@ -83,9 +116,12 @@ export class Rekey {
83
116
  fix: 'Get a key from the Rekey panel under Application → API Keys, then pass it as `secretKey`.',
84
117
  });
85
118
  }
119
+ this.config = config;
86
120
  this.apiUrl = config.apiUrl.replace(/\/$/, '');
87
121
  this.secretKey = config.secretKey;
88
122
  this.fetchImpl = config.fetch ?? fetch;
123
+ this.timeoutMs = config.timeoutMs ?? DEFAULT_TIMEOUT_MS;
124
+ this.signal = config.signal;
89
125
  this.applications = new ApplicationsClient(this);
90
126
  this.auth = new AuthClient(this);
91
127
  this.billing = new BillingClient(this);
@@ -95,9 +131,75 @@ export class Rekey {
95
131
  this.credits = new CreditsClient(this);
96
132
  this.mcp = new McpClient(this);
97
133
  }
98
- /** @internal */
99
- async request(method, path, body, extraHeaders) {
100
- 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}`, {
101
203
  method,
102
204
  headers: {
103
205
  Authorization: `Bearer ${this.secretKey}`,
@@ -105,8 +207,8 @@ export class Rekey {
105
207
  ...extraHeaders,
106
208
  },
107
209
  ...(body !== undefined ? { body: JSON.stringify(body) } : {}),
108
- });
109
- const json = (await res.json().catch(() => ({})));
210
+ }, method, path, options);
211
+ const json = (await this.readJson(res, method, path, options));
110
212
  if (!res.ok || ('success' in json && json.success === false)) {
111
213
  const requestId = res.headers.get('x-request-id') ?? undefined;
112
214
  const err = 'error' in json
@@ -131,16 +233,16 @@ export class Rekey {
131
233
  * `{ success, data }` envelope). Throws `RekeyError` on non-2xx, mapping
132
234
  * the OAuth `{ error, error_description }` body when present.
133
235
  */
134
- async requestRaw(method, path, body, auth = true) {
135
- 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}`, {
136
238
  method,
137
239
  headers: {
138
240
  ...(auth ? { Authorization: `Bearer ${this.secretKey}` } : {}),
139
241
  ...(body !== undefined ? { 'Content-Type': 'application/json' } : {}),
140
242
  },
141
243
  ...(body !== undefined ? { body: JSON.stringify(body) } : {}),
142
- });
143
- const json = (await res.json().catch(() => ({})));
244
+ }, method, path, options);
245
+ const json = (await this.readJson(res, method, path, options));
144
246
  if (!res.ok) {
145
247
  const code = typeof json.error === 'string' ? json.error : `HTTP_${res.status}`;
146
248
  const message = typeof json.error_description === 'string'
@@ -150,6 +252,82 @@ export class Rekey {
150
252
  }
151
253
  return json;
152
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
+ });
153
331
  }
154
332
  /**
155
333
  * MCP helpers for customers running their OWN MCP server behind Rekey auth.
@@ -166,7 +344,7 @@ class McpClient {
166
344
  async slug() {
167
345
  if (this.slugCache)
168
346
  return this.slugCache;
169
- const app = await this.client.request('GET', '/api/v1/me/');
347
+ const app = await this.client.send('GET', '/api/v1/me/');
170
348
  this.slugCache = app.slug;
171
349
  return app.slug;
172
350
  }
@@ -209,7 +387,7 @@ class ApplicationsClient {
209
387
  * @throws {RekeyError} with `code: "API_KEY_INVALID"` if the key is wrong/revoked/expired.
210
388
  */
211
389
  me() {
212
- return this.client.request('GET', '/api/v1/me/');
390
+ return this.client.send('GET', '/api/v1/me/');
213
391
  }
214
392
  }
215
393
  class AuthClient {
@@ -240,7 +418,7 @@ class AuthClient {
240
418
  * @throws {RekeyError} `AUTH_METHOD_DISABLED` (400) if the Application doesn't have `"password"` enabled.
241
419
  */
242
420
  signUp(input) {
243
- return this.client.request('POST', '/api/v1/auth/sign-up', input);
421
+ return this.client.send('POST', '/api/v1/auth/sign-up', input);
244
422
  }
245
423
  /**
246
424
  * Authenticate an existing end-user with email + password.
@@ -262,7 +440,7 @@ class AuthClient {
262
440
  * `sendVerificationEmail`), not for the password again.
263
441
  */
264
442
  signIn(input) {
265
- return this.client.request('POST', '/api/v1/auth/sign-in', input);
443
+ return this.client.send('POST', '/api/v1/auth/sign-in', input);
266
444
  }
267
445
  /**
268
446
  * Exchange an MFA challenge token + TOTP/backup code for a real session.
@@ -276,7 +454,7 @@ class AuthClient {
276
454
  * verify against the user's TOTP secret or remaining backup codes.
277
455
  */
278
456
  mfaVerify(input) {
279
- return this.client.request('POST', '/api/v1/auth/mfa-verify', input);
457
+ return this.client.send('POST', '/api/v1/auth/mfa-verify', input);
280
458
  }
281
459
  /**
282
460
  * Request a magic-link sign-in email. Enumeration-safe: same response
@@ -285,7 +463,7 @@ class AuthClient {
285
463
  * is null; otherwise the raw token is returned for you to forward.
286
464
  */
287
465
  requestMagicLink(input) {
288
- 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);
289
467
  }
290
468
  /**
291
469
  * Consume a magic-link token. Returns `SignInOutcome` — branch on
@@ -294,7 +472,7 @@ class AuthClient {
294
472
  * `mfaVerify(...)`.
295
473
  */
296
474
  verifyMagicLink(input) {
297
- 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);
298
476
  }
299
477
  /**
300
478
  * Begin a passkey authentication ceremony. Returns the WebAuthn options
@@ -303,7 +481,7 @@ class AuthClient {
303
481
  * pass both back via `verifyPasskeyAuthentication(...)`.
304
482
  */
305
483
  startPasskeyAuthentication(input) {
306
- 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 ?? {});
307
485
  }
308
486
  /**
309
487
  * Complete a passkey authentication. Returns the same `SignInOutcome`
@@ -311,7 +489,7 @@ class AuthClient {
311
489
  * `mfaRequired` will always be `false` in practice.
312
490
  */
313
491
  verifyPasskeyAuthentication(input) {
314
- 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);
315
493
  }
316
494
  /**
317
495
  * Begin a passkey registration ceremony for an authenticated user.
@@ -320,24 +498,29 @@ class AuthClient {
320
498
  * `verifyPasskeyRegistration(...)`.
321
499
  */
322
500
  startPasskeyRegistration(accessToken) {
323
- 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, {
324
502
  'X-Rekey-User-Token': accessToken,
325
503
  });
326
504
  }
327
505
  verifyPasskeyRegistration(accessToken, input) {
328
- 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, {
329
507
  'X-Rekey-User-Token': accessToken,
330
508
  });
331
509
  }
332
- /** List the user's registered passkeys. */
333
- listPasskeys(accessToken) {
334
- 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, {
335
518
  'X-Rekey-User-Token': accessToken,
336
519
  });
337
520
  }
338
521
  /** Remove a passkey. Returns `{deleted: false}` if the row doesn't belong to this user. */
339
522
  deletePasskey(accessToken, credentialRowId) {
340
- 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 });
341
524
  }
342
525
  // End-user organization / team methods live on `rekey.organizations.*`
343
526
  // (OrganizationsClient) — the canonical, fuller surface. The earlier
@@ -352,7 +535,7 @@ class AuthClient {
352
535
  * by a different Application than the calling secret key represents.
353
536
  */
354
537
  getCurrentUser(accessToken) {
355
- return this.client.request('GET', '/api/v1/users/me/', undefined, {
538
+ return this.client.send('GET', '/api/v1/users/me/', undefined, {
356
539
  'X-Rekey-User-Token': accessToken,
357
540
  });
358
541
  }
@@ -384,7 +567,7 @@ class AuthClient {
384
567
  * @throws {RekeyError} `USER_TOKEN_INVALID` (401) if expired/forged/wrong-secret.
385
568
  */
386
569
  updateCurrentUser(accessToken, input) {
387
- return this.client.request('PATCH', '/api/v1/users/me/', input, {
570
+ return this.client.send('PATCH', '/api/v1/users/me/', input, {
388
571
  'X-Rekey-User-Token': accessToken,
389
572
  });
390
573
  }
@@ -401,7 +584,7 @@ class AuthClient {
401
584
  // /auth/refresh returns the same shape as /auth/mfa-verify — always a
402
585
  // full session (refresh requires a prior MFA-verified session by
403
586
  // definition).
404
- return this.client.request('POST', '/api/v1/auth/refresh', { refreshToken });
587
+ return this.client.send('POST', '/api/v1/auth/refresh', { refreshToken });
405
588
  }
406
589
  /**
407
590
  * Revoke a refresh token. Idempotent — no-op for unknown tokens. The
@@ -410,7 +593,7 @@ class AuthClient {
410
593
  * the access token from your client.
411
594
  */
412
595
  signOut(refreshToken) {
413
- return this.client.request('POST', '/api/v1/auth/sign-out', { refreshToken });
596
+ return this.client.send('POST', '/api/v1/auth/sign-out', { refreshToken });
414
597
  }
415
598
  /**
416
599
  * Request a password reset for an email. Always succeeds — never tells you
@@ -432,7 +615,7 @@ class AuthClient {
432
615
  * ```
433
616
  */
434
617
  requestPasswordReset(input) {
435
- return this.client.request('POST', '/api/v1/auth/forgot-password', input);
618
+ return this.client.send('POST', '/api/v1/auth/forgot-password', input);
436
619
  }
437
620
  /**
438
621
  * Consume a reset token + set a new password. Single-use. On success,
@@ -442,7 +625,7 @@ class AuthClient {
442
625
  * @throws {RekeyError} `PASSWORD_TOO_SHORT` if below the Application's `passwordMinLength`
443
626
  */
444
627
  resetPassword(input) {
445
- return this.client.request('POST', '/api/v1/auth/reset-password', input);
628
+ return this.client.send('POST', '/api/v1/auth/reset-password', input);
446
629
  }
447
630
  /**
448
631
  * Authenticated password change. Pass the user's *current* access token.
@@ -450,7 +633,7 @@ class AuthClient {
450
633
  * are signed out.
451
634
  */
452
635
  changePassword(accessToken, input) {
453
- return this.client.request('POST', '/api/v1/auth/change-password', input, {
636
+ return this.client.send('POST', '/api/v1/auth/change-password', input, {
454
637
  'X-Rekey-User-Token': accessToken,
455
638
  });
456
639
  }
@@ -460,7 +643,7 @@ class AuthClient {
460
643
  * — clear it client-side for full logout.
461
644
  */
462
645
  signOutEverywhere(accessToken) {
463
- 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 });
464
647
  }
465
648
  /**
466
649
  * Send (or re-send) an email-verification link to the current user.
@@ -472,7 +655,7 @@ class AuthClient {
472
655
  * (e.g. `https://app.example.com/verify?t={token}`).
473
656
  */
474
657
  sendVerificationEmail(accessToken, input) {
475
- return this.client.request('POST', '/api/v1/auth/send-verification', input ?? {}, {
658
+ return this.client.send('POST', '/api/v1/auth/send-verification', input ?? {}, {
476
659
  'X-Rekey-User-Token': accessToken,
477
660
  });
478
661
  }
@@ -510,7 +693,7 @@ class AuthClient {
510
693
  * ```
511
694
  */
512
695
  resendVerificationEmail(input) {
513
- return this.client.request('POST', '/api/v1/auth/resend-verification', input);
696
+ return this.client.send('POST', '/api/v1/auth/resend-verification', input);
514
697
  }
515
698
  /**
516
699
  * Consume an email-verification token. Single-use, 24-hour lifetime.
@@ -518,7 +701,7 @@ class AuthClient {
518
701
  * tokens are refused with `EMAIL_VERIFICATION_TOKEN_WRONG_APPLICATION`.
519
702
  */
520
703
  verifyEmail(input) {
521
- return this.client.request('POST', '/api/v1/auth/verify-email', input);
704
+ return this.client.send('POST', '/api/v1/auth/verify-email', input);
522
705
  }
523
706
  // ---------- Active sessions ----------
524
707
  /**
@@ -526,14 +709,14 @@ class AuthClient {
526
709
  * first. Each carries the User-Agent + IP captured at issue time and an
527
710
  * `id` you can pass to `revokeSession(...)`.
528
711
  */
529
- listSessions(accessToken) {
530
- 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, {
531
714
  'X-Rekey-User-Token': accessToken,
532
715
  });
533
716
  }
534
717
  /** Revoke one session by id. Idempotent — `{ revoked: false }` if it isn't this user's. */
535
718
  revokeSession(accessToken, sessionId) {
536
- 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 });
537
720
  }
538
721
  // ---------- MFA enrollment / management ----------
539
722
  //
@@ -543,7 +726,7 @@ class AuthClient {
543
726
  // (403) when the policy is "off".
544
727
  /** MFA enrollment status for the current user, plus the Application's policy. */
545
728
  mfaStatus(accessToken) {
546
- return this.client.request('GET', '/api/v1/auth/mfa/status', undefined, {
729
+ return this.client.send('GET', '/api/v1/auth/mfa/status', undefined, {
547
730
  'X-Rekey-User-Token': accessToken,
548
731
  });
549
732
  }
@@ -553,13 +736,13 @@ class AuthClient {
553
736
  * Only SHA-256 hashes of the backup codes are stored — show them once.
554
737
  */
555
738
  mfaSetup(accessToken) {
556
- return this.client.request('POST', '/api/v1/auth/mfa/setup', undefined, {
739
+ return this.client.send('POST', '/api/v1/auth/mfa/setup', undefined, {
557
740
  'X-Rekey-User-Token': accessToken,
558
741
  });
559
742
  }
560
743
  /** Confirm enrollment by submitting the current 6-digit TOTP code. */
561
744
  confirmMfaSetup(accessToken, code) {
562
- 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 }, {
563
746
  'X-Rekey-User-Token': accessToken,
564
747
  });
565
748
  }
@@ -568,13 +751,13 @@ class AuthClient {
568
751
  * Backup codes are single-use — consumed on success. Returns `{ ok }`.
569
752
  */
570
753
  mfaChallenge(accessToken, code) {
571
- return this.client.request('POST', '/api/v1/auth/mfa/challenge', { code }, {
754
+ return this.client.send('POST', '/api/v1/auth/mfa/challenge', { code }, {
572
755
  'X-Rekey-User-Token': accessToken,
573
756
  });
574
757
  }
575
758
  /** Disable MFA for the current user. */
576
759
  disableMfa(accessToken) {
577
- return this.client.request('POST', '/api/v1/auth/mfa/disable', undefined, {
760
+ return this.client.send('POST', '/api/v1/auth/mfa/disable', undefined, {
578
761
  'X-Rekey-User-Token': accessToken,
579
762
  });
580
763
  }
@@ -588,7 +771,7 @@ class AuthClient {
588
771
  * unguessable `state` and verify it on return before calling `completeOAuth`.
589
772
  */
590
773
  startOAuth(provider, state) {
591
- 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 });
592
775
  }
593
776
  /**
594
777
  * Exchange the provider `code` for a Rekey session. Returns a
@@ -596,17 +779,17 @@ class AuthClient {
596
779
  * Verify the `state` CSRF value yourself before calling.
597
780
  */
598
781
  completeOAuth(provider, code) {
599
- 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 });
600
783
  }
601
784
  /** List the OAuth providers linked to the current user. */
602
785
  listOAuthIdentities(accessToken) {
603
- return this.client.request('GET', '/api/v1/auth/oauth/identities', undefined, {
786
+ return this.client.send('GET', '/api/v1/auth/oauth/identities', undefined, {
604
787
  'X-Rekey-User-Token': accessToken,
605
788
  });
606
789
  }
607
790
  /** Begin linking a provider to the *currently authenticated* user. */
608
791
  startOAuthLink(accessToken, provider, state) {
609
- 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 });
610
793
  }
611
794
  /**
612
795
  * Complete an OAuth link — attaches the provider identity to the current
@@ -614,14 +797,14 @@ class AuthClient {
614
797
  * when the provider account already belongs to a different user.
615
798
  */
616
799
  completeOAuthLink(accessToken, provider, code) {
617
- 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 });
618
801
  }
619
802
  /**
620
803
  * Remove a linked provider. Refuses with `OAUTH_UNLINK_WOULD_LOCK_OUT` (409)
621
804
  * if it would leave the account with no way to sign in.
622
805
  */
623
806
  unlinkOAuth(accessToken, provider) {
624
- 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 });
625
808
  }
626
809
  }
627
810
  function listQuery(page) {
@@ -642,47 +825,55 @@ class OrganizationsClient {
642
825
  }
643
826
  /** Create an organization; the calling user becomes the OWNER. */
644
827
  create(accessToken, input) {
645
- return this.client.request('POST', '/api/v1/users/me/organizations/', input, {
828
+ return this.client.send('POST', '/api/v1/users/me/organizations/', input, {
646
829
  'X-Rekey-User-Token': accessToken,
647
830
  });
648
831
  }
649
- /** List organizations the calling user belongs to, with their role. The
650
- * 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
+ */
651
838
  listMine(accessToken, page) {
652
- 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, {
653
840
  'X-Rekey-User-Token': accessToken,
654
841
  });
655
842
  }
656
843
  /** Fetch one organization the caller belongs to. */
657
844
  get(accessToken, organizationId) {
658
- 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 });
659
846
  }
660
847
  /** Update org name / metadata. OWNER + ADMIN only. */
661
848
  update(accessToken, organizationId, input) {
662
- 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 });
663
850
  }
664
- /** List members of an organization the caller belongs to. Paginated
665
- * (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
+ */
666
857
  listMembers(accessToken, organizationId, page) {
667
- 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 });
668
859
  }
669
860
  /**
670
861
  * Invite a user. Returns the raw token ONCE — surface via your own
671
862
  * email/share channel. OWNER + ADMIN only.
672
863
  */
673
864
  invite(accessToken, organizationId, input) {
674
- 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 });
675
866
  }
676
867
  /** Revoke a pending invitation. OWNER + ADMIN only. Idempotent. */
677
868
  revokeInvitation(accessToken, organizationId, invitationId) {
678
- 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 });
679
870
  }
680
871
  /**
681
872
  * Change a member's role. OWNER manages anyone; ADMIN manages MEMBER
682
873
  * only. Last-OWNER guard refuses demoting the only OWNER.
683
874
  */
684
875
  setMemberRole(accessToken, organizationId, targetEndUserId, input) {
685
- 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 });
686
877
  }
687
878
  /**
688
879
  * Remove a member (or self). Refuses removing the last OWNER.
@@ -692,7 +883,7 @@ class OrganizationsClient {
692
883
  * rather than assuming it is always `true`.
693
884
  */
694
885
  removeMember(accessToken, organizationId, targetEndUserId) {
695
- 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 });
696
887
  }
697
888
  /**
698
889
  * Self-leave. An OWNER cannot leave (payment + benefits are tied to the
@@ -700,14 +891,14 @@ class OrganizationsClient {
700
891
  * first, or demote yourself to ADMIN if there is another OWNER.
701
892
  */
702
893
  leave(accessToken, organizationId) {
703
- 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 });
704
895
  }
705
896
  /**
706
897
  * Accept an organization invitation by raw token. Refuses cross-
707
898
  * Application invitations. Idempotent if the caller is already a member.
708
899
  */
709
900
  acceptInvitation(accessToken, input) {
710
- 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 });
711
902
  }
712
903
  /**
713
904
  * Make `organizationId` the active org for this session (member-only).
@@ -718,14 +909,14 @@ class OrganizationsClient {
718
909
  * you switch again, clear it, or leave the org.
719
910
  */
720
911
  switch(accessToken, organizationId) {
721
- 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 });
722
913
  }
723
914
  /**
724
915
  * Clear the active org — switch the session back to the personal pool.
725
916
  * Returns a fresh token pair (no active org); **store both**.
726
917
  */
727
918
  clearActive(accessToken) {
728
- 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 });
729
920
  }
730
921
  }
731
922
  class LicensesClient {
@@ -754,7 +945,7 @@ class LicensesClient {
754
945
  * ```
755
946
  */
756
947
  verify(input) {
757
- return this.client.request('POST', '/api/v1/licenses/verify', input);
948
+ return this.client.send('POST', '/api/v1/licenses/verify', input);
758
949
  }
759
950
  }
760
951
  class UsageClient {
@@ -768,7 +959,7 @@ class UsageClient {
768
959
  * server time; pass an ISO string when ingesting historical events.
769
960
  */
770
961
  record(input) {
771
- return this.client.request('POST', '/api/v1/usage/record', input);
962
+ return this.client.send('POST', '/api/v1/usage/record', input);
772
963
  }
773
964
  /**
774
965
  * Sum recorded quantity for a meter, optionally bounded by a time window
@@ -786,7 +977,7 @@ class UsageClient {
786
977
  params.set('endUserId', input.endUserId);
787
978
  if (input.organizationId)
788
979
  params.set('organizationId', input.organizationId);
789
- return this.client.request('GET', `/api/v1/usage/aggregate?${params.toString()}`);
980
+ return this.client.send('GET', `/api/v1/usage/aggregate?${params.toString()}`);
790
981
  }
791
982
  }
792
983
  function creditSubjectQuery(subject) {
@@ -812,7 +1003,7 @@ class CreditsClient {
812
1003
  }
813
1004
  /** Current spendable balance for a subject (end-user or org); 0 if none. */
814
1005
  getBalance(subject) {
815
- return this.client.request('GET', `/api/v1/credits/balance?${creditSubjectQuery(subject)}`);
1006
+ return this.client.send('GET', `/api/v1/credits/balance?${creditSubjectQuery(subject)}`);
816
1007
  }
817
1008
  /**
818
1009
  * Deduct credits from a subject (end-user or org pool). Throws `RekeyError`
@@ -822,7 +1013,7 @@ class CreditsClient {
822
1013
  * double-charges — a repeat returns the original result with `applied: false`.
823
1014
  */
824
1015
  consume(input) {
825
- return this.client.request('POST', '/api/v1/credits/consume', input);
1016
+ return this.client.send('POST', '/api/v1/credits/consume', input);
826
1017
  }
827
1018
  /**
828
1019
  * Ledger entries for a subject, newest first. Pass `offset` to page back
@@ -835,7 +1026,7 @@ class CreditsClient {
835
1026
  params.set('limit', String(limit));
836
1027
  if (offset !== undefined)
837
1028
  params.set('offset', String(offset));
838
- return this.client.request('GET', `/api/v1/credits/ledger?${params.toString()}`);
1029
+ return this.client.send('GET', `/api/v1/credits/ledger?${params.toString()}`);
839
1030
  }
840
1031
  }
841
1032
  /**
@@ -858,7 +1049,23 @@ class CreditsClient {
858
1049
  * runtimes that can otherwise use the rest of the client.
859
1050
  */
860
1051
  function nodeCrypto() {
861
- return createRequire(import.meta.url)('node:crypto');
1052
+ // `process.getBuiltinModule` (Node 22.3+, and this package's floor is 22)
1053
+ // resolves a builtin synchronously with NO static import — which is the
1054
+ // whole point. The previous fix used `createRequire`, correct for CJS
1055
+ // interop but imported from 'node:module' at module scope, so merely
1056
+ // IMPORTING the package failed on edge runtimes with
1057
+ // `Failed to load external module node:module`. That defeated the laziness
1058
+ // this function's own comment says it exists to preserve: before, edge
1059
+ // consumers could import the client and use every fetch-based method, and
1060
+ // only calling signature verification would fail.
1061
+ const get = globalThis.process
1062
+ ?.getBuiltinModule;
1063
+ const mod = typeof get === 'function' ? get('node:crypto') : undefined;
1064
+ if (!mod) {
1065
+ throw new Error('Signature verification needs Node crypto, which is unavailable in this runtime. ' +
1066
+ 'Run verifyWebhookSignature / verifyAccessToken (RS256) on a Node server, not an edge runtime.');
1067
+ }
1068
+ return mod;
862
1069
  }
863
1070
  /**
864
1071
  * Verify the HMAC signature on an inbound webhook from Rekey. Returns
@@ -934,7 +1141,14 @@ async function loadJwks(options, forceRefetch) {
934
1141
  if (cached && !forceRefetch && Date.now() - cached.fetchedAt <= ttl)
935
1142
  return cached.jwks;
936
1143
  const fetchImpl = options.fetch ?? fetch;
937
- 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
+ }
938
1152
  if (!res.ok) {
939
1153
  throw new RekeyError({
940
1154
  code: 'JWKS_FETCH_FAILED',
@@ -943,7 +1157,20 @@ async function loadJwks(options, forceRefetch) {
943
1157
  statusCode: res.status,
944
1158
  });
945
1159
  }
946
- 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
+ }
947
1174
  if (!jwks || !Array.isArray(jwks.keys)) {
948
1175
  throw new RekeyError({
949
1176
  code: 'JWKS_FETCH_FAILED',
@@ -980,12 +1207,20 @@ async function loadJwks(options, forceRefetch) {
980
1207
  * import { verifyAccessToken } from '@rekey.dev/node';
981
1208
  *
982
1209
  * const claims = await verifyAccessToken(req.headers['x-rekey-user-token'], {
1210
+ * applicationId: MY_APP_ID,
983
1211
  * jwksUrl: 'https://rekey.example.com/.well-known/jwks.json',
984
1212
  * });
985
- * if (claims.applicationId !== MY_APP_ID) throw new Error('wrong app');
986
1213
  * req.userId = claims.sub;
987
1214
  * ```
988
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
+ *
989
1224
  * @throws {RekeyError} `TOKEN_ALG_NOT_RS256` — token is HS256 (app hasn't opted in) or another alg.
990
1225
  * @throws {RekeyError} `TOKEN_KID_UNKNOWN` — `kid` not in the JWKS (forged, or key deleted).
991
1226
  * @throws {RekeyError} `USER_TOKEN_EXPIRED` — `exp` passed; refresh the session.
@@ -1061,6 +1296,16 @@ export async function verifyAccessToken(token, options) {
1061
1296
  if (typeof payload.sub !== 'string' || typeof payload.applicationId !== 'string') {
1062
1297
  throw invalid('Token is missing the sub/applicationId claims.');
1063
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
+ }
1064
1309
  const nowSec = Math.floor((options.now ? options.now() : Date.now()) / 1000);
1065
1310
  if (typeof payload.exp !== 'number' || payload.exp <= nowSec) {
1066
1311
  throw new RekeyError({
@@ -1085,17 +1330,35 @@ class BillingClient {
1085
1330
  * `amount` is in the smallest currency unit (cents/paise/sen) — never
1086
1331
  * a float. Format on display: `${amount / 100} ${currency}`.
1087
1332
  */
1088
- getPlans() {
1089
- return this.client.request('GET', '/api/v1/billing/plans');
1333
+ getPlans(page) {
1334
+ return this.client.send('GET', `/api/v1/billing/plans${listQuery(page)}`);
1090
1335
  }
1091
1336
  /**
1092
1337
  * Fetch the current end-user's active subscription, or `null` if they
1093
1338
  * have none. Returns the most recent ACTIVE / PENDING / PAST_DUE row.
1094
1339
  *
1095
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.
1096
1352
  */
1097
- getSubscription(accessToken) {
1098
- 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, {
1099
1362
  'X-Rekey-User-Token': accessToken,
1100
1363
  });
1101
1364
  }
@@ -1124,7 +1387,7 @@ class BillingClient {
1124
1387
  * ```
1125
1388
  */
1126
1389
  createCheckout(accessToken, input) {
1127
- return this.client.request('POST', '/api/v1/billing/checkout', input, {
1390
+ return this.client.send('POST', '/api/v1/billing/checkout', input, {
1128
1391
  'X-Rekey-User-Token': accessToken,
1129
1392
  });
1130
1393
  }
@@ -1138,7 +1401,7 @@ class BillingClient {
1138
1401
  * `COUPON_USER_LIMIT_REACHED`. Surface the message + fix to the user.
1139
1402
  */
1140
1403
  validateCoupon(accessToken, input) {
1141
- return this.client.request('POST', '/api/v1/billing/coupons/validate', input, {
1404
+ return this.client.send('POST', '/api/v1/billing/coupons/validate', input, {
1142
1405
  'X-Rekey-User-Token': accessToken,
1143
1406
  });
1144
1407
  }
@@ -1156,7 +1419,7 @@ class BillingClient {
1156
1419
  const headers = {};
1157
1420
  if (country)
1158
1421
  headers['x-country'] = country.toUpperCase();
1159
- return this.client.request('GET', '/api/v1/billing/providers', undefined, headers);
1422
+ return this.client.send('GET', '/api/v1/billing/providers', undefined, headers);
1160
1423
  }
1161
1424
  /**
1162
1425
  * Resolve the calling end-user's current entitlements — feature flags +
@@ -1175,7 +1438,7 @@ class BillingClient {
1175
1438
  const qs = opts?.organizationId
1176
1439
  ? `?organizationId=${encodeURIComponent(opts.organizationId)}`
1177
1440
  : '';
1178
- return this.client.request('GET', `/api/v1/billing/entitlements${qs}`, undefined, {
1441
+ return this.client.send('GET', `/api/v1/billing/entitlements${qs}`, undefined, {
1179
1442
  'X-Rekey-User-Token': accessToken,
1180
1443
  });
1181
1444
  }
@@ -1204,7 +1467,7 @@ class BillingClient {
1204
1467
  * ```
1205
1468
  */
1206
1469
  cancelSubscription(accessToken, input) {
1207
- return this.client.request('POST', '/api/v1/billing/subscription/cancel', {
1470
+ return this.client.send('POST', '/api/v1/billing/subscription/cancel', {
1208
1471
  // Omitted rather than sent as undefined so the API applies its own
1209
1472
  // default (at period end) instead of parsing a null-ish field.
1210
1473
  ...(input?.atPeriodEnd !== undefined && { atPeriodEnd: input.atPeriodEnd }),