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