@rekey.dev/node 2.1.0 → 2.2.0-rc.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/README.md +31 -6
- package/dist/index.d.ts +530 -96
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +553 -98
- package/dist/index.js.map +1 -1
- package/package.json +6 -5
package/dist/index.js
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* @rekey.dev/node
|
|
2
|
+
* @rekey.dev/node, server SDK for Rekey.
|
|
3
3
|
*
|
|
4
4
|
* One client instance per Application. Construct with the Application's
|
|
5
5
|
* secret key (`rp_live_…` or `rp_test_…`) and the URL of your Rekey
|
|
6
|
-
* deployment. Never ship the secret key to the browser
|
|
6
|
+
* deployment. Never ship the secret key to the browser, for browser code
|
|
7
7
|
* use `@rekey.dev/react` with the Application's public key instead.
|
|
8
8
|
*
|
|
9
9
|
* @example Smoke-test your credentials
|
|
@@ -21,24 +21,24 @@
|
|
|
21
21
|
*/
|
|
22
22
|
// The canonical error class lives in shared-types; import it for internal use
|
|
23
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
|
|
24
|
+
// `/error` subpath is the zod-free module the class actually lives in, same
|
|
25
25
|
// class object the barrel re-exports, so `instanceof` is identical.
|
|
26
26
|
import { RekeyError } from '@rekey.dev/shared-types/error';
|
|
27
27
|
/**
|
|
28
28
|
* Default per-request deadline, in milliseconds. Matches the timeout the Rekey
|
|
29
29
|
* API itself uses when it POSTs your outbound webhooks.
|
|
30
30
|
*
|
|
31
|
-
* Without a deadline the effective timeout is undici's `headersTimeout
|
|
32
|
-
* minutes
|
|
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
33
|
* request handlers for that long. Ten seconds is long enough for any endpoint
|
|
34
34
|
* this SDK calls and short enough to fail a page instead of hanging it.
|
|
35
35
|
*/
|
|
36
36
|
export const DEFAULT_TIMEOUT_MS = 10_000;
|
|
37
|
-
// RekeyError is the shared class (imported above)
|
|
37
|
+
// RekeyError is the shared class (imported above), re-exported so the public
|
|
38
38
|
// API name is preserved and `instanceof` is consistent with @rekey.dev/react.
|
|
39
39
|
export { RekeyError };
|
|
40
40
|
/**
|
|
41
|
-
* Outbound webhook event registry
|
|
41
|
+
* Outbound webhook event registry, the events Rekey can POST to your app
|
|
42
42
|
* (verify them with `verifyWebhookSignature` below). `WEBHOOK_EVENTS` carries
|
|
43
43
|
* `{ name, description }` pairs for introspection/autocomplete;
|
|
44
44
|
* `KNOWN_WEBHOOK_EVENTS` is just the names. Mirrors the API's registry exactly.
|
|
@@ -47,7 +47,7 @@ export { RekeyError };
|
|
|
47
47
|
* ```ts
|
|
48
48
|
* import { WEBHOOK_EVENTS, isKnownWebhookEvent, type WebhookEventEnvelope } from '@rekey.dev/node';
|
|
49
49
|
*
|
|
50
|
-
* for (const e of WEBHOOK_EVENTS) console.log(`${e.name}
|
|
50
|
+
* for (const e of WEBHOOK_EVENTS) console.log(`${e.name}, ${e.description}`);
|
|
51
51
|
*
|
|
52
52
|
* const event = req.body as WebhookEventEnvelope; // after verifyWebhookSignature(...)
|
|
53
53
|
* if (event.type === 'subscription.activated') unlockPlan(event.data);
|
|
@@ -59,7 +59,7 @@ export { WEBHOOK_EVENTS, KNOWN_WEBHOOK_EVENTS, isKnownWebhookEvent } from '@reke
|
|
|
59
59
|
* this subscriber the rest of the period they paid for?
|
|
60
60
|
*
|
|
61
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
|
|
62
|
+
* is about to get, and it has to say so BEFORE the call, there is no response
|
|
63
63
|
* to read it off. It is the same function the API decides from, not a
|
|
64
64
|
* description of it, so a UI built on it cannot promise a behaviour the server
|
|
65
65
|
* does not have. See its docblock for the cases that still end immediately.
|
|
@@ -87,25 +87,29 @@ export class Rekey {
|
|
|
87
87
|
config;
|
|
88
88
|
/** Operations on the calling Application itself. */
|
|
89
89
|
applications;
|
|
90
|
-
/** Auth operations
|
|
90
|
+
/** Auth operations, sign-in, sign-up, sessions, passkeys, magic-link. */
|
|
91
91
|
auth;
|
|
92
|
-
/** Billing operations
|
|
92
|
+
/** Billing operations, plans, checkout, subscriptions, coupons. */
|
|
93
93
|
billing;
|
|
94
|
-
/** End-user organizations
|
|
94
|
+
/** End-user organizations, create, invite, members, role changes. */
|
|
95
95
|
organizations;
|
|
96
96
|
/** License key verification + activation. */
|
|
97
97
|
licenses;
|
|
98
|
-
/**
|
|
98
|
+
/** Devices, list and release the machines an end-user signs in from. */
|
|
99
|
+
devices;
|
|
100
|
+
/** End-users, lookup by id or email, bulk import. */
|
|
101
|
+
users;
|
|
102
|
+
/** Usage metering, record events, aggregate windows. */
|
|
99
103
|
usage;
|
|
100
|
-
/** Prepaid credits
|
|
104
|
+
/** Prepaid credits, balance reads, idempotent drawdown, ledger. */
|
|
101
105
|
credits;
|
|
102
|
-
/** MCP
|
|
106
|
+
/** MCP, validate Rekey-issued MCP tokens from your own MCP server. */
|
|
103
107
|
mcp;
|
|
104
108
|
constructor(config) {
|
|
105
109
|
if (!config.apiUrl) {
|
|
106
110
|
// Name the Cloud value outright. There is no default and there should
|
|
107
|
-
// not be one
|
|
108
|
-
// deployment's traffic at somebody else's API
|
|
111
|
+
// not be one, a wrong default would silently point a self-hosted
|
|
112
|
+
// deployment's traffic at somebody else's API, but "requires apiUrl"
|
|
109
113
|
// alone leaves a Rekey Cloud customer with no way to find out what to
|
|
110
114
|
// put, because every example in the docs reads as a placeholder.
|
|
111
115
|
throw new RekeyError({
|
|
@@ -132,12 +136,14 @@ export class Rekey {
|
|
|
132
136
|
this.billing = new BillingClient(this);
|
|
133
137
|
this.organizations = new OrganizationsClient(this);
|
|
134
138
|
this.licenses = new LicensesClient(this);
|
|
139
|
+
this.devices = new DevicesClient(this);
|
|
140
|
+
this.users = new UsersClient(this);
|
|
135
141
|
this.usage = new UsageClient(this);
|
|
136
142
|
this.credits = new CreditsClient(this);
|
|
137
143
|
this.mcp = new McpClient(this);
|
|
138
144
|
}
|
|
139
145
|
/**
|
|
140
|
-
* A clone of this client with different call options
|
|
146
|
+
* A clone of this client with different call options, the per-call knob for
|
|
141
147
|
* every wrapped method.
|
|
142
148
|
*
|
|
143
149
|
* Each namespace method (`billing.getPlans()`, `auth.signIn()`, …) has a
|
|
@@ -157,7 +163,7 @@ export class Rekey {
|
|
|
157
163
|
* });
|
|
158
164
|
* ```
|
|
159
165
|
*
|
|
160
|
-
* Cheap
|
|
166
|
+
* Cheap, it rebuilds the namespace objects, holds no connections, and
|
|
161
167
|
* shares the same `fetch`.
|
|
162
168
|
*/
|
|
163
169
|
with(options) {
|
|
@@ -174,7 +180,7 @@ export class Rekey {
|
|
|
174
180
|
* Call a Rekey endpoint this SDK does not wrap yet.
|
|
175
181
|
*
|
|
176
182
|
* This is a **supported** escape hatch, not an internal: when the API grows a
|
|
177
|
-
* route before the SDK does, use this instead of hand-rolling `fetch
|
|
183
|
+
* route before the SDK does, use this instead of hand-rolling `fetch`, you
|
|
178
184
|
* keep the auth header, the `{ success, data }` unwrapping, the `RekeyError`
|
|
179
185
|
* mapping (including transport failures) and the deadline. It takes an
|
|
180
186
|
* options object precisely so a future knob does not need a new overload.
|
|
@@ -204,6 +210,19 @@ export class Rekey {
|
|
|
204
210
|
* surface (see `stripInternal` in tsconfig).
|
|
205
211
|
*/
|
|
206
212
|
async send(method, path, body, extraHeaders, options) {
|
|
213
|
+
return (await this.sendWithStatus(method, path, body, extraHeaders, options)).data;
|
|
214
|
+
}
|
|
215
|
+
/**
|
|
216
|
+
* @internal Same request as {@link send}, but keeps the HTTP status.
|
|
217
|
+
*
|
|
218
|
+
* Almost every endpoint encodes its whole answer in the body, which is why
|
|
219
|
+
* `send` throws the status away. `POST /billing/subscribe` does not: it
|
|
220
|
+
* returns the same Subscription under 201 (it just activated the free tier)
|
|
221
|
+
* and under 200 (the caller already had it, nothing was written). Dropping
|
|
222
|
+
* the status there would make "you are now on the free tier" and "you already
|
|
223
|
+
* were" indistinguishable to the caller.
|
|
224
|
+
*/
|
|
225
|
+
async sendWithStatus(method, path, body, extraHeaders, options) {
|
|
207
226
|
const res = await this.fetchWithDeadline(`${this.apiUrl}${path}`, {
|
|
208
227
|
method,
|
|
209
228
|
headers: {
|
|
@@ -230,10 +249,10 @@ export class Rekey {
|
|
|
230
249
|
...(resolvedRequestId !== undefined && { requestId: resolvedRequestId }),
|
|
231
250
|
});
|
|
232
251
|
}
|
|
233
|
-
return json.data;
|
|
252
|
+
return { data: json.data, status: res.status };
|
|
234
253
|
}
|
|
235
254
|
/**
|
|
236
|
-
* @internal Raw request for the non-enveloped OAuth/MCP endpoints
|
|
255
|
+
* @internal Raw request for the non-enveloped OAuth/MCP endpoints, returns
|
|
237
256
|
* the parsed JSON as-is (those endpoints emit standard OAuth shapes, not the
|
|
238
257
|
* `{ success, data }` envelope). Throws `RekeyError` on non-2xx, mapping
|
|
239
258
|
* the OAuth `{ error, error_description }` body when present.
|
|
@@ -260,7 +279,7 @@ export class Rekey {
|
|
|
260
279
|
/**
|
|
261
280
|
* @internal The one place `fetch` is called. Applies the deadline, composes
|
|
262
281
|
* the caller's signals, and turns anything the transport throws into a
|
|
263
|
-
* `RekeyError
|
|
282
|
+
* `RekeyError`, without this, `ECONNREFUSED` escaped as a bare `TypeError`
|
|
264
283
|
* and slipped straight through the documented
|
|
265
284
|
* `catch (e) { if (e instanceof RekeyError) … }` pattern.
|
|
266
285
|
*/
|
|
@@ -315,7 +334,7 @@ function transportError(cause, deadline, method, path) {
|
|
|
315
334
|
return new RekeyError({
|
|
316
335
|
code: 'REQUEST_ABORTED',
|
|
317
336
|
message: `${where} was aborted by the caller's AbortSignal.`,
|
|
318
|
-
fix: 'This is your own cancellation
|
|
337
|
+
fix: 'This is your own cancellation, swallow it, or check the signal you passed to `signal` / `Rekey.with({ signal })`.',
|
|
319
338
|
cause,
|
|
320
339
|
});
|
|
321
340
|
}
|
|
@@ -336,7 +355,7 @@ function transportError(cause, deadline, method, path) {
|
|
|
336
355
|
}
|
|
337
356
|
/**
|
|
338
357
|
* MCP helpers for customers running their OWN MCP server behind Rekey auth.
|
|
339
|
-
* The hosted MCP server (account tools) is consumed by MCP clients directly
|
|
358
|
+
* The hosted MCP server (account tools) is consumed by MCP clients directly,
|
|
340
359
|
* this client is for the "bring your own MCP server" path: validate incoming
|
|
341
360
|
* Rekey-issued tokens, and read the OAuth metadata.
|
|
342
361
|
*/
|
|
@@ -380,7 +399,7 @@ class ApplicationsClient {
|
|
|
380
399
|
}
|
|
381
400
|
/**
|
|
382
401
|
* Verify credentials and fetch the calling Application. Use this as your
|
|
383
|
-
* SDK smoke test
|
|
402
|
+
* SDK smoke test, if it returns, your secret key is good and you're
|
|
384
403
|
* pointed at the right Rekey deployment.
|
|
385
404
|
*
|
|
386
405
|
* @example
|
|
@@ -406,7 +425,7 @@ class AuthClient {
|
|
|
406
425
|
* (e.g. `getCurrentUser(accessToken)`) and a `refreshToken` to renew it.
|
|
407
426
|
*
|
|
408
427
|
* Unless the Application turns `authConfig.sendVerificationEmailOnSignUp`
|
|
409
|
-
* off, Rekey also emails the verification link
|
|
428
|
+
* off, Rekey also emails the verification link, best-effort, so it never
|
|
410
429
|
* fails the sign-up, and `sendVerificationEmail` re-sends on demand.
|
|
411
430
|
*
|
|
412
431
|
* @example
|
|
@@ -415,7 +434,7 @@ class AuthClient {
|
|
|
415
434
|
* email: 'alice@example.com',
|
|
416
435
|
* password: 'correct-horse-battery-staple',
|
|
417
436
|
* });
|
|
418
|
-
* // store both in your session
|
|
437
|
+
* // store both in your session, the access token expires in 15 minutes
|
|
419
438
|
* ```
|
|
420
439
|
*
|
|
421
440
|
* @throws {RekeyError} `EMAIL_ALREADY_EXISTS` (409) if the email is taken in this Application.
|
|
@@ -434,14 +453,14 @@ class AuthClient {
|
|
|
434
453
|
* Prompt the user for their TOTP / backup code and call
|
|
435
454
|
* `mfaVerify({ mfaChallengeToken, code })` to receive a real session.
|
|
436
455
|
*
|
|
437
|
-
* **Branch on `result.mfaRequired` before reading `accessToken
|
|
456
|
+
* **Branch on `result.mfaRequired` before reading `accessToken`**, the
|
|
438
457
|
* MFA-required branch has no session tokens.
|
|
439
458
|
*
|
|
440
|
-
* @throws {RekeyError} `INVALID_CREDENTIALS` (401)
|
|
459
|
+
* @throws {RekeyError} `INVALID_CREDENTIALS` (401), single code on purpose.
|
|
441
460
|
* Don't try to distinguish wrong-email from wrong-password from the SDK side either.
|
|
442
461
|
* @throws {RekeyError} `EMAIL_NOT_VERIFIED` (403) when the Application sets
|
|
443
462
|
* `authConfig.requireEmailVerification` and the user hasn't confirmed their
|
|
444
|
-
* address. The password was correct
|
|
463
|
+
* address. The password was correct, prompt for the emailed link (or call
|
|
445
464
|
* `sendVerificationEmail`), not for the password again.
|
|
446
465
|
*/
|
|
447
466
|
signIn(input) {
|
|
@@ -471,7 +490,7 @@ class AuthClient {
|
|
|
471
490
|
return this.client.send('POST', '/api/v1/auth/magic-link/request', input);
|
|
472
491
|
}
|
|
473
492
|
/**
|
|
474
|
-
* Consume a magic-link token. Returns `SignInOutcome
|
|
493
|
+
* Consume a magic-link token. Returns `SignInOutcome`, branch on
|
|
475
494
|
* `mfaRequired` before reading `accessToken`. For MFA-enrolled users
|
|
476
495
|
* the response carries `mfaChallengeToken` and you must complete via
|
|
477
496
|
* `mfaVerify(...)`.
|
|
@@ -482,7 +501,7 @@ class AuthClient {
|
|
|
482
501
|
/**
|
|
483
502
|
* Begin a passkey authentication ceremony. Returns the WebAuthn options
|
|
484
503
|
* to forward to the browser (`navigator.credentials.get(...)`) along
|
|
485
|
-
* with `expectedChallenge
|
|
504
|
+
* with `expectedChallenge`, bind the challenge to your session and
|
|
486
505
|
* pass both back via `verifyPasskeyAuthentication(...)`.
|
|
487
506
|
*/
|
|
488
507
|
startPasskeyAuthentication(input) {
|
|
@@ -490,7 +509,7 @@ class AuthClient {
|
|
|
490
509
|
}
|
|
491
510
|
/**
|
|
492
511
|
* Complete a passkey authentication. Returns the same `SignInOutcome`
|
|
493
|
-
* shape as `signIn
|
|
512
|
+
* shape as `signIn`, but passkeys are themselves a strong factor, so
|
|
494
513
|
* `mfaRequired` will always be `false` in practice.
|
|
495
514
|
*/
|
|
496
515
|
verifyPasskeyAuthentication(input) {
|
|
@@ -515,7 +534,7 @@ class AuthClient {
|
|
|
515
534
|
/**
|
|
516
535
|
* List the user's registered passkeys, newest first.
|
|
517
536
|
*
|
|
518
|
-
* Returns `{items, page}
|
|
537
|
+
* Returns `{items, page}`, `page.total` is the number of passkeys the user
|
|
519
538
|
* has, independent of the window served.
|
|
520
539
|
*/
|
|
521
540
|
listPasskeys(accessToken, page) {
|
|
@@ -528,24 +547,46 @@ class AuthClient {
|
|
|
528
547
|
return this.client.send('DELETE', `/api/v1/auth/passkeys/${encodeURIComponent(credentialRowId)}`, undefined, { 'X-Rekey-User-Token': accessToken });
|
|
529
548
|
}
|
|
530
549
|
// End-user organization / team methods live on `rekey.organizations.*`
|
|
531
|
-
// (OrganizationsClient)
|
|
550
|
+
// (OrganizationsClient), the canonical, fuller surface. The earlier
|
|
532
551
|
// duplicates here (createOrganization / listMyOrganizations /
|
|
533
552
|
// inviteToOrganization / acceptOrganizationInvitation) were removed to
|
|
534
553
|
// avoid two divergent copies of the same endpoints.
|
|
535
554
|
/**
|
|
536
555
|
* Resolve the end-user behind a presented access token.
|
|
537
556
|
*
|
|
538
|
-
*
|
|
557
|
+
* Pass `include` to get more of what a backend needs to authorise the
|
|
558
|
+
* request in the same round trip: `entitlements` (as `billing.getEntitlements`
|
|
559
|
+
* returns them), `device` (the device the session is bound to, or null),
|
|
560
|
+
* `subscription` (as `billing.getSubscription` returns it), `organization`
|
|
561
|
+
* (the active organization with the caller's role) and `licenses` (as
|
|
562
|
+
* `{ items, truncated }`, the first 100 of `licenses.listMine`). With a literal list
|
|
563
|
+
* (inline or `as const`) the return type gains exactly those properties;
|
|
564
|
+
* with a list typed `MeInclude[]` they are optional, since the compiler
|
|
565
|
+
* cannot know which it holds.
|
|
566
|
+
*
|
|
567
|
+
* @example
|
|
568
|
+
* ```ts
|
|
569
|
+
* const me = await rekey.auth.getCurrentUser(accessToken, { include: ['entitlements', 'device'] });
|
|
570
|
+
* if (!me.entitlements.features.reports) throw new Forbidden();
|
|
571
|
+
* me.device?.status; // 'ACTIVE' when bound; a released or blocked device is a 401 instead
|
|
572
|
+
* ```
|
|
573
|
+
*
|
|
574
|
+
* @throws {RekeyError} `USER_TOKEN_INVALID` (401) if expired/forged/wrong-secret, or
|
|
575
|
+
* if the session's device was released or blocked.
|
|
539
576
|
* @throws {RekeyError} `USER_TOKEN_WRONG_APPLICATION` (401) if the token was issued
|
|
540
577
|
* by a different Application than the calling secret key represents.
|
|
578
|
+
* @throws {RekeyError} `VALIDATION_ERROR` (400) for an unknown `include` value.
|
|
579
|
+
* @throws {RekeyError} `BILLING_DISABLED` (403) or `API_KEY_SCOPE_INSUFFICIENT` (403)
|
|
580
|
+
* when `entitlements`, `subscription` or `licenses` is asked for and billing is off,
|
|
581
|
+
* or the key lacks `billing:read`.
|
|
541
582
|
*/
|
|
542
|
-
getCurrentUser(accessToken) {
|
|
543
|
-
return this.client.send('GET',
|
|
583
|
+
getCurrentUser(accessToken, options = {}) {
|
|
584
|
+
return this.client.send('GET', `/api/v1/users/me/${meIncludeQuery(options.include)}`, undefined, {
|
|
544
585
|
'X-Rekey-User-Token': accessToken,
|
|
545
586
|
});
|
|
546
587
|
}
|
|
547
588
|
/**
|
|
548
|
-
* Update the end-user behind a presented access token
|
|
589
|
+
* Update the end-user behind a presented access token, their OWN record,
|
|
549
590
|
* and only ever their own: the token identifies the subject, so there is no
|
|
550
591
|
* user id to pass and no way to aim this at anyone else.
|
|
551
592
|
*
|
|
@@ -578,21 +619,26 @@ class AuthClient {
|
|
|
578
619
|
}
|
|
579
620
|
/**
|
|
580
621
|
* Exchange a refresh token for a fresh {access, refresh} pair. The presented
|
|
581
|
-
* refresh is revoked atomically
|
|
622
|
+
* refresh is revoked atomically, call this **once** and store the new
|
|
582
623
|
* `refreshToken` from the response immediately.
|
|
583
624
|
*
|
|
584
625
|
* @throws {RekeyError} `REFRESH_TOKEN_REUSED` (401) if you replay an already-used token.
|
|
585
626
|
* This is a strong signal the original was leaked; treat as compromise.
|
|
586
627
|
* @throws {RekeyError} `REFRESH_TOKEN_EXPIRED` (401) after the 30-day refresh window.
|
|
587
628
|
*/
|
|
588
|
-
refresh(refreshToken) {
|
|
589
|
-
// /auth/refresh returns the same shape as /auth/mfa-verify
|
|
629
|
+
refresh(refreshToken, options = {}) {
|
|
630
|
+
// /auth/refresh returns the same shape as /auth/mfa-verify, always a
|
|
590
631
|
// full session (refresh requires a prior MFA-verified session by
|
|
591
|
-
// definition).
|
|
592
|
-
|
|
632
|
+
// definition). `device` identifies the machine presenting the token: a
|
|
633
|
+
// chain bound at sign-in refuses a different fingerprint, and an unbound
|
|
634
|
+
// one becomes bound (docs/devices.md).
|
|
635
|
+
return this.client.send('POST', '/api/v1/auth/refresh', {
|
|
636
|
+
refreshToken,
|
|
637
|
+
...(options.device && { device: options.device }),
|
|
638
|
+
});
|
|
593
639
|
}
|
|
594
640
|
/**
|
|
595
|
-
* Revoke a refresh token. Idempotent
|
|
641
|
+
* Revoke a refresh token. Idempotent, no-op for unknown tokens. The
|
|
596
642
|
* access token paired with this refresh remains valid until its short
|
|
597
643
|
* (15 min) expiry; for true "log out everywhere" semantics, also clear
|
|
598
644
|
* the access token from your client.
|
|
@@ -601,14 +647,14 @@ class AuthClient {
|
|
|
601
647
|
return this.client.send('POST', '/api/v1/auth/sign-out', { refreshToken });
|
|
602
648
|
}
|
|
603
649
|
/**
|
|
604
|
-
* Request a password reset for an email. Always succeeds
|
|
650
|
+
* Request a password reset for an email. Always succeeds, never tells you
|
|
605
651
|
* whether the email exists.
|
|
606
652
|
*
|
|
607
653
|
* **Branch on the result.** When the Application has an email transport
|
|
608
654
|
* (BYO Resend/SMTP, or a deployment-wide `RESEND_DEFAULT_API_KEY`) Rekey sends
|
|
609
655
|
* the mail itself and `resetToken` is null. With no transport it falls back to
|
|
610
|
-
* the original contract and hands the raw token to you
|
|
611
|
-
* only
|
|
656
|
+
* the original contract and hands the raw token to you, a secret-key caller
|
|
657
|
+
* only, so you can deliver it with your own provider.
|
|
612
658
|
*
|
|
613
659
|
* @example
|
|
614
660
|
* ```ts
|
|
@@ -634,7 +680,7 @@ class AuthClient {
|
|
|
634
680
|
}
|
|
635
681
|
/**
|
|
636
682
|
* Authenticated password change. Pass the user's *current* access token.
|
|
637
|
-
* On success, every refresh token for the user is revoked
|
|
683
|
+
* On success, every refresh token for the user is revoked, other devices
|
|
638
684
|
* are signed out.
|
|
639
685
|
*/
|
|
640
686
|
changePassword(accessToken, input) {
|
|
@@ -644,8 +690,8 @@ class AuthClient {
|
|
|
644
690
|
}
|
|
645
691
|
/**
|
|
646
692
|
* Revoke every refresh token for the calling user. "Sign out of all
|
|
647
|
-
* devices." The caller's access token remains valid until 15-min expiry
|
|
648
|
-
*
|
|
693
|
+
* devices." The caller's access token remains valid until 15-min expiry,
|
|
694
|
+
* clear it client-side for full logout.
|
|
649
695
|
*/
|
|
650
696
|
signOutEverywhere(accessToken) {
|
|
651
697
|
return this.client.send('POST', '/api/v1/auth/sign-out-everywhere', undefined, { 'X-Rekey-User-Token': accessToken });
|
|
@@ -665,7 +711,7 @@ class AuthClient {
|
|
|
665
711
|
});
|
|
666
712
|
}
|
|
667
713
|
/**
|
|
668
|
-
* Re-send a verification link to an address, with **no session
|
|
714
|
+
* Re-send a verification link to an address, with **no session**, the
|
|
669
715
|
* sessionless sibling of `sendVerificationEmail`.
|
|
670
716
|
*
|
|
671
717
|
* This is the route for a user locked out by
|
|
@@ -673,17 +719,17 @@ class AuthClient {
|
|
|
673
719
|
* session `sendVerificationEmail` needs, so a user whose first mail never
|
|
674
720
|
* arrived cannot ask for another. Takes the address instead of a token.
|
|
675
721
|
*
|
|
676
|
-
* **Branch on the result**, exactly as with `requestPasswordReset
|
|
722
|
+
* **Branch on the result**, exactly as with `requestPasswordReset`, the
|
|
677
723
|
* contract is the same one. It never throws for an unknown address and never
|
|
678
724
|
* discloses whether the address exists, is already verified, or was mailed:
|
|
679
725
|
* a publishable-key caller gets one constant body whatever happened. A
|
|
680
|
-
* secret-key caller
|
|
726
|
+
* secret-key caller, this SDK, gets the real outcome, and the raw
|
|
681
727
|
* `verificationToken` when the Application has no email transport configured,
|
|
682
728
|
* so you can deliver it with your own provider.
|
|
683
729
|
*
|
|
684
730
|
* Pass `verifyUrl` containing `{token}` to template the link target. Unlike
|
|
685
731
|
* `sendVerificationEmail`, nothing is sent and no token is minted when no
|
|
686
|
-
* link can be built at all
|
|
732
|
+
* link can be built at all, pass `verifyUrl`, or set the Application URL
|
|
687
733
|
* (Panel → Application → Auth). Mailing a locked-out user a verification
|
|
688
734
|
* message with no button in it helps nobody.
|
|
689
735
|
*
|
|
@@ -719,7 +765,7 @@ class AuthClient {
|
|
|
719
765
|
'X-Rekey-User-Token': accessToken,
|
|
720
766
|
});
|
|
721
767
|
}
|
|
722
|
-
/** Revoke one session by id. Idempotent
|
|
768
|
+
/** Revoke one session by id. Idempotent, `{ revoked: false }` if it isn't this user's. */
|
|
723
769
|
revokeSession(accessToken, sessionId) {
|
|
724
770
|
return this.client.send('DELETE', `/api/v1/auth/sessions/${encodeURIComponent(sessionId)}`, undefined, { 'X-Rekey-User-Token': accessToken });
|
|
725
771
|
}
|
|
@@ -727,7 +773,7 @@ class AuthClient {
|
|
|
727
773
|
//
|
|
728
774
|
// The login-step verification is `mfaVerify(...)` above. These manage the
|
|
729
775
|
// user's own TOTP enrollment + step-up challenges. Gated by the
|
|
730
|
-
// Application's `authConfig.mfa` policy
|
|
776
|
+
// Application's `authConfig.mfa` policy, calls return `MFA_NOT_ENABLED`
|
|
731
777
|
// (403) when the policy is "off".
|
|
732
778
|
/** MFA enrollment status for the current user, plus the Application's policy. */
|
|
733
779
|
mfaStatus(accessToken) {
|
|
@@ -738,7 +784,7 @@ class AuthClient {
|
|
|
738
784
|
/**
|
|
739
785
|
* Begin TOTP enrollment: mints a secret (as an `otpauthUrl` for the QR) and
|
|
740
786
|
* 10 single-show backup codes. **Not enrolled until `confirmMfaSetup(...)`.**
|
|
741
|
-
* Only SHA-256 hashes of the backup codes are stored
|
|
787
|
+
* Only SHA-256 hashes of the backup codes are stored, show them once.
|
|
742
788
|
*/
|
|
743
789
|
mfaSetup(accessToken) {
|
|
744
790
|
return this.client.send('POST', '/api/v1/auth/mfa/setup', undefined, {
|
|
@@ -753,7 +799,7 @@ class AuthClient {
|
|
|
753
799
|
}
|
|
754
800
|
/**
|
|
755
801
|
* Verify a TOTP or backup code as a step-up check (does NOT issue a session).
|
|
756
|
-
* Backup codes are single-use
|
|
802
|
+
* Backup codes are single-use, consumed on success. Returns `{ ok }`.
|
|
757
803
|
*/
|
|
758
804
|
mfaChallenge(accessToken, code) {
|
|
759
805
|
return this.client.send('POST', '/api/v1/auth/mfa/challenge', { code }, {
|
|
@@ -780,11 +826,11 @@ class AuthClient {
|
|
|
780
826
|
}
|
|
781
827
|
/**
|
|
782
828
|
* Exchange the provider `code` for a Rekey session. Returns a
|
|
783
|
-
* `SignInOutcome
|
|
829
|
+
* `SignInOutcome`, branch on `mfaRequired` before reading `accessToken`.
|
|
784
830
|
* Verify the `state` CSRF value yourself before calling.
|
|
785
831
|
*/
|
|
786
|
-
completeOAuth(provider, code) {
|
|
787
|
-
return this.client.send('POST', `/api/v1/auth/oauth/${encodeURIComponent(provider)}/callback`, { code });
|
|
832
|
+
completeOAuth(provider, code, options) {
|
|
833
|
+
return this.client.send('POST', `/api/v1/auth/oauth/${encodeURIComponent(provider)}/callback`, { code, ...(options?.device && { device: options.device }) });
|
|
788
834
|
}
|
|
789
835
|
/** List the OAuth providers linked to the current user. */
|
|
790
836
|
listOAuthIdentities(accessToken) {
|
|
@@ -797,7 +843,7 @@ class AuthClient {
|
|
|
797
843
|
return this.client.send('POST', `/api/v1/auth/oauth/${encodeURIComponent(provider)}/link/start`, { state }, { 'X-Rekey-User-Token': accessToken });
|
|
798
844
|
}
|
|
799
845
|
/**
|
|
800
|
-
* Complete an OAuth link
|
|
846
|
+
* Complete an OAuth link, attaches the provider identity to the current
|
|
801
847
|
* user. Refuses on unverified provider emails (account-takeover guard) or
|
|
802
848
|
* when the provider account already belongs to a different user.
|
|
803
849
|
*/
|
|
@@ -883,7 +929,7 @@ class OrganizationsClient {
|
|
|
883
929
|
return this.client.send('GET', `/api/v1/users/me/organizations/${encodeURIComponent(organizationId)}/members${listQuery(page)}`, undefined, { 'X-Rekey-User-Token': accessToken });
|
|
884
930
|
}
|
|
885
931
|
/**
|
|
886
|
-
* Invite a user. Returns the raw token ONCE
|
|
932
|
+
* Invite a user. Returns the raw token ONCE, surface via your own
|
|
887
933
|
* email/share channel. OWNER + ADMIN only.
|
|
888
934
|
*/
|
|
889
935
|
invite(accessToken, organizationId,
|
|
@@ -911,7 +957,7 @@ class OrganizationsClient {
|
|
|
911
957
|
* Remove a member (or self). Refuses removing the last OWNER.
|
|
912
958
|
*
|
|
913
959
|
* Idempotent: `removed` is `false` when the target was not a member (e.g.
|
|
914
|
-
* already removed)
|
|
960
|
+
* already removed), a no-op removal is not an error. Branch on `removed`
|
|
915
961
|
* rather than assuming it is always `true`.
|
|
916
962
|
*/
|
|
917
963
|
removeMember(accessToken, organizationId, targetEndUserId) {
|
|
@@ -919,7 +965,7 @@ class OrganizationsClient {
|
|
|
919
965
|
}
|
|
920
966
|
/**
|
|
921
967
|
* Self-leave. An OWNER cannot leave (payment + benefits are tied to the
|
|
922
|
-
* owner
|
|
968
|
+
* owner, `ORGANIZATION_OWNER_CANNOT_LEAVE`); transfer ownership via support
|
|
923
969
|
* first, or demote yourself to ADMIN if there is another OWNER.
|
|
924
970
|
*/
|
|
925
971
|
leave(accessToken, organizationId) {
|
|
@@ -934,7 +980,7 @@ class OrganizationsClient {
|
|
|
934
980
|
}
|
|
935
981
|
/**
|
|
936
982
|
* Make `organizationId` the active org for this session (member-only).
|
|
937
|
-
* Returns a fresh {accessToken, refreshToken} pair carrying the active org
|
|
983
|
+
* Returns a fresh {accessToken, refreshToken} pair carrying the active org,
|
|
938
984
|
* **store both**. Subsequent entitlement reads (`billing.getEntitlements`)
|
|
939
985
|
* then default to this org's view + shared pool without passing
|
|
940
986
|
* `organizationId` explicitly. The active org survives token refresh until
|
|
@@ -944,7 +990,7 @@ class OrganizationsClient {
|
|
|
944
990
|
return this.client.send('POST', `/api/v1/users/me/organizations/${encodeURIComponent(organizationId)}/switch`, undefined, { 'X-Rekey-User-Token': accessToken });
|
|
945
991
|
}
|
|
946
992
|
/**
|
|
947
|
-
* Clear the active org
|
|
993
|
+
* Clear the active org, switch the session back to the personal pool.
|
|
948
994
|
* Returns a fresh token pair (no active org); **store both**.
|
|
949
995
|
*/
|
|
950
996
|
clearActive(accessToken) {
|
|
@@ -959,7 +1005,7 @@ class LicensesClient {
|
|
|
959
1005
|
/**
|
|
960
1006
|
* Verify a license key + record an activation for this machine. Call
|
|
961
1007
|
* once at app startup; you'll get a deterministic body (`ok=false` for
|
|
962
|
-
* invalid licenses
|
|
1008
|
+
* invalid licenses, never an HTTP error, so your software can loop
|
|
963
1009
|
* on the result without try/catch noise).
|
|
964
1010
|
*
|
|
965
1011
|
* `machineFingerprint` should be a stable identifier you derive client-
|
|
@@ -979,6 +1025,164 @@ class LicensesClient {
|
|
|
979
1025
|
verify(input) {
|
|
980
1026
|
return this.client.send('POST', '/api/v1/licenses/verify', input);
|
|
981
1027
|
}
|
|
1028
|
+
/**
|
|
1029
|
+
* Give back the seat this machine holds; call it before a re-image or on
|
|
1030
|
+
* uninstall so the next machine can verify. Same deterministic body as
|
|
1031
|
+
* `verify`; `released: false` means the machine held no seat.
|
|
1032
|
+
*
|
|
1033
|
+
* @example
|
|
1034
|
+
* ```ts
|
|
1035
|
+
* await rekey.licenses.deactivate({ key, machineFingerprint });
|
|
1036
|
+
* ```
|
|
1037
|
+
*/
|
|
1038
|
+
deactivate(input) {
|
|
1039
|
+
return this.client.send('POST', '/api/v1/licenses/deactivate', input);
|
|
1040
|
+
}
|
|
1041
|
+
/**
|
|
1042
|
+
* The signed-in end-user's own licences, newest first:
|
|
1043
|
+
* `GET /api/v1/users/me/licenses`, authorized by their access token. In an
|
|
1044
|
+
* org-billed Application whose session acts for an organization, that
|
|
1045
|
+
* organization's pooled licences are included too.
|
|
1046
|
+
*
|
|
1047
|
+
* No raw keys: only a hash is stored, so each row carries its display
|
|
1048
|
+
* `keyPrefix`. Needs `billing:read` on a secret key.
|
|
1049
|
+
*
|
|
1050
|
+
* @example
|
|
1051
|
+
* ```ts
|
|
1052
|
+
* const { items } = await rekey.licenses.listMine(accessToken);
|
|
1053
|
+
* const active = items.filter((l) => l.status === 'ACTIVE');
|
|
1054
|
+
* ```
|
|
1055
|
+
*/
|
|
1056
|
+
listMine(accessToken, page) {
|
|
1057
|
+
return this.client.send('GET', `/api/v1/users/me/licenses/${listQuery(page)}`, undefined, {
|
|
1058
|
+
'X-Rekey-User-Token': accessToken,
|
|
1059
|
+
});
|
|
1060
|
+
}
|
|
1061
|
+
}
|
|
1062
|
+
/**
|
|
1063
|
+
* End-users' devices (docs/devices.md), both surfaces.
|
|
1064
|
+
*
|
|
1065
|
+
* `list` / `release` are the SERVER surface: secret key only, addressed by
|
|
1066
|
+
* end-user id, because they read and mutate OTHER users' devices.
|
|
1067
|
+
*
|
|
1068
|
+
* `listMine` / `releaseMine` are the END-USER surface, the one
|
|
1069
|
+
* `DEVICE_LIMIT_REACHED` tells you to offer. They take that user's own access
|
|
1070
|
+
* token and act only on their own devices, so they are what a "your signed-in
|
|
1071
|
+
* machines" screen calls, and what lets a user release a machine themselves
|
|
1072
|
+
* instead of contacting support.
|
|
1073
|
+
*/
|
|
1074
|
+
class DevicesClient {
|
|
1075
|
+
client;
|
|
1076
|
+
constructor(client) {
|
|
1077
|
+
this.client = client;
|
|
1078
|
+
}
|
|
1079
|
+
/** An end-user's devices, newest activity first. Optional `status` filter. */
|
|
1080
|
+
list(endUserId, options = {}) {
|
|
1081
|
+
const q = new URLSearchParams({ endUserId });
|
|
1082
|
+
if (options.status)
|
|
1083
|
+
q.set('status', options.status);
|
|
1084
|
+
if (options.limit !== undefined)
|
|
1085
|
+
q.set('limit', String(options.limit));
|
|
1086
|
+
if (options.offset !== undefined)
|
|
1087
|
+
q.set('offset', String(options.offset));
|
|
1088
|
+
return this.client.send('GET', `/api/v1/devices?${q.toString()}`);
|
|
1089
|
+
}
|
|
1090
|
+
/** Release a device: gives its slot back and revokes every session on it. */
|
|
1091
|
+
release(deviceId, endUserId) {
|
|
1092
|
+
return this.client.send('POST', `/api/v1/devices/${encodeURIComponent(deviceId)}/release`, { endUserId });
|
|
1093
|
+
}
|
|
1094
|
+
/**
|
|
1095
|
+
* The calling end-user's OWN devices, newest activity first.
|
|
1096
|
+
*
|
|
1097
|
+
* `GET /api/v1/users/me/devices`, authorized by the user's access token
|
|
1098
|
+
* rather than by an end-user id. Operator notes (`blockedReason`) and IPs are
|
|
1099
|
+
* not on this surface, which is why it resolves to `EndUserDeviceDto`.
|
|
1100
|
+
*
|
|
1101
|
+
* The device the current session is bound to is the one whose `id` matches
|
|
1102
|
+
* the access token's `dev` claim (see {@link VerifiedAccessTokenClaims}), so
|
|
1103
|
+
* a "your devices" screen can mark "this device" without a second call.
|
|
1104
|
+
*
|
|
1105
|
+
* @example
|
|
1106
|
+
* ```ts
|
|
1107
|
+
* const { items } = await rekey.devices.listMine(accessToken, { status: 'ACTIVE' });
|
|
1108
|
+
* ```
|
|
1109
|
+
*/
|
|
1110
|
+
listMine(accessToken, options = {}) {
|
|
1111
|
+
const q = new URLSearchParams();
|
|
1112
|
+
if (options.status)
|
|
1113
|
+
q.set('status', options.status);
|
|
1114
|
+
if (options.limit !== undefined)
|
|
1115
|
+
q.set('limit', String(options.limit));
|
|
1116
|
+
if (options.offset !== undefined)
|
|
1117
|
+
q.set('offset', String(options.offset));
|
|
1118
|
+
const query = q.toString();
|
|
1119
|
+
return this.client.send('GET', `/api/v1/users/me/devices/${query ? `?${query}` : ''}`, undefined, { 'X-Rekey-User-Token': accessToken });
|
|
1120
|
+
}
|
|
1121
|
+
/**
|
|
1122
|
+
* Release one of the calling end-user's own devices.
|
|
1123
|
+
*
|
|
1124
|
+
* This is the flow `DEVICE_LIMIT_REACHED` names: that refusal carries
|
|
1125
|
+
* `details.limit` and `details.devices` (typed as `DeviceLimitDetails`), so
|
|
1126
|
+
* you can show the user their machines and release one here rather than
|
|
1127
|
+
* leaving them at a dead end.
|
|
1128
|
+
*
|
|
1129
|
+
* Gives the slot back and revokes every session minted on that device,
|
|
1130
|
+
* INCLUDING the current one when it is the same device, so treat a release of
|
|
1131
|
+
* `claims.dev` as a sign-out. Idempotent for an already-released device; a
|
|
1132
|
+
* BLOCKED device refuses with `DEVICE_BLOCKED` (only an operator can unblock).
|
|
1133
|
+
*
|
|
1134
|
+
* @example
|
|
1135
|
+
* ```ts
|
|
1136
|
+
* try {
|
|
1137
|
+
* await rekey.auth.signIn({ email, password, device: { fingerprint } });
|
|
1138
|
+
* } catch (e) {
|
|
1139
|
+
* if (e instanceof RekeyError && e.code === 'DEVICE_LIMIT_REACHED') {
|
|
1140
|
+
* const { devices } = e.details as DeviceLimitDetails;
|
|
1141
|
+
* // …let the user pick one, then, with a token from a session that has one:
|
|
1142
|
+
* await rekey.devices.releaseMine(accessToken, devices[0]!.id);
|
|
1143
|
+
* }
|
|
1144
|
+
* }
|
|
1145
|
+
* ```
|
|
1146
|
+
*/
|
|
1147
|
+
releaseMine(accessToken, deviceId) {
|
|
1148
|
+
return this.client.send('DELETE', `/api/v1/users/me/devices/${encodeURIComponent(deviceId)}`, undefined, { 'X-Rekey-User-Token': accessToken });
|
|
1149
|
+
}
|
|
1150
|
+
}
|
|
1151
|
+
/**
|
|
1152
|
+
* Server-side end-user lookup (secret key only). `/users/me` answers "who is
|
|
1153
|
+
* this token"; these answer "who is this id / email" for a backend that holds
|
|
1154
|
+
* no token.
|
|
1155
|
+
*/
|
|
1156
|
+
class UsersClient {
|
|
1157
|
+
client;
|
|
1158
|
+
constructor(client) {
|
|
1159
|
+
this.client = client;
|
|
1160
|
+
}
|
|
1161
|
+
/** Exact, case-insensitive email match in the calling Application. Throws END_USER_NOT_FOUND. */
|
|
1162
|
+
getByEmail(email) {
|
|
1163
|
+
return this.client.send('GET', `/api/v1/users?email=${encodeURIComponent(email)}`);
|
|
1164
|
+
}
|
|
1165
|
+
/** By id, scoped to the calling Application. Throws END_USER_NOT_FOUND. */
|
|
1166
|
+
get(endUserId) {
|
|
1167
|
+
return this.client.send('GET', `/api/v1/users/${encodeURIComponent(endUserId)}`);
|
|
1168
|
+
}
|
|
1169
|
+
/**
|
|
1170
|
+
* Import up to 500 users from another auth system in one call. Password
|
|
1171
|
+
* hashes (argon2id or bcrypt) are stored as given and verified as-is at
|
|
1172
|
+
* sign-in; bcrypt is upgraded to argon2id on first success. Existing
|
|
1173
|
+
* addresses are skipped, never updated.
|
|
1174
|
+
*
|
|
1175
|
+
* @example
|
|
1176
|
+
* ```ts
|
|
1177
|
+
* const { created, skipped } = await rekey.users.import([
|
|
1178
|
+
* { email: 'a@example.com', passwordHash: '$2b$10$…', emailVerified: true },
|
|
1179
|
+
* { email: 'b@example.com', oauthIdentities: [{ provider: 'google', providerAccountId: '1234' }] },
|
|
1180
|
+
* ]);
|
|
1181
|
+
* ```
|
|
1182
|
+
*/
|
|
1183
|
+
import(users) {
|
|
1184
|
+
return this.client.send('POST', '/api/v1/users/import', { users });
|
|
1185
|
+
}
|
|
982
1186
|
}
|
|
983
1187
|
class UsageClient {
|
|
984
1188
|
client;
|
|
@@ -1011,6 +1215,60 @@ class UsageClient {
|
|
|
1011
1215
|
params.set('organizationId', input.organizationId);
|
|
1012
1216
|
return this.client.send('GET', `/api/v1/usage/aggregate?${params.toString()}`);
|
|
1013
1217
|
}
|
|
1218
|
+
/**
|
|
1219
|
+
* The signed-in end-user's included quota, usage and remaining units this
|
|
1220
|
+
* period, per meter (or one meter with `{ meter }`). Computed by the code
|
|
1221
|
+
* `record` enforces with: a record of more than `remaining` is the one that
|
|
1222
|
+
* is refused (402 `USAGE_QUOTA_EXCEEDED`) or, on a priced meter, charged.
|
|
1223
|
+
*
|
|
1224
|
+
* Reads the personal quota, or the active organization's in an Application
|
|
1225
|
+
* that bills organizations; `{ organizationId }` (member-only) picks one.
|
|
1226
|
+
*
|
|
1227
|
+
* @example
|
|
1228
|
+
* ```ts
|
|
1229
|
+
* const { meters } = await rekey.usage.getRemaining(accessToken, { meter: 'api_calls' });
|
|
1230
|
+
* if (meters[0].remaining === 0) showUpgradePrompt();
|
|
1231
|
+
* ```
|
|
1232
|
+
*/
|
|
1233
|
+
getRemaining(accessToken, opts) {
|
|
1234
|
+
const q = new URLSearchParams();
|
|
1235
|
+
if (opts?.meter)
|
|
1236
|
+
q.set('meter', opts.meter);
|
|
1237
|
+
if (opts?.organizationId)
|
|
1238
|
+
q.set('organizationId', opts.organizationId);
|
|
1239
|
+
const qs = q.toString() ? `?${q.toString()}` : '';
|
|
1240
|
+
return this.client.send('GET', `/api/v1/usage/remaining${qs}`, undefined, {
|
|
1241
|
+
'X-Rekey-User-Token': accessToken,
|
|
1242
|
+
});
|
|
1243
|
+
}
|
|
1244
|
+
/**
|
|
1245
|
+
* The same answer as `getRemaining`, for a subject you name instead of one
|
|
1246
|
+
* whose token you hold: `{ endUserId }`, `{ organizationId }`, or both to
|
|
1247
|
+
* read the organization as that member. Secret key only.
|
|
1248
|
+
*/
|
|
1249
|
+
getRemainingFor(subject, opts) {
|
|
1250
|
+
const q = new URLSearchParams();
|
|
1251
|
+
if (subject.endUserId)
|
|
1252
|
+
q.set('endUserId', subject.endUserId);
|
|
1253
|
+
if (subject.organizationId)
|
|
1254
|
+
q.set('organizationId', subject.organizationId);
|
|
1255
|
+
if (opts?.meter)
|
|
1256
|
+
q.set('meter', opts.meter);
|
|
1257
|
+
return this.client.send('GET', `/api/v1/usage/remaining/for-user?${q.toString()}`);
|
|
1258
|
+
}
|
|
1259
|
+
/**
|
|
1260
|
+
* The Application's usage meters: the slugs `record` takes, their units,
|
|
1261
|
+
* whether each accepts records, and its fallback credit price. Secret key.
|
|
1262
|
+
*/
|
|
1263
|
+
listMeters(opts) {
|
|
1264
|
+
const q = new URLSearchParams();
|
|
1265
|
+
if (opts?.limit !== undefined)
|
|
1266
|
+
q.set('limit', String(opts.limit));
|
|
1267
|
+
if (opts?.offset !== undefined)
|
|
1268
|
+
q.set('offset', String(opts.offset));
|
|
1269
|
+
const qs = q.toString() ? `?${q.toString()}` : '';
|
|
1270
|
+
return this.client.send('GET', `/api/v1/usage/meters${qs}`);
|
|
1271
|
+
}
|
|
1014
1272
|
}
|
|
1015
1273
|
function creditSubjectQuery(subject) {
|
|
1016
1274
|
const p = new URLSearchParams();
|
|
@@ -1021,11 +1279,11 @@ function creditSubjectQuery(subject) {
|
|
|
1021
1279
|
return p;
|
|
1022
1280
|
}
|
|
1023
1281
|
/**
|
|
1024
|
-
* Prepaid credits
|
|
1282
|
+
* Prepaid credits, the "lead pack" / pay-as-you-go drawdown model. The
|
|
1025
1283
|
* customer's backend grants credits (by selling a CREDIT-kind plan, which
|
|
1026
1284
|
* grants automatically on payment) and draws them down per unit consumed.
|
|
1027
1285
|
*
|
|
1028
|
-
* All calls are server-to-server (secret key) and scoped to a `CreditSubject
|
|
1286
|
+
* All calls are server-to-server (secret key) and scoped to a `CreditSubject`,
|
|
1029
1287
|
* an end-user's personal balance, or an organization's shared pool.
|
|
1030
1288
|
*/
|
|
1031
1289
|
class CreditsClient {
|
|
@@ -1042,7 +1300,7 @@ class CreditsClient {
|
|
|
1042
1300
|
* `code: "CREDITS_INSUFFICIENT"` (HTTP 402) when the balance is too low.
|
|
1043
1301
|
*
|
|
1044
1302
|
* Pass `idempotencyKey` (e.g. the lead id) so a retried call never
|
|
1045
|
-
* double-charges
|
|
1303
|
+
* double-charges, a repeat returns the original result with `applied: false`.
|
|
1046
1304
|
*/
|
|
1047
1305
|
consume(input) {
|
|
1048
1306
|
return this.client.send('POST', '/api/v1/credits/consume', input);
|
|
@@ -1060,18 +1318,61 @@ class CreditsClient {
|
|
|
1060
1318
|
params.set('offset', String(offset));
|
|
1061
1319
|
return this.client.send('GET', `/api/v1/credits/ledger?${params.toString()}`);
|
|
1062
1320
|
}
|
|
1321
|
+
/**
|
|
1322
|
+
* Grant credits to an end-user or organization pool with the Application
|
|
1323
|
+
* key. Needs a key minted with the elevated `credits:grant` scope named:
|
|
1324
|
+
* `*` does not include it, so a default key gets 403
|
|
1325
|
+
* `API_KEY_SCOPE_INSUFFICIENT`.
|
|
1326
|
+
*
|
|
1327
|
+
* `idempotencyKey` is required; a repeat returns the original entry with
|
|
1328
|
+
* `applied: false` and grants nothing. `amount` is 1 to 1,000,000 per call.
|
|
1329
|
+
*
|
|
1330
|
+
* @example
|
|
1331
|
+
* ```ts
|
|
1332
|
+
* await rekey.credits.grant({ endUserId, amount: 500, idempotencyKey: `referral:${referralId}` });
|
|
1333
|
+
* ```
|
|
1334
|
+
*/
|
|
1335
|
+
grant(input) {
|
|
1336
|
+
return this.client.send('POST', '/api/v1/credits/grant', input);
|
|
1337
|
+
}
|
|
1338
|
+
/**
|
|
1339
|
+
* The signed-in end-user's own credit ledger, newest first (the active
|
|
1340
|
+
* organization's pool in an Application that bills organizations, or the one
|
|
1341
|
+
* `{ organizationId }` names, member-only). Entries carry no `metadata`.
|
|
1342
|
+
*/
|
|
1343
|
+
listMyLedger(accessToken, opts) {
|
|
1344
|
+
const q = new URLSearchParams();
|
|
1345
|
+
if (opts?.organizationId)
|
|
1346
|
+
q.set('organizationId', opts.organizationId);
|
|
1347
|
+
if (opts?.limit !== undefined)
|
|
1348
|
+
q.set('limit', String(opts.limit));
|
|
1349
|
+
if (opts?.offset !== undefined)
|
|
1350
|
+
q.set('offset', String(opts.offset));
|
|
1351
|
+
const qs = q.toString() ? `?${q.toString()}` : '';
|
|
1352
|
+
return this.client.send('GET', `/api/v1/credits/me/ledger${qs}`, undefined, {
|
|
1353
|
+
'X-Rekey-User-Token': accessToken,
|
|
1354
|
+
});
|
|
1355
|
+
}
|
|
1356
|
+
}
|
|
1357
|
+
/**
|
|
1358
|
+
* `?include=` for the current-user routes, deduplicated. The API ignores order
|
|
1359
|
+
* and duplicates too; this keeps the URL short and stable for caching.
|
|
1360
|
+
*/
|
|
1361
|
+
function meIncludeQuery(include) {
|
|
1362
|
+
const values = [...new Set(include ?? [])];
|
|
1363
|
+
return values.length > 0 ? `?include=${values.join(',')}` : '';
|
|
1063
1364
|
}
|
|
1064
1365
|
/**
|
|
1065
1366
|
* Load Node's crypto lazily, from an ESM module.
|
|
1066
1367
|
*
|
|
1067
1368
|
* This is deliberately `createRequire` and not a bare `require`. The package is
|
|
1068
1369
|
* `"type": "module"` with ESM-only `exports`, so in the built `dist` a bare
|
|
1069
|
-
* `require` is simply not defined
|
|
1370
|
+
* `require` is simply not defined, `verifyWebhookSignature` and the RS256 path
|
|
1070
1371
|
* of `verifyAccessToken` threw `ReferenceError: require is not defined` for
|
|
1071
1372
|
* every consumer who installed from npm, in every published version.
|
|
1072
1373
|
*
|
|
1073
1374
|
* It went unnoticed because `node -e` defines `globalThis.require`, so the
|
|
1074
|
-
* failure does not reproduce in a one-liner
|
|
1375
|
+
* failure does not reproduce in a one-liner, only in a real `.mjs`, `.cjs`, or
|
|
1075
1376
|
* `"type": "module"` package, which is to say only in real use. The tests
|
|
1076
1377
|
* exercise the TypeScript source rather than the built ESM artifact, so they
|
|
1077
1378
|
* never saw it either.
|
|
@@ -1082,7 +1383,7 @@ class CreditsClient {
|
|
|
1082
1383
|
*/
|
|
1083
1384
|
function nodeCrypto() {
|
|
1084
1385
|
// `process.getBuiltinModule` (Node 22.3+, and this package's floor is 22)
|
|
1085
|
-
// resolves a builtin synchronously with NO static import
|
|
1386
|
+
// resolves a builtin synchronously with NO static import, which is the
|
|
1086
1387
|
// whole point. The previous fix used `createRequire`, correct for CJS
|
|
1087
1388
|
// interop but imported from 'node:module' at module scope, so merely
|
|
1088
1389
|
// IMPORTING the package failed on edge runtimes with
|
|
@@ -1105,7 +1406,7 @@ function nodeCrypto() {
|
|
|
1105
1406
|
* default 300) AND (b) the signature matches a constant-time compare.
|
|
1106
1407
|
*
|
|
1107
1408
|
* Use against the `X-Rekey-Signature` header and the raw request body
|
|
1108
|
-
* BYTES (not the parsed JSON
|
|
1409
|
+
* BYTES (not the parsed JSON, any reserialization breaks the HMAC).
|
|
1109
1410
|
*
|
|
1110
1411
|
* @example
|
|
1111
1412
|
* ```ts
|
|
@@ -1150,7 +1451,7 @@ export function verifyWebhookSignature(args) {
|
|
|
1150
1451
|
return timingSafeEqual(a, b);
|
|
1151
1452
|
}
|
|
1152
1453
|
const jwksCache = new Map();
|
|
1153
|
-
/** @internal Test hook
|
|
1454
|
+
/** @internal Test hook, drop cached JWKS responses. */
|
|
1154
1455
|
export function _clearJwksCacheForTests() {
|
|
1155
1456
|
jwksCache.clear();
|
|
1156
1457
|
}
|
|
@@ -1214,14 +1515,14 @@ async function loadJwks(options, forceRefetch) {
|
|
|
1214
1515
|
return jwks;
|
|
1215
1516
|
}
|
|
1216
1517
|
/**
|
|
1217
|
-
* Verify an end-user ACCESS token **offline
|
|
1518
|
+
* Verify an end-user ACCESS token **offline**, no round-trip to the Rekey
|
|
1218
1519
|
* API. Works only for Applications that opted into RS256 tokens
|
|
1219
1520
|
* (`authConfig.tokenAlg = "RS256"`, Panel → Application → Auth); the default
|
|
1220
1521
|
* HS256 tokens are symmetric and can only be verified by the API itself
|
|
1221
1522
|
* (use `rekey.auth.getCurrentUser(token)` for those).
|
|
1222
1523
|
*
|
|
1223
1524
|
* Checks performed (same posture as the API's verifier):
|
|
1224
|
-
* - header `alg` must be `RS256` and `kid` must exist in the JWKS
|
|
1525
|
+
* - header `alg` must be `RS256` and `kid` must exist in the JWKS,
|
|
1225
1526
|
* a strict allowlist, immune to alg-confusion;
|
|
1226
1527
|
* - RSA-SHA256 signature against that public key;
|
|
1227
1528
|
* - `exp` in the future, `typ === "eu_access"` (refresh/MFA/MCP tokens
|
|
@@ -1231,6 +1532,11 @@ async function loadJwks(options, forceRefetch) {
|
|
|
1231
1532
|
* user deletion. The 15-minute access lifetime bounds both; for hard
|
|
1232
1533
|
* revocation guarantees keep using `auth.getCurrentUser`.
|
|
1233
1534
|
*
|
|
1535
|
+
* Nor can it see any server-side revocation: a locally verified token stays
|
|
1536
|
+
* valid until it expires, even after sign-out everywhere, a password change,
|
|
1537
|
+
* a session revoke or a device release. Call the API when immediate
|
|
1538
|
+
* revocation matters.
|
|
1539
|
+
*
|
|
1234
1540
|
* Node-only (uses `node:crypto`). Returns the verified claims; throws
|
|
1235
1541
|
* `RekeyError` on any failure.
|
|
1236
1542
|
*
|
|
@@ -1253,10 +1559,10 @@ async function loadJwks(options, forceRefetch) {
|
|
|
1253
1559
|
* it moved inside: the shortest correct path should not be the one nobody
|
|
1254
1560
|
* takes.
|
|
1255
1561
|
*
|
|
1256
|
-
* @throws {RekeyError} `TOKEN_ALG_NOT_RS256
|
|
1257
|
-
* @throws {RekeyError} `TOKEN_KID_UNKNOWN
|
|
1258
|
-
* @throws {RekeyError} `USER_TOKEN_EXPIRED
|
|
1259
|
-
* @throws {RekeyError} `USER_TOKEN_INVALID
|
|
1562
|
+
* @throws {RekeyError} `TOKEN_ALG_NOT_RS256`, token is HS256 (app hasn't opted in) or another alg.
|
|
1563
|
+
* @throws {RekeyError} `TOKEN_KID_UNKNOWN`, `kid` not in the JWKS (forged, or key deleted).
|
|
1564
|
+
* @throws {RekeyError} `USER_TOKEN_EXPIRED`, `exp` passed; refresh the session.
|
|
1565
|
+
* @throws {RekeyError} `USER_TOKEN_INVALID`, malformed, bad signature, or wrong `typ`.
|
|
1260
1566
|
*/
|
|
1261
1567
|
export async function verifyAccessToken(token, options) {
|
|
1262
1568
|
const invalid = (message) => new RekeyError({
|
|
@@ -1278,7 +1584,7 @@ export async function verifyAccessToken(token, options) {
|
|
|
1278
1584
|
catch {
|
|
1279
1585
|
throw invalid('The token header/payload is not valid base64url JSON.');
|
|
1280
1586
|
}
|
|
1281
|
-
// Strict alg allowlist
|
|
1587
|
+
// Strict alg allowlist, this helper verifies RS256 ONLY. HS256 tokens are
|
|
1282
1588
|
// symmetric (the verifying key can also MINT tokens), so they are never
|
|
1283
1589
|
// verified client-side.
|
|
1284
1590
|
if (header.alg !== 'RS256') {
|
|
@@ -1308,10 +1614,10 @@ export async function verifyAccessToken(token, options) {
|
|
|
1308
1614
|
statusCode: 401,
|
|
1309
1615
|
});
|
|
1310
1616
|
}
|
|
1311
|
-
// Lazy-load node:crypto (same posture as verifyWebhookSignature)
|
|
1617
|
+
// Lazy-load node:crypto (same posture as verifyWebhookSignature), keeps
|
|
1312
1618
|
// the import graph clean for bundlers that tree-shake this helper away.
|
|
1313
1619
|
const { createPublicKey, verify } = nodeCrypto();
|
|
1314
|
-
let signatureOk
|
|
1620
|
+
let signatureOk;
|
|
1315
1621
|
try {
|
|
1316
1622
|
const publicKey = createPublicKey({ key: { kty: jwk.kty, n: jwk.n, e: jwk.e }, format: 'jwk' });
|
|
1317
1623
|
signatureOk = verify('sha256', Buffer.from(`${parts[0]}.${parts[1]}`, 'utf8'), publicKey, Buffer.from(parts[2], 'base64url'));
|
|
@@ -1321,7 +1627,7 @@ export async function verifyAccessToken(token, options) {
|
|
|
1321
1627
|
}
|
|
1322
1628
|
if (!signatureOk)
|
|
1323
1629
|
throw invalid('The token signature does not verify against the JWKS key.');
|
|
1324
|
-
// Claims
|
|
1630
|
+
// Claims, mirror the API's verifier: typ is load-bearing, exp is enforced.
|
|
1325
1631
|
if (payload.typ !== 'eu_access') {
|
|
1326
1632
|
throw invalid(`Token typ is ${JSON.stringify(payload.typ)}, expected "eu_access".`);
|
|
1327
1633
|
}
|
|
@@ -1330,7 +1636,7 @@ export async function verifyAccessToken(token, options) {
|
|
|
1330
1636
|
}
|
|
1331
1637
|
// Bind the token to ONE Application. The signing key is deployment-wide and
|
|
1332
1638
|
// `eu_access` carries no `iss`/`aud`, so a token minted for a different
|
|
1333
|
-
// Application on the same deployment is cryptographically valid here
|
|
1639
|
+
// Application on the same deployment is cryptographically valid here, on a
|
|
1334
1640
|
// multi-app self-host that means accepting someone else's end-user as your
|
|
1335
1641
|
// own. The API does compare this server-side; the SDK left it to the caller
|
|
1336
1642
|
// and documented it as a follow-up step, which made the shortest correct
|
|
@@ -1355,12 +1661,17 @@ class BillingClient {
|
|
|
1355
1661
|
this.client = client;
|
|
1356
1662
|
}
|
|
1357
1663
|
/**
|
|
1358
|
-
* List the calling Application's active plans. Public
|
|
1664
|
+
* List the calling Application's active plans. Public, pricing pages
|
|
1359
1665
|
* typically render straight from this. Application API key only; no
|
|
1360
1666
|
* user JWT needed.
|
|
1361
1667
|
*
|
|
1362
|
-
* `amount` is in the smallest currency unit (cents/paise/sen)
|
|
1668
|
+
* `amount` is in the smallest currency unit (cents/paise/sen), never
|
|
1363
1669
|
* a float. Format on display: `${amount / 100} ${currency}`.
|
|
1670
|
+
*
|
|
1671
|
+
* `checkout.ready` is false when a buyer sent to checkout for the plan would
|
|
1672
|
+
* be refused, so a pricing page can hide it. Why is on the operator plan
|
|
1673
|
+
* list, not here. Keep the free tier in: with no provider connected it reads
|
|
1674
|
+
* `ready: false` and still applies to every signed-in user.
|
|
1364
1675
|
*/
|
|
1365
1676
|
getPlans(page) {
|
|
1366
1677
|
return this.client.send('GET', `/api/v1/billing/plans${listQuery(page)}`);
|
|
@@ -1372,7 +1683,7 @@ class BillingClient {
|
|
|
1372
1683
|
* Pass the user's access token (the SDK puts it in `X-Rekey-User-Token`).
|
|
1373
1684
|
*
|
|
1374
1685
|
* `opts.includeEnded` falls back to the most recent CANCELED/EXPIRED
|
|
1375
|
-
* subscription **only when the answer would otherwise be null
|
|
1686
|
+
* subscription **only when the answer would otherwise be null**, for a
|
|
1376
1687
|
* billing page that has to tell a former subscriber what they were on and
|
|
1377
1688
|
* when it ended, rather than showing them the same blank state as somebody
|
|
1378
1689
|
* who never subscribed. It can never replace a live subscription, so it is
|
|
@@ -1397,13 +1708,22 @@ class BillingClient {
|
|
|
1397
1708
|
/**
|
|
1398
1709
|
* Start a hosted-checkout session. Returns the URL to redirect the user
|
|
1399
1710
|
* to and the local PENDING Subscription row. Subscription activation
|
|
1400
|
-
* happens via the provider's webhook
|
|
1711
|
+
* happens via the provider's webhook, not synchronously here.
|
|
1401
1712
|
*
|
|
1402
1713
|
* Pass `couponCode` to apply a discount. The whole checkout fails if the
|
|
1403
1714
|
* coupon doesn't validate (typed `RekeyError` with the precise reason).
|
|
1404
1715
|
*
|
|
1716
|
+
* A buyer who has already used their free trial is refused with
|
|
1717
|
+
* `BILLING_TRIAL_ALREADY_USED` (409). The escape hatch is
|
|
1718
|
+
* `allowWithoutTrial: true` AND a fresh `Idempotency-Key`, but send it only
|
|
1719
|
+
* after the buyer has been told they are paying today. Read
|
|
1720
|
+
* {@link getTrialEligibility} and render the paid price instead of retrying
|
|
1721
|
+
* blindly: a buyer who merely abandoned a trial checkout still reads
|
|
1722
|
+
* `eligible: true`, and acknowledging on their behalf charges them today for
|
|
1723
|
+
* the trial the next checkout was about to grant.
|
|
1724
|
+
*
|
|
1405
1725
|
* If the Application's billing subject is **org** (Panel → Application →
|
|
1406
|
-
* Billing → Subject), an individual can't hold a subscription
|
|
1726
|
+
* Billing → Subject), an individual can't hold a subscription, you MUST
|
|
1407
1727
|
* pass `organizationId` of a team the user owns/admins. Omitting it throws
|
|
1408
1728
|
* `RekeyError` `code: "BILLING_ORGANIZATION_REQUIRED"`.
|
|
1409
1729
|
*
|
|
@@ -1423,6 +1743,83 @@ class BillingClient {
|
|
|
1423
1743
|
'X-Rekey-User-Token': accessToken,
|
|
1424
1744
|
});
|
|
1425
1745
|
}
|
|
1746
|
+
/**
|
|
1747
|
+
* Put the calling end-user on the Application's free tier
|
|
1748
|
+
* (`billingConfig.defaultPlanSlug`). No payment provider is involved and none
|
|
1749
|
+
* needs to be configured: the plan costs nothing.
|
|
1750
|
+
*
|
|
1751
|
+
* This is how a freemium product hands a new signup their included credits,
|
|
1752
|
+
* licence or quota. `defaultPlanSlug` alone covers only the read-time half
|
|
1753
|
+
* (feature flags and included usage); CREDIT and LICENSE entitlements are
|
|
1754
|
+
* stateful and need a real subscription, which is what this creates.
|
|
1755
|
+
*
|
|
1756
|
+
* **Idempotent**, and the answer says which happened: `activated: true` is a
|
|
1757
|
+
* first activation (201, `subscription.activated` emitted), `activated:
|
|
1758
|
+
* false` means they were already entitled and nothing was written,
|
|
1759
|
+
* re-provisioned or re-announced.
|
|
1760
|
+
*
|
|
1761
|
+
* Pass `organizationId` on an org-billed Application; the caller must be an
|
|
1762
|
+
* OWNER or ADMIN of it. Omit it and the session's active organization is used.
|
|
1763
|
+
*
|
|
1764
|
+
* @throws {RekeyError} `BILLING_NO_FREE_PLAN` (404) when the Application
|
|
1765
|
+
* nominates no default plan; `BILLING_FREE_PLAN_NOT_FREE` (409) when that
|
|
1766
|
+
* plan charges money, use {@link createCheckout} instead;
|
|
1767
|
+
* `BILLING_FREE_TIER_ALREADY_CLAIMED` (409) when the plan grants credits or a
|
|
1768
|
+
* licence and this caller already claimed it for a different beneficiary.
|
|
1769
|
+
*
|
|
1770
|
+
* @example
|
|
1771
|
+
* ```ts
|
|
1772
|
+
* const { subscription, activated } = await rekey.billing.subscribe(accessToken);
|
|
1773
|
+
* if (activated) welcomeWithStarterCredits(subscription);
|
|
1774
|
+
* ```
|
|
1775
|
+
*/
|
|
1776
|
+
async subscribe(accessToken, input = {}) {
|
|
1777
|
+
const { data, status } = await this.client.sendWithStatus('POST', '/api/v1/billing/subscribe', { ...(input.organizationId !== undefined && { organizationId: input.organizationId }) }, { 'X-Rekey-User-Token': accessToken });
|
|
1778
|
+
// 201 = activated now, 200 = already on it. The body is the same row in
|
|
1779
|
+
// both cases, so the status is the only place this distinction lives.
|
|
1780
|
+
return { subscription: data, activated: status === 201 };
|
|
1781
|
+
}
|
|
1782
|
+
/**
|
|
1783
|
+
* Whether THIS buyer may start each plan's free trial, under the
|
|
1784
|
+
* Application's `trialPolicy`.
|
|
1785
|
+
*
|
|
1786
|
+
* Read this before offering a trial: a buyer who is not eligible should be
|
|
1787
|
+
* shown the paid price, not a trial that checkout refuses with
|
|
1788
|
+
* `BILLING_TRIAL_ALREADY_USED`. Feed the result straight into
|
|
1789
|
+
* `<PricingTable trialEligibility={…}>` from `@rekey.dev/react`.
|
|
1790
|
+
*
|
|
1791
|
+
* **Advisory.** The authoritative decision is taken under a lock at checkout,
|
|
1792
|
+
* so two tabs can both read `eligible: true` and only one gets the trial.
|
|
1793
|
+
* Treat a 409 at checkout as normal, not as a contradiction.
|
|
1794
|
+
*
|
|
1795
|
+
* **Provider-dependent.** `PLAN_TRIAL_MISCONFIGURED` can be the resolved
|
|
1796
|
+
* provider's answer, so the response echoes `provider`; re-read this when the
|
|
1797
|
+
* buyer changes processor. Pass `country` (ISO 3166-1 alpha-2) to steer the
|
|
1798
|
+
* geo router the way {@link getProviders} does.
|
|
1799
|
+
*
|
|
1800
|
+
* @example
|
|
1801
|
+
* ```ts
|
|
1802
|
+
* const { items, policy } = await rekey.billing.getTrialEligibility(accessToken);
|
|
1803
|
+
* const pro = items.find((i) => i.planSlug === 'pro');
|
|
1804
|
+
* const label = pro?.eligible ? `Start ${pro.trialDays} days free` : 'Subscribe';
|
|
1805
|
+
* ```
|
|
1806
|
+
*/
|
|
1807
|
+
getTrialEligibility(accessToken, opts) {
|
|
1808
|
+
const q = new URLSearchParams();
|
|
1809
|
+
if (opts?.organizationId)
|
|
1810
|
+
q.set('organizationId', opts.organizationId);
|
|
1811
|
+
if (opts?.planSlug)
|
|
1812
|
+
q.set('planSlug', opts.planSlug);
|
|
1813
|
+
if (opts?.limit !== undefined)
|
|
1814
|
+
q.set('limit', String(opts.limit));
|
|
1815
|
+
if (opts?.offset !== undefined)
|
|
1816
|
+
q.set('offset', String(opts.offset));
|
|
1817
|
+
const query = q.toString();
|
|
1818
|
+
return this.client.send('GET', `/api/v1/billing/trial-eligibility${query ? `?${query}` : ''}`, undefined, {
|
|
1819
|
+
'X-Rekey-User-Token': accessToken,
|
|
1820
|
+
...(opts?.country ? { 'x-country': opts.country.toUpperCase() } : {}),
|
|
1821
|
+
});
|
|
1822
|
+
}
|
|
1426
1823
|
/**
|
|
1427
1824
|
* Validate a coupon for the current user against a plan, *without*
|
|
1428
1825
|
* applying it. Render "$50 off" on a pricing page before submit.
|
|
@@ -1440,7 +1837,7 @@ class BillingClient {
|
|
|
1440
1837
|
/**
|
|
1441
1838
|
* List the billing providers configured + enabled for this Application,
|
|
1442
1839
|
* in the order the geo router would prefer them. Forward the end-user's
|
|
1443
|
-
* `country` (ISO 3166-1 alpha-2) when you have it
|
|
1840
|
+
* `country` (ISO 3166-1 alpha-2) when you have it, the panel/SDK will
|
|
1444
1841
|
* surface India-specific providers (Razorpay) for IN-country users, etc.
|
|
1445
1842
|
*
|
|
1446
1843
|
* Returns the resolved country (echoed back from the server's view of
|
|
@@ -1454,7 +1851,7 @@ class BillingClient {
|
|
|
1454
1851
|
return this.client.send('GET', '/api/v1/billing/providers', undefined, headers);
|
|
1455
1852
|
}
|
|
1456
1853
|
/**
|
|
1457
|
-
* Resolve the calling end-user's current entitlements
|
|
1854
|
+
* Resolve the calling end-user's current entitlements, feature flags +
|
|
1458
1855
|
* limits, the live credit balance, and the raw entitlement list, unioned
|
|
1459
1856
|
* across their active subscriptions (and subscriptions of orgs they belong
|
|
1460
1857
|
* to). Pass `{ organizationId }` (member-only) for that org's view + shared
|
|
@@ -1474,10 +1871,68 @@ class BillingClient {
|
|
|
1474
1871
|
'X-Rekey-User-Token': accessToken,
|
|
1475
1872
|
});
|
|
1476
1873
|
}
|
|
1874
|
+
/**
|
|
1875
|
+
* The same union as `getEntitlements`, for an end-user you name rather than
|
|
1876
|
+
* one whose token you hold. Secret key only, for a licence server, a
|
|
1877
|
+
* support tool or a batch job.
|
|
1878
|
+
*
|
|
1879
|
+
* @example
|
|
1880
|
+
* ```ts
|
|
1881
|
+
* const { features } = await rekey.billing.getEntitlementsFor(endUserId);
|
|
1882
|
+
* if (features.max_devices !== undefined) capDevices(features.max_devices);
|
|
1883
|
+
* ```
|
|
1884
|
+
*/
|
|
1885
|
+
getEntitlementsFor(endUserId, opts) {
|
|
1886
|
+
const q = new URLSearchParams({ endUserId });
|
|
1887
|
+
if (opts?.organizationId)
|
|
1888
|
+
q.set('organizationId', opts.organizationId);
|
|
1889
|
+
return this.client.send('GET', `/api/v1/billing/entitlements/for-user?${q.toString()}`);
|
|
1890
|
+
}
|
|
1891
|
+
/**
|
|
1892
|
+
* One feature for the calling end-user: `{ key, granted, value }`, where
|
|
1893
|
+
* `value` is what `features[key]` holds in `getEntitlements` (null when
|
|
1894
|
+
* nothing grants it) and `granted` is `Boolean(value)`. The subject is the
|
|
1895
|
+
* one `getCurrentUser(token, { include: ['entitlements'] })` resolves, or
|
|
1896
|
+
* the organization you pass (member-only).
|
|
1897
|
+
*
|
|
1898
|
+
* @example
|
|
1899
|
+
* ```ts
|
|
1900
|
+
* const { value } = await rekey.billing.getFeature(accessToken, 'projects');
|
|
1901
|
+
* if (typeof value === 'number' && count >= value) throw new LimitReached();
|
|
1902
|
+
* ```
|
|
1903
|
+
*/
|
|
1904
|
+
getFeature(accessToken, key, opts) {
|
|
1905
|
+
const qs = opts?.organizationId ? `?organizationId=${encodeURIComponent(opts.organizationId)}` : '';
|
|
1906
|
+
return this.client.send('GET', `/api/v1/billing/entitlements/features/${encodeURIComponent(key)}${qs}`, undefined, { 'X-Rekey-User-Token': accessToken });
|
|
1907
|
+
}
|
|
1908
|
+
/**
|
|
1909
|
+
* Whether the calling end-user holds a feature: `Boolean(value)`, the test
|
|
1910
|
+
* `if (features[key])` makes, so a false flag, a 0 limit and an unknown key
|
|
1911
|
+
* are all `false`. Use `getFeature` to read a numeric limit.
|
|
1912
|
+
*
|
|
1913
|
+
* @example
|
|
1914
|
+
* ```ts
|
|
1915
|
+
* if (!(await rekey.billing.hasFeature(accessToken, 'reports'))) throw new Forbidden();
|
|
1916
|
+
* ```
|
|
1917
|
+
*/
|
|
1918
|
+
async hasFeature(accessToken, key, opts) {
|
|
1919
|
+
return (await this.getFeature(accessToken, key, opts)).granted;
|
|
1920
|
+
}
|
|
1921
|
+
/** `getFeature` for an end-user you name. Secret key only, like `getEntitlementsFor`. */
|
|
1922
|
+
getFeatureFor(endUserId, key, opts) {
|
|
1923
|
+
const q = new URLSearchParams({ endUserId });
|
|
1924
|
+
if (opts?.organizationId)
|
|
1925
|
+
q.set('organizationId', opts.organizationId);
|
|
1926
|
+
return this.client.send('GET', `/api/v1/billing/entitlements/for-user/features/${encodeURIComponent(key)}?${q.toString()}`);
|
|
1927
|
+
}
|
|
1928
|
+
/** `hasFeature` for an end-user you name. Secret key only. */
|
|
1929
|
+
async hasFeatureFor(endUserId, key, opts) {
|
|
1930
|
+
return (await this.getFeatureFor(endUserId, key, opts)).granted;
|
|
1931
|
+
}
|
|
1477
1932
|
/**
|
|
1478
1933
|
* Cancel the calling end-user's current subscription.
|
|
1479
1934
|
*
|
|
1480
|
-
* Defaults to cancelling **at period end
|
|
1935
|
+
* Defaults to cancelling **at period end**, the user keeps what they paid
|
|
1481
1936
|
* for until the period they already bought runs out. A provider-backed
|
|
1482
1937
|
* subscription therefore stays ACTIVE with `cancelAt` set, and the provider
|
|
1483
1938
|
* webhook is what eventually terminates it; read `cancelAt` on the returned
|
|
@@ -1485,7 +1940,7 @@ class BillingClient {
|
|
|
1485
1940
|
* `{ atPeriodEnd: false }` to end it immediately, forfeiting the remainder.
|
|
1486
1941
|
*
|
|
1487
1942
|
* PENDING checkouts (and anything with no provider-side record) are
|
|
1488
|
-
* cancelled locally straight away regardless of the flag
|
|
1943
|
+
* cancelled locally straight away regardless of the flag, there is nothing
|
|
1489
1944
|
* at the provider to schedule against.
|
|
1490
1945
|
*
|
|
1491
1946
|
* Pass `organizationId` when the subscription belongs to a team; the caller
|