@rekey.dev/node 1.1.2

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 ADDED
@@ -0,0 +1,1073 @@
1
+ /**
2
+ * @rekey.dev/node — server SDK for Rekey.
3
+ *
4
+ * One client instance per Application. Construct with the Application's
5
+ * secret key (`rp_live_…` or `rp_test_…`) and the URL of your Rekey
6
+ * deployment. Never ship the secret key to the browser — for browser code
7
+ * use `@rekey.dev/react` with the Application's public key instead.
8
+ *
9
+ * @example Smoke-test your credentials
10
+ * ```ts
11
+ * import { Rekey } from "@rekey.dev/node";
12
+ *
13
+ * const rekey = new Rekey({
14
+ * apiUrl: process.env.RELIPAY_URL!,
15
+ * secretKey: process.env.RELIPAY_SECRET!,
16
+ * });
17
+ *
18
+ * const me = await rekey.applications.me();
19
+ * console.log(`Connected to "${me.name}" (${me.slug})`);
20
+ * ```
21
+ */
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
+ // RekeyError is the shared class (imported above) — re-exported so the public
26
+ // API name is preserved and `instanceof` is consistent with @rekey.dev/react.
27
+ export { RekeyError };
28
+ /**
29
+ * Outbound webhook event registry — the events Rekey can POST to your app
30
+ * (verify them with `verifyWebhookSignature` below). `WEBHOOK_EVENTS` carries
31
+ * `{ name, description }` pairs for introspection/autocomplete;
32
+ * `KNOWN_WEBHOOK_EVENTS` is just the names. Mirrors the API's registry exactly.
33
+ *
34
+ * @example
35
+ * ```ts
36
+ * import { WEBHOOK_EVENTS, isKnownWebhookEvent, type WebhookEventEnvelope } from '@rekey.dev/node';
37
+ *
38
+ * for (const e of WEBHOOK_EVENTS) console.log(`${e.name} — ${e.description}`);
39
+ *
40
+ * const event = req.body as WebhookEventEnvelope; // after verifyWebhookSignature(...)
41
+ * if (event.type === 'subscription.activated') unlockPlan(event.data);
42
+ * ```
43
+ */
44
+ export { WEBHOOK_EVENTS, KNOWN_WEBHOOK_EVENTS, isKnownWebhookEvent } from '@rekey.dev/shared-types';
45
+ /**
46
+ * Top-level Rekey client. Auth and billing live as namespaces
47
+ * (`rekey.applications`, `rekey.auth`, `rekey.billing`) so an agent
48
+ * reading `rekey.` in an editor sees a discoverable surface.
49
+ */
50
+ export class Rekey {
51
+ apiUrl;
52
+ secretKey;
53
+ fetchImpl;
54
+ /** Operations on the calling Application itself. */
55
+ applications;
56
+ /** Auth operations — sign-in, sign-up, sessions, passkeys, magic-link. */
57
+ auth;
58
+ /** Billing operations — plans, checkout, subscriptions, coupons. */
59
+ billing;
60
+ /** End-user organizations — create, invite, members, role changes. */
61
+ organizations;
62
+ /** License key verification + activation. */
63
+ licenses;
64
+ /** Usage metering — record events, aggregate windows. */
65
+ usage;
66
+ /** Prepaid credits — balance reads, idempotent drawdown, ledger. */
67
+ credits;
68
+ /** MCP — validate Rekey-issued MCP tokens from your own MCP server. */
69
+ mcp;
70
+ constructor(config) {
71
+ if (!config.apiUrl) {
72
+ throw new RekeyError({
73
+ code: 'CONFIG_MISSING_API_URL',
74
+ message: 'Rekey client requires `apiUrl`.',
75
+ fix: 'Pass `apiUrl: process.env.RELIPAY_URL` when constructing the client.',
76
+ });
77
+ }
78
+ if (!config.secretKey || !config.secretKey.startsWith('rp_')) {
79
+ throw new RekeyError({
80
+ code: 'CONFIG_INVALID_SECRET_KEY',
81
+ message: 'Rekey client requires a valid `secretKey` (starts with `rp_`).',
82
+ fix: 'Get a key from the Rekey panel under Application → API Keys, then pass it as `secretKey`.',
83
+ });
84
+ }
85
+ this.apiUrl = config.apiUrl.replace(/\/$/, '');
86
+ this.secretKey = config.secretKey;
87
+ this.fetchImpl = config.fetch ?? fetch;
88
+ this.applications = new ApplicationsClient(this);
89
+ this.auth = new AuthClient(this);
90
+ this.billing = new BillingClient(this);
91
+ this.organizations = new OrganizationsClient(this);
92
+ this.licenses = new LicensesClient(this);
93
+ this.usage = new UsageClient(this);
94
+ this.credits = new CreditsClient(this);
95
+ this.mcp = new McpClient(this);
96
+ }
97
+ /** @internal */
98
+ async request(method, path, body, extraHeaders) {
99
+ const res = await this.fetchImpl(`${this.apiUrl}${path}`, {
100
+ method,
101
+ headers: {
102
+ Authorization: `Bearer ${this.secretKey}`,
103
+ ...(body !== undefined ? { 'Content-Type': 'application/json' } : {}),
104
+ ...extraHeaders,
105
+ },
106
+ ...(body !== undefined ? { body: JSON.stringify(body) } : {}),
107
+ });
108
+ const json = (await res.json().catch(() => ({})));
109
+ if (!res.ok || ('success' in json && json.success === false)) {
110
+ const requestId = res.headers.get('x-request-id') ?? undefined;
111
+ const err = 'error' in json
112
+ ? json.error
113
+ : {
114
+ code: 'UNKNOWN_ERROR',
115
+ message: `Request failed with status ${res.status}.`,
116
+ fix: 'Check the Rekey API logs for the matching request id.',
117
+ };
118
+ const resolvedRequestId = ('requestId' in err && err.requestId) || requestId;
119
+ throw new RekeyError({
120
+ ...err,
121
+ statusCode: res.status,
122
+ ...(resolvedRequestId !== undefined && { requestId: resolvedRequestId }),
123
+ });
124
+ }
125
+ return json.data;
126
+ }
127
+ /**
128
+ * @internal Raw request for the non-enveloped OAuth/MCP endpoints — returns
129
+ * the parsed JSON as-is (those endpoints emit standard OAuth shapes, not the
130
+ * `{ success, data }` envelope). Throws `RekeyError` on non-2xx, mapping
131
+ * the OAuth `{ error, error_description }` body when present.
132
+ */
133
+ async requestRaw(method, path, body, auth = true) {
134
+ const res = await this.fetchImpl(`${this.apiUrl}${path}`, {
135
+ method,
136
+ headers: {
137
+ ...(auth ? { Authorization: `Bearer ${this.secretKey}` } : {}),
138
+ ...(body !== undefined ? { 'Content-Type': 'application/json' } : {}),
139
+ },
140
+ ...(body !== undefined ? { body: JSON.stringify(body) } : {}),
141
+ });
142
+ const json = (await res.json().catch(() => ({})));
143
+ if (!res.ok) {
144
+ const code = typeof json.error === 'string' ? json.error : `HTTP_${res.status}`;
145
+ const message = typeof json.error_description === 'string'
146
+ ? json.error_description
147
+ : `Request failed with status ${res.status}.`;
148
+ throw new RekeyError({ code, message, statusCode: res.status });
149
+ }
150
+ return json;
151
+ }
152
+ }
153
+ /**
154
+ * MCP helpers for customers running their OWN MCP server behind Rekey auth.
155
+ * The hosted MCP server (account tools) is consumed by MCP clients directly —
156
+ * this client is for the "bring your own MCP server" path: validate incoming
157
+ * Rekey-issued tokens, and read the OAuth metadata.
158
+ */
159
+ class McpClient {
160
+ client;
161
+ slugCache = null;
162
+ constructor(client) {
163
+ this.client = client;
164
+ }
165
+ async slug() {
166
+ if (this.slugCache)
167
+ return this.slugCache;
168
+ const app = await this.client.request('GET', '/api/v1/me/');
169
+ this.slugCache = app.slug;
170
+ return app.slug;
171
+ }
172
+ /**
173
+ * Validate an MCP access token (RFC 7662 introspection). Call this from your
174
+ * own MCP server to authorize an incoming request. Authenticated with this
175
+ * client's secret key.
176
+ *
177
+ * @example
178
+ * ```ts
179
+ * const result = await rekey.mcp.introspect(bearerToken);
180
+ * if (!result.active) throw new Error('unauthorized');
181
+ * const endUserId = result.sub;
182
+ * ```
183
+ */
184
+ introspect(token) {
185
+ return this.slug().then((slug) => this.client.requestRaw('POST', `/api/v1/mcp/${slug}/oauth/introspect`, { token }));
186
+ }
187
+ /** Fetch this application's OAuth authorization-server metadata (RFC 8414). */
188
+ metadata() {
189
+ return this.slug().then((slug) => this.client.requestRaw('GET', `/api/v1/mcp/${slug}/.well-known/oauth-authorization-server`, undefined, false));
190
+ }
191
+ }
192
+ class ApplicationsClient {
193
+ client;
194
+ constructor(client) {
195
+ this.client = client;
196
+ }
197
+ /**
198
+ * Verify credentials and fetch the calling Application. Use this as your
199
+ * SDK smoke test — if it returns, your secret key is good and you're
200
+ * pointed at the right Rekey deployment.
201
+ *
202
+ * @example
203
+ * ```ts
204
+ * const me = await rekey.applications.me();
205
+ * console.log(`Connected to "${me.name}" (${me.slug})`);
206
+ * ```
207
+ *
208
+ * @throws {RekeyError} with `code: "API_KEY_INVALID"` if the key is wrong/revoked/expired.
209
+ */
210
+ me() {
211
+ return this.client.request('GET', '/api/v1/me/');
212
+ }
213
+ }
214
+ class AuthClient {
215
+ client;
216
+ constructor(client) {
217
+ this.client = client;
218
+ }
219
+ /**
220
+ * Create a new end-user in the calling Application via email + password.
221
+ * Returns the user and a JWT to use for subsequent per-user calls
222
+ * (e.g. `getCurrentUser(token)`).
223
+ *
224
+ * @example
225
+ * ```ts
226
+ * const { endUser, token } = await rekey.auth.signUp({
227
+ * email: 'alice@example.com',
228
+ * password: 'correct-horse-battery-staple',
229
+ * });
230
+ * // store token in your session, return it to the browser, etc.
231
+ * ```
232
+ *
233
+ * @throws {RekeyError} `EMAIL_ALREADY_EXISTS` (409) if the email is taken in this Application.
234
+ * @throws {RekeyError} `PASSWORD_TOO_SHORT` (400) if shorter than the Application's `passwordMinLength`.
235
+ * @throws {RekeyError} `AUTH_METHOD_DISABLED` (400) if the Application doesn't have `"password"` enabled.
236
+ */
237
+ signUp(input) {
238
+ return this.client.request('POST', '/api/v1/auth/sign-up', input);
239
+ }
240
+ /**
241
+ * Authenticate an existing end-user with email + password.
242
+ *
243
+ * Returns a discriminated union over `mfaRequired`:
244
+ * - `mfaRequired === false` → full `AuthResultDto` with access+refresh.
245
+ * - `mfaRequired === true` → `mfaChallengeToken` (5-minute lifetime).
246
+ * Prompt the user for their TOTP / backup code and call
247
+ * `mfaVerify({ mfaChallengeToken, code })` to receive a real session.
248
+ *
249
+ * **Branch on `result.mfaRequired` before reading `accessToken`** — the
250
+ * MFA-required branch has no session tokens.
251
+ *
252
+ * @throws {RekeyError} `INVALID_CREDENTIALS` (401) — single code on purpose.
253
+ * Don't try to distinguish wrong-email from wrong-password from the SDK side either.
254
+ */
255
+ signIn(input) {
256
+ return this.client.request('POST', '/api/v1/auth/sign-in', input);
257
+ }
258
+ /**
259
+ * Exchange an MFA challenge token + TOTP/backup code for a real session.
260
+ * Use after `signIn` (or OAuth callback) returns `mfaRequired: true`.
261
+ *
262
+ * @throws {RekeyError} `MFA_CHALLENGE_INVALID` (401) if the token is
263
+ * forged, expired, or signed with a different secret.
264
+ * @throws {RekeyError} `MFA_CHALLENGE_WRONG_APPLICATION` (401) if the
265
+ * token was issued under a different Application.
266
+ * @throws {RekeyError} `MFA_CODE_INVALID` (401) if the code doesn't
267
+ * verify against the user's TOTP secret or remaining backup codes.
268
+ */
269
+ mfaVerify(input) {
270
+ return this.client.request('POST', '/api/v1/auth/mfa-verify', input);
271
+ }
272
+ /**
273
+ * Request a magic-link sign-in email. Enumeration-safe: same response
274
+ * shape whether the email exists or not. When the Application has
275
+ * email transport configured, the link is sent and `magicLinkToken`
276
+ * is null; otherwise the raw token is returned for you to forward.
277
+ */
278
+ requestMagicLink(input) {
279
+ return this.client.request('POST', '/api/v1/auth/magic-link/request', input);
280
+ }
281
+ /**
282
+ * Consume a magic-link token. Returns `SignInOutcome` — branch on
283
+ * `mfaRequired` before reading `accessToken`. For MFA-enrolled users
284
+ * the response carries `mfaChallengeToken` and you must complete via
285
+ * `mfaVerify(...)`.
286
+ */
287
+ verifyMagicLink(input) {
288
+ return this.client.request('POST', '/api/v1/auth/magic-link/verify', input);
289
+ }
290
+ /**
291
+ * Begin a passkey authentication ceremony. Returns the WebAuthn options
292
+ * to forward to the browser (`navigator.credentials.get(...)`) along
293
+ * with `expectedChallenge` — bind the challenge to your session and
294
+ * pass both back via `verifyPasskeyAuthentication(...)`.
295
+ */
296
+ startPasskeyAuthentication(input) {
297
+ return this.client.request('POST', '/api/v1/auth/passkey/authenticate/start', input ?? {});
298
+ }
299
+ /**
300
+ * Complete a passkey authentication. Returns the same `SignInOutcome`
301
+ * shape as `signIn` — but passkeys are themselves a strong factor, so
302
+ * `mfaRequired` will always be `false` in practice.
303
+ */
304
+ verifyPasskeyAuthentication(input) {
305
+ return this.client.request('POST', '/api/v1/auth/passkey/authenticate/complete', input);
306
+ }
307
+ /**
308
+ * Begin a passkey registration ceremony for an authenticated user.
309
+ * Forward `options` to `navigator.credentials.create(...)`; store
310
+ * `expectedChallenge` in session; POST both back via
311
+ * `verifyPasskeyRegistration(...)`.
312
+ */
313
+ startPasskeyRegistration(accessToken) {
314
+ return this.client.request('POST', '/api/v1/auth/passkey/register/start', undefined, {
315
+ 'X-Rekey-User-Token': accessToken,
316
+ });
317
+ }
318
+ verifyPasskeyRegistration(accessToken, input) {
319
+ return this.client.request('POST', '/api/v1/auth/passkey/register/complete', input, {
320
+ 'X-Rekey-User-Token': accessToken,
321
+ });
322
+ }
323
+ /** List the user's registered passkeys. */
324
+ listPasskeys(accessToken) {
325
+ return this.client.request('GET', '/api/v1/auth/passkeys', undefined, {
326
+ 'X-Rekey-User-Token': accessToken,
327
+ });
328
+ }
329
+ /** Remove a passkey. Returns `{deleted: false}` if the row doesn't belong to this user. */
330
+ deletePasskey(accessToken, credentialRowId) {
331
+ return this.client.request('DELETE', `/api/v1/auth/passkeys/${encodeURIComponent(credentialRowId)}`, undefined, { 'X-Rekey-User-Token': accessToken });
332
+ }
333
+ // End-user organization / team methods live on `rekey.organizations.*`
334
+ // (OrganizationsClient) — the canonical, fuller surface. The earlier
335
+ // duplicates here (createOrganization / listMyOrganizations /
336
+ // inviteToOrganization / acceptOrganizationInvitation) were removed to
337
+ // avoid two divergent copies of the same endpoints.
338
+ /**
339
+ * Resolve the end-user behind a presented access token.
340
+ *
341
+ * @throws {RekeyError} `USER_TOKEN_INVALID` (401) if expired/forged/wrong-secret.
342
+ * @throws {RekeyError} `USER_TOKEN_WRONG_APPLICATION` (401) if the token was issued
343
+ * by a different Application than the calling secret key represents.
344
+ */
345
+ getCurrentUser(accessToken) {
346
+ return this.client.request('GET', '/api/v1/users/me/', undefined, {
347
+ 'X-Rekey-User-Token': accessToken,
348
+ });
349
+ }
350
+ /**
351
+ * Exchange a refresh token for a fresh {access, refresh} pair. The presented
352
+ * refresh is revoked atomically — call this **once** and store the new
353
+ * `refreshToken` from the response immediately.
354
+ *
355
+ * @throws {RekeyError} `REFRESH_TOKEN_REUSED` (401) if you replay an already-used token.
356
+ * This is a strong signal the original was leaked; treat as compromise.
357
+ * @throws {RekeyError} `REFRESH_TOKEN_EXPIRED` (401) after the 30-day refresh window.
358
+ */
359
+ refresh(refreshToken) {
360
+ // /auth/refresh returns the same shape as /auth/mfa-verify — always a
361
+ // full session (refresh requires a prior MFA-verified session by
362
+ // definition).
363
+ return this.client.request('POST', '/api/v1/auth/refresh', { refreshToken });
364
+ }
365
+ /**
366
+ * Revoke a refresh token. Idempotent — no-op for unknown tokens. The
367
+ * access token paired with this refresh remains valid until its short
368
+ * (15 min) expiry; for true "log out everywhere" semantics, also clear
369
+ * the access token from your client.
370
+ */
371
+ signOut(refreshToken) {
372
+ return this.client.request('POST', '/api/v1/auth/sign-out', { refreshToken });
373
+ }
374
+ /**
375
+ * Request a password-reset token for an email. Always succeeds — never
376
+ * tells you whether the email exists. **You must email the returned
377
+ * `resetToken` to the user**: Rekey does not send email.
378
+ *
379
+ * @example
380
+ * ```ts
381
+ * const { resetToken } = await rekey.auth.requestPasswordReset({ email });
382
+ * if (resetToken) await sendgrid.send({ to: email, subject: 'Reset', text: `link: ${url(resetToken)}` });
383
+ * ```
384
+ */
385
+ requestPasswordReset(input) {
386
+ return this.client.request('POST', '/api/v1/auth/forgot-password', input);
387
+ }
388
+ /**
389
+ * Consume a reset token + set a new password. Single-use. On success,
390
+ * every refresh token for the user is revoked.
391
+ *
392
+ * @throws {RekeyError} `PASSWORD_RESET_TOKEN_INVALID` / `_USED` / `_EXPIRED` / `_WRONG_APPLICATION`
393
+ * @throws {RekeyError} `PASSWORD_TOO_SHORT` if below the Application's `passwordMinLength`
394
+ */
395
+ resetPassword(input) {
396
+ return this.client.request('POST', '/api/v1/auth/reset-password', input);
397
+ }
398
+ /**
399
+ * Authenticated password change. Pass the user's *current* access token.
400
+ * On success, every refresh token for the user is revoked — other devices
401
+ * are signed out.
402
+ */
403
+ changePassword(accessToken, input) {
404
+ return this.client.request('POST', '/api/v1/auth/change-password', input, {
405
+ 'X-Rekey-User-Token': accessToken,
406
+ });
407
+ }
408
+ /**
409
+ * Revoke every refresh token for the calling user. "Sign out of all
410
+ * devices." The caller's access token remains valid until 15-min expiry
411
+ * — clear it client-side for full logout.
412
+ */
413
+ signOutEverywhere(accessToken) {
414
+ return this.client.request('POST', '/api/v1/auth/sign-out-everywhere', undefined, { 'X-Rekey-User-Token': accessToken });
415
+ }
416
+ /**
417
+ * Send (or re-send) an email-verification link to the current user.
418
+ * If email transport is configured on the Application, Rekey sends
419
+ * the email and `verificationToken` is null. Otherwise the raw token
420
+ * is returned for the caller to forward via their own provider.
421
+ *
422
+ * Pass `verifyUrl` containing `{token}` to template the link target
423
+ * (e.g. `https://app.example.com/verify?t={token}`).
424
+ */
425
+ sendVerificationEmail(accessToken, input) {
426
+ return this.client.request('POST', '/api/v1/auth/send-verification', input ?? {}, {
427
+ 'X-Rekey-User-Token': accessToken,
428
+ });
429
+ }
430
+ /**
431
+ * Consume an email-verification token. Single-use, 24-hour lifetime.
432
+ * Marks `emailVerified: true` on the user record. Cross-Application
433
+ * tokens are refused with `EMAIL_VERIFICATION_TOKEN_WRONG_APPLICATION`.
434
+ */
435
+ verifyEmail(input) {
436
+ return this.client.request('POST', '/api/v1/auth/verify-email', input);
437
+ }
438
+ // ---------- Active sessions ----------
439
+ /**
440
+ * List the current user's active sessions (live refresh tokens), newest
441
+ * first. Each carries the User-Agent + IP captured at issue time and an
442
+ * `id` you can pass to `revokeSession(...)`.
443
+ */
444
+ listSessions(accessToken) {
445
+ return this.client.request('GET', '/api/v1/auth/sessions', undefined, {
446
+ 'X-Rekey-User-Token': accessToken,
447
+ });
448
+ }
449
+ /** Revoke one session by id. Idempotent — `{ revoked: false }` if it isn't this user's. */
450
+ revokeSession(accessToken, sessionId) {
451
+ return this.client.request('DELETE', `/api/v1/auth/sessions/${encodeURIComponent(sessionId)}`, undefined, { 'X-Rekey-User-Token': accessToken });
452
+ }
453
+ // ---------- MFA enrollment / management ----------
454
+ //
455
+ // The login-step verification is `mfaVerify(...)` above. These manage the
456
+ // user's own TOTP enrollment + step-up challenges. Gated by the
457
+ // Application's `authConfig.mfa` policy — calls return `MFA_NOT_ENABLED`
458
+ // (403) when the policy is "off".
459
+ /** MFA enrollment status for the current user, plus the Application's policy. */
460
+ mfaStatus(accessToken) {
461
+ return this.client.request('GET', '/api/v1/auth/mfa/status', undefined, {
462
+ 'X-Rekey-User-Token': accessToken,
463
+ });
464
+ }
465
+ /**
466
+ * Begin TOTP enrollment: mints a secret (as an `otpauthUrl` for the QR) and
467
+ * 10 single-show backup codes. **Not enrolled until `confirmMfaSetup(...)`.**
468
+ * Only SHA-256 hashes of the backup codes are stored — show them once.
469
+ */
470
+ mfaSetup(accessToken) {
471
+ return this.client.request('POST', '/api/v1/auth/mfa/setup', undefined, {
472
+ 'X-Rekey-User-Token': accessToken,
473
+ });
474
+ }
475
+ /** Confirm enrollment by submitting the current 6-digit TOTP code. */
476
+ confirmMfaSetup(accessToken, code) {
477
+ return this.client.request('POST', '/api/v1/auth/mfa/setup-confirm', { code }, {
478
+ 'X-Rekey-User-Token': accessToken,
479
+ });
480
+ }
481
+ /**
482
+ * Verify a TOTP or backup code as a step-up check (does NOT issue a session).
483
+ * Backup codes are single-use — consumed on success. Returns `{ ok }`.
484
+ */
485
+ mfaChallenge(accessToken, code) {
486
+ return this.client.request('POST', '/api/v1/auth/mfa/challenge', { code }, {
487
+ 'X-Rekey-User-Token': accessToken,
488
+ });
489
+ }
490
+ /** Disable MFA for the current user. */
491
+ disableMfa(accessToken) {
492
+ return this.client.request('POST', '/api/v1/auth/mfa/disable', undefined, {
493
+ 'X-Rekey-User-Token': accessToken,
494
+ });
495
+ }
496
+ // ---------- OAuth (social sign-in + account linking) ----------
497
+ //
498
+ // Sign-in flow (no user token): `startOAuth` → redirect the browser → your
499
+ // server receives `code` → `completeOAuth` returns a SignInOutcome.
500
+ // Linking flow (authenticated): `startOAuthLink` → `completeOAuthLink`.
501
+ /**
502
+ * Get the provider authorization URL to redirect the browser to. Pass an
503
+ * unguessable `state` and verify it on return before calling `completeOAuth`.
504
+ */
505
+ startOAuth(provider, state) {
506
+ return this.client.request('POST', `/api/v1/auth/oauth/${encodeURIComponent(provider)}/start`, { state });
507
+ }
508
+ /**
509
+ * Exchange the provider `code` for a Rekey session. Returns a
510
+ * `SignInOutcome` — branch on `mfaRequired` before reading `accessToken`.
511
+ * Verify the `state` CSRF value yourself before calling.
512
+ */
513
+ completeOAuth(provider, code) {
514
+ return this.client.request('POST', `/api/v1/auth/oauth/${encodeURIComponent(provider)}/callback`, { code });
515
+ }
516
+ /** List the OAuth providers linked to the current user. */
517
+ listOAuthIdentities(accessToken) {
518
+ return this.client.request('GET', '/api/v1/auth/oauth/identities', undefined, {
519
+ 'X-Rekey-User-Token': accessToken,
520
+ });
521
+ }
522
+ /** Begin linking a provider to the *currently authenticated* user. */
523
+ startOAuthLink(accessToken, provider, state) {
524
+ return this.client.request('POST', `/api/v1/auth/oauth/${encodeURIComponent(provider)}/link/start`, { state }, { 'X-Rekey-User-Token': accessToken });
525
+ }
526
+ /**
527
+ * Complete an OAuth link — attaches the provider identity to the current
528
+ * user. Refuses on unverified provider emails (account-takeover guard) or
529
+ * when the provider account already belongs to a different user.
530
+ */
531
+ completeOAuthLink(accessToken, provider, code) {
532
+ return this.client.request('POST', `/api/v1/auth/oauth/${encodeURIComponent(provider)}/link/complete`, { code }, { 'X-Rekey-User-Token': accessToken });
533
+ }
534
+ /**
535
+ * Remove a linked provider. Refuses with `OAUTH_UNLINK_WOULD_LOCK_OUT` (409)
536
+ * if it would leave the account with no way to sign in.
537
+ */
538
+ unlinkOAuth(accessToken, provider) {
539
+ return this.client.request('DELETE', `/api/v1/auth/oauth/${encodeURIComponent(provider)}`, undefined, { 'X-Rekey-User-Token': accessToken });
540
+ }
541
+ }
542
+ function listQuery(page) {
543
+ if (!page)
544
+ return '';
545
+ const p = new URLSearchParams();
546
+ if (page.limit !== undefined)
547
+ p.set('limit', String(page.limit));
548
+ if (page.offset !== undefined)
549
+ p.set('offset', String(page.offset));
550
+ const s = p.toString();
551
+ return s ? `?${s}` : '';
552
+ }
553
+ class OrganizationsClient {
554
+ client;
555
+ constructor(client) {
556
+ this.client = client;
557
+ }
558
+ /** Create an organization; the calling user becomes the OWNER. */
559
+ create(accessToken, input) {
560
+ return this.client.request('POST', '/api/v1/users/me/organizations/', input, {
561
+ 'X-Rekey-User-Token': accessToken,
562
+ });
563
+ }
564
+ /** List organizations the calling user belongs to, with their role. The
565
+ * result is paginated (default 50, max 100); pass `page.offset` for more. */
566
+ listMine(accessToken, page) {
567
+ return this.client.request('GET', `/api/v1/users/me/organizations/${listQuery(page)}`, undefined, {
568
+ 'X-Rekey-User-Token': accessToken,
569
+ });
570
+ }
571
+ /** Fetch one organization the caller belongs to. */
572
+ get(accessToken, organizationId) {
573
+ return this.client.request('GET', `/api/v1/users/me/organizations/${encodeURIComponent(organizationId)}`, undefined, { 'X-Rekey-User-Token': accessToken });
574
+ }
575
+ /** Update org name / metadata. OWNER + ADMIN only. */
576
+ update(accessToken, organizationId, input) {
577
+ return this.client.request('PATCH', `/api/v1/users/me/organizations/${encodeURIComponent(organizationId)}`, input, { 'X-Rekey-User-Token': accessToken });
578
+ }
579
+ /** List members of an organization the caller belongs to. Paginated
580
+ * (default 50, max 100); pass `page.offset` to page beyond the first window. */
581
+ listMembers(accessToken, organizationId, page) {
582
+ return this.client.request('GET', `/api/v1/users/me/organizations/${encodeURIComponent(organizationId)}/members${listQuery(page)}`, undefined, { 'X-Rekey-User-Token': accessToken });
583
+ }
584
+ /**
585
+ * Invite a user. Returns the raw token ONCE — surface via your own
586
+ * email/share channel. OWNER + ADMIN only.
587
+ */
588
+ invite(accessToken, organizationId, input) {
589
+ return this.client.request('POST', `/api/v1/users/me/organizations/${encodeURIComponent(organizationId)}/invitations`, input, { 'X-Rekey-User-Token': accessToken });
590
+ }
591
+ /** Revoke a pending invitation. OWNER + ADMIN only. Idempotent. */
592
+ revokeInvitation(accessToken, organizationId, invitationId) {
593
+ return this.client.request('POST', `/api/v1/users/me/organizations/${encodeURIComponent(organizationId)}/invitations/${encodeURIComponent(invitationId)}/revoke`, undefined, { 'X-Rekey-User-Token': accessToken });
594
+ }
595
+ /**
596
+ * Change a member's role. OWNER manages anyone; ADMIN manages MEMBER
597
+ * only. Last-OWNER guard refuses demoting the only OWNER.
598
+ */
599
+ setMemberRole(accessToken, organizationId, targetEndUserId, input) {
600
+ return this.client.request('PATCH', `/api/v1/users/me/organizations/${encodeURIComponent(organizationId)}/members/${encodeURIComponent(targetEndUserId)}`, input, { 'X-Rekey-User-Token': accessToken });
601
+ }
602
+ /**
603
+ * Remove a member (or self). Refuses removing the last OWNER.
604
+ *
605
+ * Idempotent: `removed` is `false` when the target was not a member (e.g.
606
+ * already removed) — a no-op removal is not an error. Branch on `removed`
607
+ * rather than assuming it is always `true`.
608
+ */
609
+ removeMember(accessToken, organizationId, targetEndUserId) {
610
+ return this.client.request('DELETE', `/api/v1/users/me/organizations/${encodeURIComponent(organizationId)}/members/${encodeURIComponent(targetEndUserId)}`, undefined, { 'X-Rekey-User-Token': accessToken });
611
+ }
612
+ /**
613
+ * Self-leave. An OWNER cannot leave (payment + benefits are tied to the
614
+ * owner — `ORGANIZATION_OWNER_CANNOT_LEAVE`); transfer ownership via support
615
+ * first, or demote yourself to ADMIN if there is another OWNER.
616
+ */
617
+ leave(accessToken, organizationId) {
618
+ return this.client.request('POST', `/api/v1/users/me/organizations/${encodeURIComponent(organizationId)}/leave`, undefined, { 'X-Rekey-User-Token': accessToken });
619
+ }
620
+ /**
621
+ * Accept an organization invitation by raw token. Refuses cross-
622
+ * Application invitations. Idempotent if the caller is already a member.
623
+ */
624
+ acceptInvitation(accessToken, input) {
625
+ return this.client.request('POST', '/api/v1/auth/organizations/accept-invitation', input, { 'X-Rekey-User-Token': accessToken });
626
+ }
627
+ /**
628
+ * Make `organizationId` the active org for this session (member-only).
629
+ * Returns a fresh {accessToken, refreshToken} pair carrying the active org —
630
+ * **store both**. Subsequent entitlement reads (`billing.getEntitlements`)
631
+ * then default to this org's view + shared pool without passing
632
+ * `organizationId` explicitly. The active org survives token refresh until
633
+ * you switch again, clear it, or leave the org.
634
+ */
635
+ switch(accessToken, organizationId) {
636
+ return this.client.request('POST', `/api/v1/users/me/organizations/${encodeURIComponent(organizationId)}/switch`, undefined, { 'X-Rekey-User-Token': accessToken });
637
+ }
638
+ /**
639
+ * Clear the active org — switch the session back to the personal pool.
640
+ * Returns a fresh token pair (no active org); **store both**.
641
+ */
642
+ clearActive(accessToken) {
643
+ return this.client.request('POST', '/api/v1/users/me/organizations/clear-active-organization', undefined, { 'X-Rekey-User-Token': accessToken });
644
+ }
645
+ }
646
+ class LicensesClient {
647
+ client;
648
+ constructor(client) {
649
+ this.client = client;
650
+ }
651
+ /**
652
+ * Verify a license key + record an activation for this machine. Call
653
+ * once at app startup; you'll get a deterministic body (`ok=false` for
654
+ * invalid licenses — never an HTTP error — so your software can loop
655
+ * on the result without try/catch noise).
656
+ *
657
+ * `machineFingerprint` should be a stable identifier you derive client-
658
+ * side (hostname + OS + mac address, hashed). The same fingerprint
659
+ * across re-verifications does NOT consume a new seat.
660
+ *
661
+ * @example
662
+ * ```ts
663
+ * const result = await rekey.licenses.verify({
664
+ * key,
665
+ * machineFingerprint,
666
+ * label: 'Adam\'s MacBook',
667
+ * });
668
+ * if (!result.ok) showLicenseError(result.reason);
669
+ * ```
670
+ */
671
+ verify(input) {
672
+ return this.client.request('POST', '/api/v1/licenses/verify', input);
673
+ }
674
+ }
675
+ class UsageClient {
676
+ client;
677
+ constructor(client) {
678
+ this.client = client;
679
+ }
680
+ /**
681
+ * Record a usage event against a named meter. `quantity` can be
682
+ * negative to credit back (e.g. refunds). `occurredAt` defaults to
683
+ * server time; pass an ISO string when ingesting historical events.
684
+ */
685
+ record(input) {
686
+ return this.client.request('POST', '/api/v1/usage/record', input);
687
+ }
688
+ /**
689
+ * Sum recorded quantity for a meter, optionally bounded by a time window
690
+ * and/or scoped to a subject (`endUserId` or `organizationId`). Drives
691
+ * "you've used X of your Y quota" displays.
692
+ */
693
+ aggregate(input) {
694
+ const params = new URLSearchParams();
695
+ params.set('meterSlug', input.meterSlug);
696
+ if (input.from)
697
+ params.set('from', input.from);
698
+ if (input.to)
699
+ params.set('to', input.to);
700
+ if (input.endUserId)
701
+ params.set('endUserId', input.endUserId);
702
+ if (input.organizationId)
703
+ params.set('organizationId', input.organizationId);
704
+ return this.client.request('GET', `/api/v1/usage/aggregate?${params.toString()}`);
705
+ }
706
+ }
707
+ function creditSubjectQuery(subject) {
708
+ const p = new URLSearchParams();
709
+ if ('organizationId' in subject)
710
+ p.set('organizationId', subject.organizationId);
711
+ else
712
+ p.set('endUserId', subject.endUserId);
713
+ return p;
714
+ }
715
+ class CreditsClient {
716
+ client;
717
+ constructor(client) {
718
+ this.client = client;
719
+ }
720
+ /** Current spendable balance for a subject (end-user or org); 0 if none. */
721
+ getBalance(subject) {
722
+ return this.client.request('GET', `/api/v1/credits/balance?${creditSubjectQuery(subject)}`);
723
+ }
724
+ /**
725
+ * Deduct credits from a subject (end-user or org pool). Throws `RekeyError`
726
+ * `code: "CREDITS_INSUFFICIENT"` (HTTP 402) when the balance is too low.
727
+ *
728
+ * Pass `idempotencyKey` (e.g. the lead id) so a retried call never
729
+ * double-charges — a repeat returns the original result with `applied: false`.
730
+ */
731
+ consume(input) {
732
+ return this.client.request('POST', '/api/v1/credits/consume', input);
733
+ }
734
+ /**
735
+ * Ledger entries for a subject, newest first. Pass `offset` to page back
736
+ * through the full append-only history (the ledger grows for the life of a
737
+ * subject); `limit` is capped at 200 server-side.
738
+ */
739
+ listLedger(subject, limit, offset) {
740
+ const params = new URLSearchParams(creditSubjectQuery(subject));
741
+ if (limit !== undefined)
742
+ params.set('limit', String(limit));
743
+ if (offset !== undefined)
744
+ params.set('offset', String(offset));
745
+ return this.client.request('GET', `/api/v1/credits/ledger?${params.toString()}`);
746
+ }
747
+ }
748
+ /**
749
+ * Verify the HMAC signature on an inbound webhook from Rekey. Returns
750
+ * `true` only when (a) the timestamp is fresh (within `toleranceSeconds`,
751
+ * default 300) AND (b) the signature matches a constant-time compare.
752
+ *
753
+ * Use against the `X-Rekey-Signature` header and the raw request body
754
+ * BYTES (not the parsed JSON — any reserialization breaks the HMAC).
755
+ *
756
+ * @example
757
+ * ```ts
758
+ * import { verifyWebhookSignature } from '@rekey.dev/node';
759
+ *
760
+ * app.post('/webhooks/rekey', { config: { rawBody: true } }, (req) => {
761
+ * const ok = verifyWebhookSignature({
762
+ * header: req.headers['x-rekey-signature'] as string,
763
+ * payload: req.rawBody!,
764
+ * secret: process.env.RELIPAY_WEBHOOK_SECRET!,
765
+ * });
766
+ * if (!ok) return reply.status(401).send({ error: 'bad signature' });
767
+ * // safe to act on req.body
768
+ * });
769
+ * ```
770
+ */
771
+ export function verifyWebhookSignature(args) {
772
+ if (!args.header)
773
+ return false;
774
+ const tolerance = (args.toleranceSeconds ?? 300) * 1000;
775
+ const nowMs = args.now ? args.now() : Date.now();
776
+ const parts = args.header.split(',').reduce((acc, p) => {
777
+ const [k, v] = p.split('=', 2);
778
+ if (k && v)
779
+ acc[k.trim()] = v.trim();
780
+ return acc;
781
+ }, {});
782
+ const t = Number(parts.t);
783
+ const v1 = parts.v1;
784
+ if (!Number.isFinite(t) || !v1)
785
+ return false;
786
+ if (Math.abs(nowMs - t * 1000) > tolerance)
787
+ return false;
788
+ // Lazy-load Node crypto so the SDK still runs in edge runtimes that
789
+ // don't bundle it (signature verification is the only place we need
790
+ // crypto — everything else uses fetch).
791
+ // eslint-disable-next-line @typescript-eslint/no-require-imports
792
+ const { createHmac, timingSafeEqual } = require('node:crypto');
793
+ const body = typeof args.payload === 'string' ? Buffer.from(args.payload, 'utf8') : args.payload;
794
+ const signed = `${t}.${body.toString('utf8')}`;
795
+ const expected = createHmac('sha256', args.secret).update(signed).digest('hex');
796
+ const a = Buffer.from(expected, 'hex');
797
+ const b = Buffer.from(v1, 'hex');
798
+ if (a.length !== b.length)
799
+ return false;
800
+ return timingSafeEqual(a, b);
801
+ }
802
+ const jwksCache = new Map();
803
+ /** @internal Test hook — drop cached JWKS responses. */
804
+ export function _clearJwksCacheForTests() {
805
+ jwksCache.clear();
806
+ }
807
+ function b64urlJson(segment) {
808
+ return JSON.parse(Buffer.from(segment, 'base64url').toString('utf8'));
809
+ }
810
+ async function loadJwks(options, forceRefetch) {
811
+ if (options.jwks)
812
+ return options.jwks;
813
+ const url = options.jwksUrl;
814
+ if (!url) {
815
+ throw new RekeyError({
816
+ code: 'CONFIG_MISSING_JWKS',
817
+ message: 'verifyAccessToken requires either `jwksUrl` or a pre-fetched `jwks`.',
818
+ fix: 'Pass `jwksUrl: "https://<your-rekey>/.well-known/jwks.json"`.',
819
+ });
820
+ }
821
+ const ttl = options.cacheTtlMs ?? 5 * 60 * 1000;
822
+ const cached = jwksCache.get(url);
823
+ if (cached && !forceRefetch && Date.now() - cached.fetchedAt <= ttl)
824
+ return cached.jwks;
825
+ const fetchImpl = options.fetch ?? fetch;
826
+ const res = await fetchImpl(url);
827
+ if (!res.ok) {
828
+ throw new RekeyError({
829
+ code: 'JWKS_FETCH_FAILED',
830
+ message: `Fetching the JWKS from ${url} failed with status ${res.status}.`,
831
+ fix: 'Check the URL points at your Rekey deployment’s /.well-known/jwks.json.',
832
+ statusCode: res.status,
833
+ });
834
+ }
835
+ const jwks = (await res.json());
836
+ if (!jwks || !Array.isArray(jwks.keys)) {
837
+ throw new RekeyError({
838
+ code: 'JWKS_FETCH_FAILED',
839
+ message: 'The JWKS endpoint did not return a `{ keys: [...] }` body.',
840
+ fix: 'Check the URL points at /.well-known/jwks.json, not another route.',
841
+ });
842
+ }
843
+ jwksCache.set(url, { jwks, fetchedAt: Date.now() });
844
+ return jwks;
845
+ }
846
+ /**
847
+ * Verify an end-user ACCESS token **offline** — no round-trip to the Rekey
848
+ * API. Works only for Applications that opted into RS256 tokens
849
+ * (`authConfig.tokenAlg = "RS256"`, Panel → Application → Auth); the default
850
+ * HS256 tokens are symmetric and can only be verified by the API itself
851
+ * (use `rekey.auth.getCurrentUser(token)` for those).
852
+ *
853
+ * Checks performed (same posture as the API's verifier):
854
+ * - header `alg` must be `RS256` and `kid` must exist in the JWKS —
855
+ * a strict allowlist, immune to alg-confusion;
856
+ * - RSA-SHA256 signature against that public key;
857
+ * - `exp` in the future, `typ === "eu_access"` (refresh/MFA/MCP tokens
858
+ * are refused), `sub` + `applicationId` present.
859
+ *
860
+ * What it CANNOT check offline: the app's `tokenGeneration` kill-switch and
861
+ * user deletion. The 15-minute access lifetime bounds both; for hard
862
+ * revocation guarantees keep using `auth.getCurrentUser`.
863
+ *
864
+ * Node-only (uses `node:crypto`). Returns the verified claims; throws
865
+ * `RekeyError` on any failure.
866
+ *
867
+ * @example Express/Fastify middleware at the edge
868
+ * ```ts
869
+ * import { verifyAccessToken } from '@rekey.dev/node';
870
+ *
871
+ * const claims = await verifyAccessToken(req.headers['x-rekey-user-token'], {
872
+ * jwksUrl: 'https://rekey.example.com/.well-known/jwks.json',
873
+ * });
874
+ * if (claims.applicationId !== MY_APP_ID) throw new Error('wrong app');
875
+ * req.userId = claims.sub;
876
+ * ```
877
+ *
878
+ * @throws {RekeyError} `TOKEN_ALG_NOT_RS256` — token is HS256 (app hasn't opted in) or another alg.
879
+ * @throws {RekeyError} `TOKEN_KID_UNKNOWN` — `kid` not in the JWKS (forged, or key deleted).
880
+ * @throws {RekeyError} `USER_TOKEN_EXPIRED` — `exp` passed; refresh the session.
881
+ * @throws {RekeyError} `USER_TOKEN_INVALID` — malformed, bad signature, or wrong `typ`.
882
+ */
883
+ export async function verifyAccessToken(token, options) {
884
+ const invalid = (message) => new RekeyError({
885
+ code: 'USER_TOKEN_INVALID',
886
+ message,
887
+ fix: 'Have the user sign in again to obtain a fresh token.',
888
+ statusCode: 401,
889
+ });
890
+ const parts = token.split('.');
891
+ if (parts.length !== 3 || !parts[0] || !parts[1] || !parts[2]) {
892
+ throw invalid('The token is not a three-part JWT.');
893
+ }
894
+ let header;
895
+ let payload;
896
+ try {
897
+ header = b64urlJson(parts[0]);
898
+ payload = b64urlJson(parts[1]);
899
+ }
900
+ catch {
901
+ throw invalid('The token header/payload is not valid base64url JSON.');
902
+ }
903
+ // Strict alg allowlist — this helper verifies RS256 ONLY. HS256 tokens are
904
+ // symmetric (the verifying key can also MINT tokens), so they are never
905
+ // verified client-side.
906
+ if (header.alg !== 'RS256') {
907
+ throw new RekeyError({
908
+ code: 'TOKEN_ALG_NOT_RS256',
909
+ message: `Offline verification supports RS256 tokens only (got alg=${String(header.alg)}).`,
910
+ fix: 'Enable RS256 for the Application (authConfig.tokenAlg) or verify via auth.getCurrentUser().',
911
+ statusCode: 401,
912
+ });
913
+ }
914
+ if (typeof header.kid !== 'string' || header.kid.length === 0) {
915
+ throw invalid('RS256 token is missing the `kid` header.');
916
+ }
917
+ // kid lookup, with one forced refetch on miss (key rotated in since the
918
+ // cached copy was fetched).
919
+ let jwks = await loadJwks(options, false);
920
+ let jwk = jwks.keys.find((k) => k.kid === header.kid);
921
+ if (!jwk && options.jwksUrl && !options.jwks) {
922
+ jwks = await loadJwks(options, true);
923
+ jwk = jwks.keys.find((k) => k.kid === header.kid);
924
+ }
925
+ if (!jwk) {
926
+ throw new RekeyError({
927
+ code: 'TOKEN_KID_UNKNOWN',
928
+ message: `No JWKS key matches the token's kid (${header.kid}).`,
929
+ fix: 'The token may be forged, or its signing key was deleted after rotation. Re-authenticate the user.',
930
+ statusCode: 401,
931
+ });
932
+ }
933
+ // Lazy-load node:crypto (same posture as verifyWebhookSignature) — keeps
934
+ // the import graph clean for bundlers that tree-shake this helper away.
935
+ // eslint-disable-next-line @typescript-eslint/no-require-imports
936
+ const { createPublicKey, verify } = require('node:crypto');
937
+ let signatureOk = false;
938
+ try {
939
+ const publicKey = createPublicKey({ key: { kty: jwk.kty, n: jwk.n, e: jwk.e }, format: 'jwk' });
940
+ signatureOk = verify('sha256', Buffer.from(`${parts[0]}.${parts[1]}`, 'utf8'), publicKey, Buffer.from(parts[2], 'base64url'));
941
+ }
942
+ catch {
943
+ signatureOk = false;
944
+ }
945
+ if (!signatureOk)
946
+ throw invalid('The token signature does not verify against the JWKS key.');
947
+ // Claims — mirror the API's verifier: typ is load-bearing, exp is enforced.
948
+ if (payload.typ !== 'eu_access') {
949
+ throw invalid(`Token typ is ${JSON.stringify(payload.typ)}, expected "eu_access".`);
950
+ }
951
+ if (typeof payload.sub !== 'string' || typeof payload.applicationId !== 'string') {
952
+ throw invalid('Token is missing the sub/applicationId claims.');
953
+ }
954
+ const nowSec = Math.floor((options.now ? options.now() : Date.now()) / 1000);
955
+ if (typeof payload.exp !== 'number' || payload.exp <= nowSec) {
956
+ throw new RekeyError({
957
+ code: 'USER_TOKEN_EXPIRED',
958
+ message: 'The access token has expired.',
959
+ fix: 'Refresh the session (auth.refresh) or have the user sign in again.',
960
+ statusCode: 401,
961
+ });
962
+ }
963
+ return payload;
964
+ }
965
+ class BillingClient {
966
+ client;
967
+ constructor(client) {
968
+ this.client = client;
969
+ }
970
+ /**
971
+ * List the calling Application's active plans. Public — pricing pages
972
+ * typically render straight from this. Application API key only; no
973
+ * user JWT needed.
974
+ *
975
+ * `amount` is in the smallest currency unit (cents/paise/sen) — never
976
+ * a float. Format on display: `${amount / 100} ${currency}`.
977
+ */
978
+ getPlans() {
979
+ return this.client.request('GET', '/api/v1/billing/plans');
980
+ }
981
+ /**
982
+ * Fetch the current end-user's active subscription, or `null` if they
983
+ * have none. Returns the most recent ACTIVE / PENDING / PAST_DUE row.
984
+ *
985
+ * Pass the user's access token (the SDK puts it in `X-Rekey-User-Token`).
986
+ */
987
+ getSubscription(accessToken) {
988
+ return this.client.request('GET', '/api/v1/billing/subscription', undefined, {
989
+ 'X-Rekey-User-Token': accessToken,
990
+ });
991
+ }
992
+ /**
993
+ * Start a hosted-checkout session. Returns the URL to redirect the user
994
+ * to and the local PENDING Subscription row. Subscription activation
995
+ * happens via the provider's webhook — not synchronously here.
996
+ *
997
+ * Pass `couponCode` to apply a discount. The whole checkout fails if the
998
+ * coupon doesn't validate (typed `RekeyError` with the precise reason).
999
+ *
1000
+ * If the Application's billing subject is **org** (Panel → Application →
1001
+ * Billing → Subject), an individual can't hold a subscription — you MUST
1002
+ * pass `organizationId` of a team the user owns/admins. Omitting it throws
1003
+ * `RekeyError` `code: "BILLING_ORGANIZATION_REQUIRED"`.
1004
+ *
1005
+ * @example
1006
+ * ```ts
1007
+ * const { url, discountAmount } = await rekey.billing.createCheckout(userAccessToken, {
1008
+ * planSlug: 'pro_monthly',
1009
+ * successUrl: 'https://yourapp.com/billing?status=ok',
1010
+ * cancelUrl: 'https://yourapp.com/billing?status=cancel',
1011
+ * couponCode: 'LAUNCH50', // optional
1012
+ * });
1013
+ * res.redirect(url);
1014
+ * ```
1015
+ */
1016
+ createCheckout(accessToken, input) {
1017
+ return this.client.request('POST', '/api/v1/billing/checkout', input, {
1018
+ 'X-Rekey-User-Token': accessToken,
1019
+ });
1020
+ }
1021
+ /**
1022
+ * Validate a coupon for the current user against a plan, *without*
1023
+ * applying it. Render "$50 off" on a pricing page before submit.
1024
+ *
1025
+ * @throws {RekeyError} with one of `COUPON_NOT_FOUND` / `COUPON_INACTIVE`
1026
+ * / `COUPON_NOT_YET_STARTED` / `COUPON_EXPIRED` / `COUPON_NOT_APPLICABLE`
1027
+ * / `COUPON_CURRENCY_MISMATCH` / `COUPON_REDEMPTION_LIMIT_REACHED` /
1028
+ * `COUPON_USER_LIMIT_REACHED`. Surface the message + fix to the user.
1029
+ */
1030
+ validateCoupon(accessToken, input) {
1031
+ return this.client.request('POST', '/api/v1/billing/coupons/validate', input, {
1032
+ 'X-Rekey-User-Token': accessToken,
1033
+ });
1034
+ }
1035
+ /**
1036
+ * List the billing providers configured + enabled for this Application,
1037
+ * in the order the geo router would prefer them. Forward the end-user's
1038
+ * `country` (ISO 3166-1 alpha-2) when you have it — the panel/SDK will
1039
+ * surface India-specific providers (Razorpay) for IN-country users, etc.
1040
+ *
1041
+ * Returns the resolved country (echoed back from the server's view of
1042
+ * `CF-IPCountry` etc.) plus the ordered provider list. Use this to render
1043
+ * a "Pay with..." picker on your pricing page.
1044
+ */
1045
+ getProviders(country) {
1046
+ const headers = {};
1047
+ if (country)
1048
+ headers['x-country'] = country.toUpperCase();
1049
+ return this.client.request('GET', '/api/v1/billing/providers', undefined, headers);
1050
+ }
1051
+ /**
1052
+ * Resolve the calling end-user's current entitlements — feature flags +
1053
+ * limits, the live credit balance, and the raw entitlement list, unioned
1054
+ * across their active subscriptions (and subscriptions of orgs they belong
1055
+ * to). Pass `{ organizationId }` (member-only) for that org's view + shared
1056
+ * pool. Gate your app's features on `features`.
1057
+ *
1058
+ * @example
1059
+ * ```ts
1060
+ * const { features } = await rekey.billing.getEntitlements(userAccessToken);
1061
+ * if (features.advanced_reporting) renderReportingTab();
1062
+ * ```
1063
+ */
1064
+ getEntitlements(accessToken, opts) {
1065
+ const qs = opts?.organizationId
1066
+ ? `?organizationId=${encodeURIComponent(opts.organizationId)}`
1067
+ : '';
1068
+ return this.client.request('GET', `/api/v1/billing/entitlements${qs}`, undefined, {
1069
+ 'X-Rekey-User-Token': accessToken,
1070
+ });
1071
+ }
1072
+ }
1073
+ //# sourceMappingURL=index.js.map