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