@nulvless/nulvless-js 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.cjs ADDED
@@ -0,0 +1,2557 @@
1
+ 'use strict';
2
+
3
+ // src/exceptions/nulvless.exception.ts
4
+ var NulvlessException = class extends Error {
5
+ code;
6
+ status_code;
7
+ constructor(message, code, status_code) {
8
+ super(Array.isArray(message) ? message.join(", ") : message);
9
+ this.name = "NulvlessException";
10
+ this.code = code;
11
+ this.status_code = status_code;
12
+ }
13
+ };
14
+
15
+ // src/http.ts
16
+ var DOT_SEGMENT = /(^|\/)\.{1,2}(\/|$)/;
17
+ function resolveConfigHeaders(value) {
18
+ return typeof value === "function" ? value() : value;
19
+ }
20
+ function mergeHeaders(...sources) {
21
+ const merged = /* @__PURE__ */ new Map();
22
+ for (const source of sources) {
23
+ for (const [name, value] of Object.entries(source ?? {})) {
24
+ merged.set(name.toLowerCase(), [name, value]);
25
+ }
26
+ }
27
+ return Object.fromEntries(merged.values());
28
+ }
29
+ function findHeader(headers, name) {
30
+ const wanted = name.toLowerCase();
31
+ return Object.entries(headers ?? {}).find(([key]) => key.toLowerCase() === wanted)?.[1];
32
+ }
33
+ function route(strings, ...values) {
34
+ return strings.reduce((path, part, index) => path + encodeURIComponent(String(values[index - 1])) + part);
35
+ }
36
+ var InvalidPathError = class extends Error {
37
+ constructor(path) {
38
+ super(`Path parameters cannot be "." or "..": ${path}`);
39
+ this.name = "InvalidPathError";
40
+ }
41
+ };
42
+ function toError(err, timeout) {
43
+ if (err instanceof InvalidPathError) {
44
+ return { code: "invalid_path", message: err.message, status_code: 400 };
45
+ }
46
+ if (err instanceof Error && err.name === "AbortError") {
47
+ return {
48
+ code: "request_timeout",
49
+ message: `Request timed out after ${timeout ?? 0}ms`,
50
+ status_code: 408
51
+ };
52
+ }
53
+ return {
54
+ code: "network_error",
55
+ message: err instanceof Error ? err.message : "Network request failed",
56
+ status_code: 0
57
+ };
58
+ }
59
+ function extractErrorPayload(body, fallback) {
60
+ if (typeof body !== "object" || body === null) {
61
+ return { code: "unknown_error", message: fallback };
62
+ }
63
+ const record = body;
64
+ const code = typeof record["code"] === "string" ? record["code"] : "unknown_error";
65
+ const message = typeof record["message"] === "string" || Array.isArray(record["message"]) ? record["message"] : fallback;
66
+ return { code, message };
67
+ }
68
+ var HttpClient = class {
69
+ config;
70
+ onSudoRequired;
71
+ constructor(config) {
72
+ this.config = {
73
+ ...config,
74
+ baseUrl: config.baseUrl.replace(/\/+$/, "")
75
+ };
76
+ }
77
+ /**
78
+ * Injected after construction to avoid circular dependency.
79
+ * Called when a request fails with `403 protected_action_denied`,
80
+ * indicating the user must enable sudo mode before retrying.
81
+ */
82
+ setOnSudoRequired(cb) {
83
+ this.onSudoRequired = cb;
84
+ }
85
+ hasAuthorization(headers) {
86
+ return findHeader(headers, "Authorization") !== void 0 || findHeader(resolveConfigHeaders(this.config.headers), "Authorization") !== void 0;
87
+ }
88
+ async get(path, options) {
89
+ return this.send("GET", path, options);
90
+ }
91
+ async post(path, body, options) {
92
+ return this.send("POST", path, { ...options, body });
93
+ }
94
+ async put(path, body, options) {
95
+ return this.send("PUT", path, { ...options, body });
96
+ }
97
+ async patch(path, body, options) {
98
+ return this.send("PATCH", path, { ...options, body });
99
+ }
100
+ async delete(path, options) {
101
+ return this.send("DELETE", path, options);
102
+ }
103
+ /**
104
+ * Returns `true` when the error warrants a single token-refresh retry:
105
+ * - Any 401 on an authenticated request
106
+ * - 403 with `forbidden_access_waiting_accept` or `forbidden_resource_access` codes
107
+ */
108
+ shouldRetryWithRefresh(status, code, headers) {
109
+ if (!findHeader(headers, "Authorization")) return false;
110
+ if (status === 401) return true;
111
+ if (status === 403 && ["forbidden_resource_access", "forbidden_access_waiting_accept"].includes(code)) return true;
112
+ return false;
113
+ }
114
+ async send(method, path, options = {}, isRetry = false) {
115
+ const headers = this.buildHeaders(options.headers, options.language, options.nonce, options.skipApiKey);
116
+ const shouldThrow = options.throwErrors ?? this.config.throwErrors ?? false;
117
+ const timeout = options.timeout ?? this.config.timeout;
118
+ const controller = new AbortController();
119
+ const timeoutHandle = timeout && timeout > 0 ? setTimeout(() => controller.abort(), timeout) : void 0;
120
+ const start = Date.now();
121
+ if (this.config.debug) {
122
+ console.debug(`[nulvless] \u2192 ${method} ${path}`);
123
+ }
124
+ try {
125
+ const url = this.buildUrl(path, options.query);
126
+ const init = { method, headers, signal: controller.signal };
127
+ if (options.body !== void 0) {
128
+ const contentType = findHeader(headers, "Content-Type");
129
+ if (contentType === "application/x-www-form-urlencoded") {
130
+ init.body = new URLSearchParams(options.body).toString();
131
+ } else {
132
+ init.body = JSON.stringify(options.body);
133
+ }
134
+ }
135
+ const response = await fetch(url, init);
136
+ const body = await this.parseBody(response);
137
+ if (this.config.debug) {
138
+ console.debug(`[nulvless] \u2190 ${method} ${path} ${response.status} (${Date.now() - start}ms)`);
139
+ }
140
+ if (!response.ok) {
141
+ const { code, message } = extractErrorPayload(body, response.statusText);
142
+ const error = { code, message, status_code: response.status };
143
+ if (!isRetry && this.config.onTokenExpired && this.shouldRetryWithRefresh(response.status, code, headers)) {
144
+ const deauthOnFailure = response.status === 401;
145
+ const expiredToken = findHeader(headers, "Authorization")?.replace(/^Bearer\s+/i, "");
146
+ const newToken = await this.config.onTokenExpired({
147
+ deauthOnFailure,
148
+ ...expiredToken ? { expiredToken } : {}
149
+ });
150
+ if (newToken) {
151
+ return this.send(
152
+ method,
153
+ path,
154
+ { ...options, headers: mergeHeaders(options.headers, { Authorization: `Bearer ${newToken}` }) },
155
+ true
156
+ );
157
+ }
158
+ }
159
+ if (response.status === 403 && code === "protected_action_denied") {
160
+ this.onSudoRequired?.();
161
+ }
162
+ if (shouldThrow) {
163
+ throw new NulvlessException(message, code, response.status);
164
+ }
165
+ return { data: null, error };
166
+ }
167
+ return { data: body, error: null };
168
+ } catch (err) {
169
+ if (err instanceof NulvlessException) throw err;
170
+ const error = toError(err, timeout);
171
+ if (shouldThrow) {
172
+ throw new NulvlessException(error.message, error.code, error.status_code);
173
+ }
174
+ return { data: null, error };
175
+ } finally {
176
+ clearTimeout(timeoutHandle);
177
+ }
178
+ }
179
+ buildUrl(path, query) {
180
+ if (DOT_SEGMENT.test(path)) throw new InvalidPathError(path);
181
+ const url = new URL(`${this.config.baseUrl}/${path.replace(/^\/+/, "")}`);
182
+ if (query) {
183
+ for (const [key, value] of Object.entries(query)) {
184
+ if (value === void 0) continue;
185
+ const values = Array.isArray(value) ? value : [value];
186
+ for (const v of values) {
187
+ url.searchParams.append(Array.isArray(value) ? `${key}[]` : key, String(v));
188
+ }
189
+ }
190
+ }
191
+ return url.toString();
192
+ }
193
+ buildHeaders(extra, languageOverride, nonce, skipApiKey) {
194
+ const language = languageOverride?.() ?? this.config.language?.() ?? "en-US";
195
+ return mergeHeaders(
196
+ {
197
+ "Content-Type": "application/json",
198
+ ...skipApiKey ? {} : { "x-platform-key": this.config.apiKey },
199
+ "nlv-lang": language,
200
+ ...this.config.trackUser !== void 0 ? { "nlv-user-tracking": String(this.config.trackUser) } : {},
201
+ ...nonce ? { "nlv-sudo-nonce": nonce } : {}
202
+ },
203
+ resolveConfigHeaders(this.config.headers),
204
+ extra
205
+ );
206
+ }
207
+ async parseBody(response) {
208
+ const contentType = response.headers.get("content-type") ?? "";
209
+ if (!contentType.includes("json")) return null;
210
+ try {
211
+ return await response.json();
212
+ } catch {
213
+ return null;
214
+ }
215
+ }
216
+ };
217
+
218
+ // src/session/tabChannel.ts
219
+ var TabChannel = class {
220
+ channel;
221
+ constructor(name, onMessage) {
222
+ const Ctor = globalThis.BroadcastChannel;
223
+ if (!Ctor) {
224
+ this.channel = null;
225
+ return;
226
+ }
227
+ try {
228
+ this.channel = new Ctor(name);
229
+ } catch {
230
+ this.channel = null;
231
+ return;
232
+ }
233
+ this.channel.addEventListener("message", (event) => {
234
+ onMessage(event.data);
235
+ });
236
+ this.channel.unref?.();
237
+ }
238
+ post(message) {
239
+ try {
240
+ this.channel?.postMessage(message);
241
+ } catch {
242
+ this.channel = null;
243
+ }
244
+ }
245
+ close() {
246
+ this.channel?.close();
247
+ }
248
+ };
249
+
250
+ // src/auth/base.ts
251
+ var NO_SESSION_ERROR = {
252
+ code: "no_session",
253
+ message: "No active session.",
254
+ status_code: 401
255
+ };
256
+ async function resolveBearer(http, getToken, options) {
257
+ if (http.hasAuthorization(options?.headers)) return { ok: true, headers: {} };
258
+ const token = await getToken();
259
+ if (!token) return { ok: false, error: { ...NO_SESSION_ERROR } };
260
+ return { ok: true, headers: { Authorization: `Bearer ${token}` } };
261
+ }
262
+ var AuthSubClient = class {
263
+ constructor(http, getToken) {
264
+ this.http = http;
265
+ this.getToken = getToken;
266
+ }
267
+ http;
268
+ getToken;
269
+ bearer(options) {
270
+ return resolveBearer(this.http, this.getToken, options);
271
+ }
272
+ };
273
+
274
+ // src/auth/factors/index.ts
275
+ var FactorsClient = class extends AuthSubClient {
276
+ /**
277
+ * Lists all configured MFA factors for the authenticated user.
278
+ */
279
+ async list(options) {
280
+ const auth = await this.bearer(options);
281
+ if (!auth.ok) return { data: null, error: auth.error };
282
+ return this.http.get("/factors", {
283
+ ...options,
284
+ headers: { ...options?.headers, ...auth.headers }
285
+ });
286
+ }
287
+ /**
288
+ * Returns the user's recovery codes.
289
+ *
290
+ * Requires sudo: `MANAGE_MFA`.
291
+ */
292
+ async getRecoveryCodes(params, options) {
293
+ const auth = await this.bearer(options);
294
+ if (!auth.ok) return { data: null, error: auth.error };
295
+ return this.http.get("/factors/codes", {
296
+ ...options,
297
+ nonce: params?.nonce,
298
+ headers: { ...options?.headers, ...auth.headers }
299
+ });
300
+ }
301
+ /**
302
+ * Deletes (invalidates) the user's recovery codes.
303
+ *
304
+ * Requires sudo: `MANAGE_MFA`.
305
+ */
306
+ async deleteRecoveryCodes(params, options) {
307
+ const auth = await this.bearer(options);
308
+ if (!auth.ok) return { data: null, error: auth.error };
309
+ return this.http.delete("/factors/codes", {
310
+ ...options,
311
+ nonce: params?.nonce,
312
+ headers: { ...options?.headers, ...auth.headers }
313
+ });
314
+ }
315
+ /**
316
+ * Begins enrolling a new MFA factor. Returns credentials needed to configure
317
+ * the factor on the user's device.
318
+ *
319
+ * For TOTP: use `secret` for manual entry or `url` to generate a QR code.
320
+ *
321
+ * After the user configures their device, call `enable` with the `token` and
322
+ * the first OTP code to activate the factor.
323
+ *
324
+ * Requires sudo: `MANAGE_MFA`.
325
+ *
326
+ * @example
327
+ * const { data } = await nulvless.auth.factors.enroll({ type: 'totp', name: 'My Authenticator' })
328
+ * // data.url → QR code provisioning URL
329
+ * // data.secret → manual entry secret
330
+ * // data.token → pass to factors.enable
331
+ */
332
+ async enroll(params, options) {
333
+ const auth = await this.bearer(options);
334
+ if (!auth.ok) return { data: null, error: auth.error };
335
+ const { nonce, type, name } = params;
336
+ return this.http.post(route`/factors/${type}`, name ? { name } : {}, {
337
+ ...options,
338
+ nonce,
339
+ headers: { ...options?.headers, ...auth.headers }
340
+ });
341
+ }
342
+ /**
343
+ * Activates a pending factor enrollment by confirming the first OTP code.
344
+ *
345
+ * Pass the `token` from `enroll` and the code generated by the user's device.
346
+ */
347
+ async enable(params, options) {
348
+ const auth = await this.bearer(options);
349
+ if (!auth.ok) return { data: null, error: auth.error };
350
+ return this.http.post("/factors", params, {
351
+ ...options,
352
+ headers: { ...options?.headers, ...auth.headers }
353
+ });
354
+ }
355
+ /**
356
+ * Unenrolls (removes) an MFA factor by its ID.
357
+ *
358
+ * Use `factors.list()` to retrieve the factor `id` before calling this.
359
+ * Returns the removed factor.
360
+ *
361
+ * Requires sudo: `MANAGE_MFA`.
362
+ *
363
+ * @example
364
+ * const { data: factors } = await nulvless.auth.factors.list()
365
+ * const totp = factors?.find(f => f.type === 'totp')
366
+ * if (totp?.id) await nulvless.auth.factors.unenroll({ id: totp.id })
367
+ */
368
+ async unenroll(params, options) {
369
+ const auth = await this.bearer(options);
370
+ if (!auth.ok) return { data: null, error: auth.error };
371
+ const { nonce, id } = params;
372
+ return this.http.delete(route`/factors/${id}`, {
373
+ ...options,
374
+ nonce,
375
+ headers: { ...options?.headers, ...auth.headers }
376
+ });
377
+ }
378
+ };
379
+
380
+ // src/auth/users/index.ts
381
+ var UsersClient = class extends AuthSubClient {
382
+ // ─── Profile ────────────────────────────────────────────────────────────────
383
+ /**
384
+ * Returns the authenticated user's full profile.
385
+ */
386
+ async me(options) {
387
+ const auth = await this.bearer(options);
388
+ if (!auth.ok) return { data: null, error: auth.error };
389
+ return this.http.get("/users/me", {
390
+ ...options,
391
+ headers: { ...options?.headers, ...auth.headers }
392
+ });
393
+ }
394
+ /**
395
+ * Updates the authenticated user's profile fields.
396
+ *
397
+ * Omit fields to leave them unchanged.
398
+ */
399
+ async update(params, options) {
400
+ const auth = await this.bearer(options);
401
+ if (!auth.ok) return { data: null, error: auth.error };
402
+ return this.http.patch("/users", params, {
403
+ ...options,
404
+ headers: { ...options?.headers, ...auth.headers }
405
+ });
406
+ }
407
+ // ─── Identities ─────────────────────────────────────────────────────────────
408
+ /**
409
+ * Lists all identities (email, phone, OAuth) linked to the authenticated user.
410
+ *
411
+ * @param type Filter by identity type: `'email'`, `'phone'`, or `'oauth'`.
412
+ */
413
+ async getIdentities(type, options) {
414
+ const auth = await this.bearer(options);
415
+ if (!auth.ok) return { data: null, error: auth.error };
416
+ return this.http.get("/users/identities", {
417
+ ...options,
418
+ headers: { ...options?.headers, ...auth.headers },
419
+ ...type ? { query: { type } } : {}
420
+ });
421
+ }
422
+ /**
423
+ * Initiates adding a new email identity. Sends an OTP to the provided address.
424
+ *
425
+ * Complete the flow with `verifyIdentity`.
426
+ *
427
+ * Requires sudo: `MANAGE_IDENTITIES`.
428
+ */
429
+ async addEmailIdentity(params, options) {
430
+ const auth = await this.bearer(options);
431
+ if (!auth.ok) return { data: null, error: auth.error };
432
+ const { nonce, email } = params;
433
+ return this.http.post("/users/identities/emails", { email }, {
434
+ ...options,
435
+ nonce,
436
+ headers: { ...options?.headers, ...auth.headers }
437
+ });
438
+ }
439
+ /**
440
+ * Initiates adding a new phone identity. Sends an OTP to the provided number.
441
+ *
442
+ * Complete the flow with `verifyIdentity`.
443
+ *
444
+ * Requires sudo: `MANAGE_IDENTITIES`.
445
+ */
446
+ async addPhoneIdentity(params, options) {
447
+ const auth = await this.bearer(options);
448
+ if (!auth.ok) return { data: null, error: auth.error };
449
+ const { nonce, phone } = params;
450
+ return this.http.post("/users/identities/phones", phone, {
451
+ ...options,
452
+ nonce,
453
+ headers: { ...options?.headers, ...auth.headers }
454
+ });
455
+ }
456
+ /**
457
+ * Confirms an identity addition by submitting the OTP received via
458
+ * `addEmailIdentity` or `addPhoneIdentity`.
459
+ */
460
+ async verifyIdentity(params, options) {
461
+ const auth = await this.bearer(options);
462
+ if (!auth.ok) return { data: null, error: auth.error };
463
+ return this.http.post("/users/identities/verify", params, {
464
+ ...options,
465
+ headers: { ...options?.headers, ...auth.headers }
466
+ });
467
+ }
468
+ /**
469
+ * Updates an identity — currently used to set an identity as primary.
470
+ *
471
+ * Requires sudo: `MANAGE_IDENTITIES`.
472
+ */
473
+ async updateIdentity(params, options) {
474
+ const auth = await this.bearer(options);
475
+ if (!auth.ok) return { data: null, error: auth.error };
476
+ const { nonce, id, primary } = params;
477
+ return this.http.patch(route`/users/identities/${id}`, { primary }, {
478
+ ...options,
479
+ nonce,
480
+ headers: { ...options?.headers, ...auth.headers }
481
+ });
482
+ }
483
+ /**
484
+ * Unlinks (removes) an identity from the authenticated user.
485
+ *
486
+ * Requires sudo: `MANAGE_IDENTITIES`.
487
+ */
488
+ async removeIdentity(params, options) {
489
+ const auth = await this.bearer(options);
490
+ if (!auth.ok) return { data: null, error: auth.error };
491
+ const { nonce, id } = params;
492
+ return this.http.delete(route`/users/identities/${id}`, {
493
+ ...options,
494
+ nonce,
495
+ headers: { ...options?.headers, ...auth.headers }
496
+ });
497
+ }
498
+ // ─── Credential updates ──────────────────────────────────────────────────────
499
+ /**
500
+ * Sends an OTP to the new email address to initiate an email change.
501
+ *
502
+ * Complete the flow with `confirmEmailUpdate`.
503
+ *
504
+ * Requires sudo: `MANAGE_CREDENTIALS`.
505
+ */
506
+ async sendOTPToUpdateEmail(params, options) {
507
+ const auth = await this.bearer(options);
508
+ if (!auth.ok) return { data: null, error: auth.error };
509
+ const { nonce, email } = params;
510
+ return this.http.post("/users/emails/code/send", { email }, {
511
+ ...options,
512
+ nonce,
513
+ headers: { ...options?.headers, ...auth.headers }
514
+ });
515
+ }
516
+ /**
517
+ * Confirms an email change by submitting the OTP received via `sendOTPToUpdateEmail`.
518
+ */
519
+ async confirmEmailUpdate(params, options) {
520
+ const auth = await this.bearer(options);
521
+ if (!auth.ok) return { data: null, error: auth.error };
522
+ return this.http.patch("/users/emails/code/confirm", params, {
523
+ ...options,
524
+ headers: { ...options?.headers, ...auth.headers }
525
+ });
526
+ }
527
+ /**
528
+ * Sends an OTP to the new phone number to initiate a phone change.
529
+ *
530
+ * Complete the flow with `confirmPhoneUpdate`.
531
+ *
532
+ * Requires sudo: `MANAGE_CREDENTIALS`.
533
+ */
534
+ async sendOTPToUpdatePhone(params, options) {
535
+ const auth = await this.bearer(options);
536
+ if (!auth.ok) return { data: null, error: auth.error };
537
+ const { nonce, phone } = params;
538
+ return this.http.post("/users/phones/code/send", phone, {
539
+ ...options,
540
+ nonce,
541
+ headers: { ...options?.headers, ...auth.headers }
542
+ });
543
+ }
544
+ /**
545
+ * Confirms a phone change by submitting the OTP received via `sendOTPToUpdatePhone`.
546
+ */
547
+ async confirmPhoneUpdate(params, options) {
548
+ const auth = await this.bearer(options);
549
+ if (!auth.ok) return { data: null, error: auth.error };
550
+ return this.http.patch("/users/phones/code/confirm", params, {
551
+ ...options,
552
+ headers: { ...options?.headers, ...auth.headers }
553
+ });
554
+ }
555
+ };
556
+
557
+ // src/auth/sessions/index.ts
558
+ var SessionsClient = class extends AuthSubClient {
559
+ constructor(http, getToken, persist) {
560
+ super(http, getToken);
561
+ this.persist = persist;
562
+ }
563
+ persist;
564
+ /**
565
+ * Returns all active sessions for the authenticated user across all devices.
566
+ * The current session is marked with `is_current_session: true`.
567
+ */
568
+ async list(options) {
569
+ const auth = await this.bearer(options);
570
+ if (!auth.ok) return { data: null, error: auth.error };
571
+ return this.http.get("/sessions", {
572
+ ...options,
573
+ headers: { ...options?.headers, ...auth.headers }
574
+ });
575
+ }
576
+ /**
577
+ * Revokes (invalidates) a specific session by its ID.
578
+ *
579
+ * Use `auth.getSession()` to get the current session ID.
580
+ *
581
+ * Requires sudo: `MANAGE_SESSIONS`.
582
+ */
583
+ async revoke(params, options) {
584
+ const auth = await this.bearer(options);
585
+ if (!auth.ok) return { data: null, error: auth.error };
586
+ const { nonce, id } = params;
587
+ return this.http.delete(route`/sessions/${id}`, {
588
+ ...options,
589
+ nonce,
590
+ headers: { ...options?.headers, ...auth.headers }
591
+ });
592
+ }
593
+ /**
594
+ * Starts a sudo elevation by requesting a verification challenge.
595
+ *
596
+ * The available methods are derived from what the user has (TOTP if active,
597
+ * email OTP, SMS OTP). Pass `method` to request a specific one; if omitted and
598
+ * the user has a single method it is emitted automatically, otherwise an empty
599
+ * challenge is returned so the user can pick which one to send.
600
+ *
601
+ * Prompt the user for the code, then call `sudoVerify({ code })` to complete
602
+ * the elevation. The verify response carries `sudo.expires_in` (seconds until
603
+ * sudo mode expires) alongside the refreshed session tokens.
604
+ *
605
+ * @example
606
+ * const { data } = await nulvless.auth.sessions.sudo() // or sudo('email_otp')
607
+ * // prompt the user for the code, then call sudoVerify({ code })
608
+ */
609
+ async sudo(method, options) {
610
+ const auth = await this.bearer(options);
611
+ if (!auth.ok) return { data: null, error: auth.error };
612
+ return this.http.get("/sessions/sudo/request", {
613
+ ...options,
614
+ ...method ? { query: { method } } : {},
615
+ headers: { ...options?.headers, ...auth.headers }
616
+ });
617
+ }
618
+ /**
619
+ * Completes a sudo elevation started by `sudo()`. Submit the `code` the user
620
+ * received (or read from their authenticator) — the pending sudo challenge is
621
+ * resolved by the current session, so no challenge token is needed.
622
+ *
623
+ * On success the session is re-issued with sudo mode active and stored
624
+ * automatically; `data.sudo.expires_in` carries the seconds until it expires.
625
+ *
626
+ * @example
627
+ * const { data } = await nulvless.auth.sessions.sudoVerify({ code })
628
+ * // data.sudo?.expires_in → 300
629
+ */
630
+ async sudoVerify(params, options) {
631
+ const auth = await this.bearer(options);
632
+ if (!auth.ok) return { data: null, error: auth.error };
633
+ return this.persist(
634
+ await this.http.post("/sessions/sudo/verify", params, {
635
+ ...options,
636
+ headers: { ...options?.headers, ...auth.headers }
637
+ })
638
+ );
639
+ }
640
+ /**
641
+ * Verifies that sudo mode is currently active for the session.
642
+ *
643
+ * Returns `204 No Content` on success. Returns an error if sudo is not active.
644
+ *
645
+ * @example
646
+ * const { error } = await nulvless.auth.sessions.checkSudo()
647
+ * if (error) {
648
+ * // Start the sudo flow: sessions.sudo() → sessions.sudoVerify
649
+ * }
650
+ */
651
+ async checkSudo(params, options) {
652
+ const auth = await this.bearer(options);
653
+ if (!auth.ok) return { data: null, error: auth.error };
654
+ return this.http.get("/sessions/sudo", {
655
+ ...options,
656
+ nonce: params?.nonce,
657
+ headers: { ...options?.headers, ...auth.headers }
658
+ });
659
+ }
660
+ };
661
+
662
+ // src/auth/security/index.ts
663
+ var SecurityClient = class extends AuthSubClient {
664
+ /**
665
+ * Changes the authenticated user's password.
666
+ *
667
+ * @example
668
+ * await nulvless.auth.security.changePassword({
669
+ * password: 'current-password',
670
+ * new_password: 'new-password',
671
+ * })
672
+ */
673
+ async changePassword(params, options) {
674
+ const auth = await this.bearer(options);
675
+ if (!auth.ok) return { data: null, error: auth.error };
676
+ const { nonce, ...body } = params;
677
+ return this.http.put("/security/password", body, {
678
+ ...options,
679
+ nonce,
680
+ headers: { ...options?.headers, ...auth.headers }
681
+ });
682
+ }
683
+ };
684
+
685
+ // src/auth/challenges/index.ts
686
+ var ChallengesClient = class extends AuthSubClient {
687
+ constructor(http, getToken, persist) {
688
+ super(http, getToken);
689
+ this.persist = persist;
690
+ }
691
+ persist;
692
+ /**
693
+ * Creates a verification challenge for the given MFA method (step-up).
694
+ *
695
+ * Requires an authenticated session. Prompt the user for the code, then call
696
+ * `verify({ token, code })`.
697
+ *
698
+ * @example
699
+ * const { data } = await nulvless.auth.challenges.create('totp')
700
+ */
701
+ async create(targetType, options) {
702
+ const auth = await this.bearer(options);
703
+ if (!auth.ok) return { data: null, error: auth.error };
704
+ return this.http.post(route`/challenges/${targetType}`, void 0, {
705
+ ...options,
706
+ headers: { ...options?.headers, ...auth.headers }
707
+ });
708
+ }
709
+ /**
710
+ * Retrieves the current state of a challenge by its token.
711
+ */
712
+ get(token, options) {
713
+ return this.http.get("/challenges", { ...options, query: { token } });
714
+ }
715
+ /**
716
+ * Switches the preferred MFA method for an ongoing challenge.
717
+ *
718
+ * @example
719
+ * const { data } = await nulvless.auth.challenges.change({ token, type: 'totp' })
720
+ */
721
+ change(params, options) {
722
+ return this.http.patch(
723
+ route`/challenges/${params.type}`,
724
+ { token: params.token },
725
+ options
726
+ );
727
+ }
728
+ /**
729
+ * Verifies a challenge by submitting the token and the code entered by the user.
730
+ *
731
+ * Used for OTP sign-in, MFA verification after `signIn`, and sign-up
732
+ * confirmation. Session is stored automatically when the response contains an
733
+ * access token.
734
+ */
735
+ async verify(params, options) {
736
+ return this.persist(await this.http.post("/challenges/verify", params, options));
737
+ }
738
+ };
739
+
740
+ // src/auth/grants/index.ts
741
+ var GrantsClient = class extends AuthSubClient {
742
+ /**
743
+ * Returns all grants granted to the authenticated user, optionally
744
+ * filtered by context.
745
+ */
746
+ async list(context, options) {
747
+ const auth = await this.bearer(options);
748
+ if (!auth.ok) return { data: null, error: auth.error };
749
+ return this.http.get("/grants", {
750
+ ...options,
751
+ headers: { ...options?.headers, ...auth.headers },
752
+ ...context ? { query: { context } } : {}
753
+ });
754
+ }
755
+ /**
756
+ * Returns all grants for a given resource, grouped by status
757
+ * (granted, invited, requested, blocked).
758
+ *
759
+ * The authenticated user must own or manage the resource.
760
+ */
761
+ async getMembers(resource, options) {
762
+ const auth = await this.bearer(options);
763
+ if (!auth.ok) return { data: null, error: auth.error };
764
+ return this.http.get("/grants/members", {
765
+ ...options,
766
+ headers: { ...options?.headers, ...auth.headers },
767
+ query: { resource }
768
+ });
769
+ }
770
+ /**
771
+ * Creates grant invitations for one or more users on a resource.
772
+ *
773
+ * Each invite sends a notification to the target email address.
774
+ *
775
+ * Requires sudo: `MANAGE_GRANTS`.
776
+ *
777
+ * @example
778
+ * await nulvless.auth.grants.invite({
779
+ * resource: 'project-123',
780
+ * invites: [{ email: 'user@example.com', scopes: ['read', 'write'] }],
781
+ * })
782
+ */
783
+ async invite(params, options) {
784
+ const auth = await this.bearer(options);
785
+ if (!auth.ok) return { data: null, error: auth.error };
786
+ const { nonce, ...body } = params;
787
+ return this.http.post("/grants/invites", {
788
+ ...body,
789
+ ...options?.redirect_uri ? { redirect_uri: options.redirect_uri } : {}
790
+ }, {
791
+ ...options,
792
+ nonce,
793
+ headers: { ...options?.headers, ...auth.headers }
794
+ });
795
+ }
796
+ /**
797
+ * Returns a single grant by ID.
798
+ */
799
+ async get(id, options) {
800
+ const auth = await this.bearer(options);
801
+ if (!auth.ok) return { data: null, error: auth.error };
802
+ return this.http.get(route`/grants/${id}`, {
803
+ ...options,
804
+ headers: { ...options?.headers, ...auth.headers }
805
+ });
806
+ }
807
+ /**
808
+ * Revokes (removes) a grant.
809
+ *
810
+ * Requires sudo: `MANAGE_GRANTS`.
811
+ */
812
+ async revoke(params, options) {
813
+ const auth = await this.bearer(options);
814
+ if (!auth.ok) return { data: null, error: auth.error };
815
+ const { nonce, id } = params;
816
+ return this.http.delete(route`/grants/${id}/revoke`, {
817
+ ...options,
818
+ nonce,
819
+ headers: { ...options?.headers, ...auth.headers }
820
+ });
821
+ }
822
+ /**
823
+ * Accepts a pending invitation (`status: 'invited'`).
824
+ */
825
+ async accept(id, options) {
826
+ const auth = await this.bearer(options);
827
+ if (!auth.ok) return { data: null, error: auth.error };
828
+ return this.http.patch(route`/grants/${id}/accept`, void 0, {
829
+ ...options,
830
+ headers: { ...options?.headers, ...auth.headers }
831
+ });
832
+ }
833
+ /**
834
+ * Cancels a pending invitation or request (`status: 'invited'` or `'requested'`).
835
+ */
836
+ async cancel(id, options) {
837
+ const auth = await this.bearer(options);
838
+ if (!auth.ok) return { data: null, error: auth.error };
839
+ return this.http.patch(route`/grants/${id}/cancel`, void 0, {
840
+ ...options,
841
+ headers: { ...options?.headers, ...auth.headers }
842
+ });
843
+ }
844
+ /**
845
+ * Blocks a grant.
846
+ *
847
+ * Requires sudo: `MANAGE_GRANTS`.
848
+ */
849
+ async block(params, options) {
850
+ const auth = await this.bearer(options);
851
+ if (!auth.ok) return { data: null, error: auth.error };
852
+ const { nonce, id } = params;
853
+ return this.http.patch(route`/grants/${id}/block`, void 0, {
854
+ ...options,
855
+ nonce,
856
+ headers: { ...options?.headers, ...auth.headers }
857
+ });
858
+ }
859
+ /**
860
+ * Updates the scopes of an existing grant.
861
+ *
862
+ * Requires sudo: `MANAGE_GRANTS`.
863
+ */
864
+ async update(id, params, options) {
865
+ const auth = await this.bearer(options);
866
+ if (!auth.ok) return { data: null, error: auth.error };
867
+ const { nonce, ...body } = params;
868
+ return this.http.patch(route`/grants/${id}`, body, {
869
+ ...options,
870
+ nonce,
871
+ headers: { ...options?.headers, ...auth.headers }
872
+ });
873
+ }
874
+ /**
875
+ * Removes the authenticated user from a resource they were granted access to.
876
+ *
877
+ * Requires sudo: `MANAGE_GRANTS`.
878
+ */
879
+ async leave(params, options) {
880
+ const auth = await this.bearer(options);
881
+ if (!auth.ok) return { data: null, error: auth.error };
882
+ const { nonce, id } = params;
883
+ return this.http.patch(route`/grants/${id}/leave`, void 0, {
884
+ ...options,
885
+ nonce,
886
+ headers: { ...options?.headers, ...auth.headers }
887
+ });
888
+ }
889
+ };
890
+
891
+ // src/auth/oauth/index.ts
892
+ var OAuthGrantsClient = class extends AuthSubClient {
893
+ /**
894
+ * Lists the apps the authenticated user has granted access to.
895
+ */
896
+ async list(options) {
897
+ const auth = await this.bearer(options);
898
+ if (!auth.ok) return { data: null, error: auth.error };
899
+ return this.http.get("/user/oauth/grants", {
900
+ ...options,
901
+ headers: { ...options?.headers, ...auth.headers }
902
+ });
903
+ }
904
+ /**
905
+ * Revokes a previously granted app by its public `client_id`.
906
+ * Also invalidates the app's active sessions for this user.
907
+ */
908
+ async revoke(clientId, options) {
909
+ const auth = await this.bearer(options);
910
+ if (!auth.ok) return { data: null, error: auth.error };
911
+ return this.http.delete("/user/oauth/grants", {
912
+ ...options,
913
+ headers: { ...options?.headers, ...auth.headers },
914
+ query: { client_id: clientId }
915
+ });
916
+ }
917
+ };
918
+ var OAuthClient = class extends AuthSubClient {
919
+ /** Apps the authenticated user has authorized — list and revoke. */
920
+ grants;
921
+ constructor(http, getToken) {
922
+ super(http, getToken);
923
+ this.grants = new OAuthGrantsClient(http, getToken);
924
+ }
925
+ // ─── Authorization redirect (platform key) ──────────────────────────────────
926
+ /**
927
+ * Builds the OAuth authorization URL and, in the browser, redirects the user
928
+ * agent to it. On the server the URL is returned for manual handling.
929
+ *
930
+ * Mirrors `auth.authorize` (social sign-in): redirects automatically in the
931
+ * browser, returns `data.url` on the server. The endpoint itself is a 302
932
+ * redirect, so the URL is composed from the discovery `authorization_endpoint`.
933
+ *
934
+ * PKCE is required (RFC 7636, https://www.rfc-editor.org/rfc/rfc7636): provide
935
+ * `code_challenge` = BASE64URL(SHA256(code_verifier)) with
936
+ * `code_challenge_method: 'S256'`. Send the `code_verifier` later to
937
+ * `exchangeCodeForToken`.
938
+ *
939
+ * @example
940
+ * // Browser — redirects automatically
941
+ * await nulvless.auth.oauth.authorize({ client_id, redirect_uri, code_challenge, code_challenge_method: 'S256' })
942
+ *
943
+ * // Server — use the returned URL
944
+ * const { data } = await nulvless.auth.oauth.authorize(params)
945
+ * return Response.redirect(data.url)
946
+ */
947
+ async authorize(params, options) {
948
+ const discovery = await this.discovery(options);
949
+ if (discovery.error) return { data: null, error: discovery.error };
950
+ const url = new URL(discovery.data.authorization_endpoint);
951
+ const query = {
952
+ client_id: params.client_id,
953
+ redirect_uri: params.redirect_uri,
954
+ response_type: params.response_type ?? "code",
955
+ code_challenge: params.code_challenge,
956
+ code_challenge_method: params.code_challenge_method,
957
+ scope: params.scope,
958
+ state: params.state,
959
+ resource: params.resource,
960
+ nonce: params.nonce
961
+ };
962
+ for (const [key, value] of Object.entries(query)) {
963
+ if (value !== void 0) url.searchParams.set(key, value);
964
+ }
965
+ if (typeof window !== "undefined") {
966
+ window.location.href = url.toString();
967
+ }
968
+ return { data: { url: url.toString() }, error: null };
969
+ }
970
+ // ─── Token endpoint (public) ────────────────────────────────────────────────
971
+ /**
972
+ * Exchanges an authorization code for tokens
973
+ * (`POST /oauth/token`, `grant_type=authorization_code`).
974
+ *
975
+ * Called by the relying-party app after the user is redirected back with the
976
+ * `code`. PKCE `code_verifier` is required; `client_secret` only for
977
+ * confidential clients. Public endpoint — no platform key.
978
+ */
979
+ exchangeCodeForToken(params, options) {
980
+ return this.http.post(
981
+ "/oauth/token",
982
+ { grant_type: "authorization_code", ...params },
983
+ { ...options, skipApiKey: true }
984
+ );
985
+ }
986
+ /**
987
+ * Refreshes an access token using a refresh token
988
+ * (`POST /oauth/token`, `grant_type=refresh_token`).
989
+ *
990
+ * `client_secret` only for confidential clients. Public endpoint — no
991
+ * platform key.
992
+ */
993
+ refreshToken(params, options) {
994
+ return this.http.post(
995
+ "/oauth/token",
996
+ { grant_type: "refresh_token", ...params },
997
+ { ...options, skipApiKey: true }
998
+ );
999
+ }
1000
+ // ─── Dynamic registration (platform key) ────────────────────────────────────
1001
+ /**
1002
+ * Dynamically registers a new OAuth app (RFC 7591, `POST /oauth/register`).
1003
+ *
1004
+ * Requires the OAuth server to be enabled with dynamic client registration
1005
+ * allowed. For server-side managed registration use `admin.oauthClients`.
1006
+ */
1007
+ register(params, options) {
1008
+ return this.http.post("/oauth/register", params, { ...options, skipApiKey: true });
1009
+ }
1010
+ // ─── Consent flow (authenticated user) ──────────────────────────────────────
1011
+ /**
1012
+ * Loads a pending authorization for the consent screen
1013
+ * (`GET /oauth/authorizations/:id`).
1014
+ *
1015
+ * Claims the authorization for the authenticated user. When a matching
1016
+ * consent already exists it is auto-approved and `redirect.uri` is returned.
1017
+ *
1018
+ * Requires authentication.
1019
+ */
1020
+ async getAuthorization(authorizationId, options) {
1021
+ const auth = await this.bearer(options);
1022
+ if (!auth.ok) return { data: null, error: auth.error };
1023
+ return this.http.get(route`/oauth/authorizations/${authorizationId}`, {
1024
+ ...options,
1025
+ headers: { ...options?.headers, ...auth.headers }
1026
+ });
1027
+ }
1028
+ /**
1029
+ * Approves or denies a pending authorization
1030
+ * (`POST /oauth/authorizations/:id/consent`).
1031
+ *
1032
+ * Returns the `redirect.uri` to send the user agent back to (carrying the
1033
+ * authorization `code` on approve, or `error=access_denied` on deny).
1034
+ *
1035
+ * Requires authentication.
1036
+ */
1037
+ async consent(authorizationId, action, options) {
1038
+ const auth = await this.bearer(options);
1039
+ if (!auth.ok) return { data: null, error: auth.error };
1040
+ return this.http.post(route`/oauth/authorizations/${authorizationId}/consent`, { action }, {
1041
+ ...options,
1042
+ headers: { ...options?.headers, ...auth.headers }
1043
+ });
1044
+ }
1045
+ // ─── UserInfo (authenticated user) ──────────────────────────────────────────
1046
+ /**
1047
+ * Returns OIDC UserInfo claims for the access token (`GET /oauth/userinfo`).
1048
+ *
1049
+ * The access token must have been issued with the `openid` scope.
1050
+ *
1051
+ * Requires authentication.
1052
+ */
1053
+ async userinfo(options) {
1054
+ const auth = await this.bearer(options);
1055
+ if (!auth.ok) return { data: null, error: auth.error };
1056
+ return this.http.get("/oauth/userinfo", {
1057
+ ...options,
1058
+ headers: { ...options?.headers, ...auth.headers }
1059
+ });
1060
+ }
1061
+ // ─── Public OIDC metadata (no auth) ─────────────────────────────────────────
1062
+ /**
1063
+ * Returns the OpenID Connect discovery document
1064
+ * (`GET /.well-known/openid-configuration`). Public — no auth required.
1065
+ */
1066
+ discovery(options) {
1067
+ return this.http.get("/.well-known/openid-configuration", { ...options, skipApiKey: true });
1068
+ }
1069
+ /**
1070
+ * Returns the JSON Web Key Set used to verify issued tokens
1071
+ * (`GET /.well-known/jwks.json`). Public — no auth required.
1072
+ */
1073
+ jwks(options) {
1074
+ return this.http.get("/.well-known/jwks.json", { ...options, skipApiKey: true });
1075
+ }
1076
+ };
1077
+
1078
+ // src/auth/admin/index.ts
1079
+ var AdminUsersClient = class {
1080
+ constructor(http) {
1081
+ this.http = http;
1082
+ }
1083
+ http;
1084
+ /**
1085
+ * Creates a new user bypassing normal sign-up flows.
1086
+ */
1087
+ create(params, options) {
1088
+ return this.http.post("/admin/users", params, options);
1089
+ }
1090
+ /**
1091
+ * Lists users with pagination and optional filters.
1092
+ */
1093
+ list(params, options) {
1094
+ return this.http.get("/admin/users", {
1095
+ ...options,
1096
+ ...params ? { query: params } : {}
1097
+ });
1098
+ }
1099
+ /**
1100
+ * Returns a single user by ID.
1101
+ */
1102
+ get(userId, options) {
1103
+ return this.http.get(route`/admin/users/${userId}`, options);
1104
+ }
1105
+ /**
1106
+ * Updates a user's profile fields.
1107
+ */
1108
+ update(userId, params, options) {
1109
+ return this.http.patch(route`/admin/users/${userId}`, params, options);
1110
+ }
1111
+ /**
1112
+ * Permanently deletes a user.
1113
+ */
1114
+ delete(userId, options) {
1115
+ return this.http.delete(route`/admin/users/${userId}`, options);
1116
+ }
1117
+ /**
1118
+ * Bans a user, preventing them from signing in.
1119
+ */
1120
+ ban(userId, options) {
1121
+ return this.http.patch(route`/admin/users/${userId}/ban`, void 0, options);
1122
+ }
1123
+ /**
1124
+ * Lifts a ban on a user.
1125
+ */
1126
+ unban(userId, options) {
1127
+ return this.http.patch(route`/admin/users/${userId}/unban`, void 0, options);
1128
+ }
1129
+ /**
1130
+ * Returns all active sessions for a specific user.
1131
+ */
1132
+ getSessions(userId, options) {
1133
+ return this.http.get(route`/admin/users/${userId}/sessions`, options);
1134
+ }
1135
+ /**
1136
+ * Invalidates all sessions for a specific user (signs them out everywhere).
1137
+ */
1138
+ signOutAll(userId, options) {
1139
+ return this.http.delete(route`/admin/users/${userId}/sessions`, options);
1140
+ }
1141
+ /**
1142
+ * Invalidates a specific session for a user.
1143
+ */
1144
+ revokeSession(userId, sessionId, options) {
1145
+ return this.http.delete(route`/admin/users/${userId}/sessions/${sessionId}`, options);
1146
+ }
1147
+ /**
1148
+ * Returns all configured MFA factors for a specific user.
1149
+ */
1150
+ getFactors(userId, options) {
1151
+ return this.http.get(route`/admin/users/${userId}/factors`, options);
1152
+ }
1153
+ /**
1154
+ * Removes a specific MFA factor from a user.
1155
+ */
1156
+ removeFactor(userId, factorId, options) {
1157
+ return this.http.delete(route`/admin/users/${userId}/factors/${factorId}`, options);
1158
+ }
1159
+ };
1160
+ var AdminRolesClient = class {
1161
+ constructor(http) {
1162
+ this.http = http;
1163
+ }
1164
+ http;
1165
+ /**
1166
+ * Returns all roles, optionally filtered by context.
1167
+ */
1168
+ list(context, options) {
1169
+ return this.http.get("/admin/roles", {
1170
+ ...options,
1171
+ ...context ? { query: { context } } : {}
1172
+ });
1173
+ }
1174
+ /**
1175
+ * Creates a new role with the given scopes.
1176
+ */
1177
+ create(params, options) {
1178
+ return this.http.post("/admin/roles", params, options);
1179
+ }
1180
+ /**
1181
+ * Updates an existing role.
1182
+ */
1183
+ update(role_id, params, options) {
1184
+ return this.http.patch(route`/admin/roles/${role_id}`, params, options);
1185
+ }
1186
+ /**
1187
+ * Deletes a role.
1188
+ */
1189
+ delete(role_id, options) {
1190
+ return this.http.delete(route`/admin/roles/${role_id}`, options);
1191
+ }
1192
+ };
1193
+ var AdminGrantsClient = class {
1194
+ constructor(http) {
1195
+ this.http = http;
1196
+ }
1197
+ http;
1198
+ /**
1199
+ * Lists grants with pagination and optional filters.
1200
+ */
1201
+ list(params, options) {
1202
+ return this.http.get("/admin/grants", {
1203
+ ...options,
1204
+ ...params ? { query: params } : {}
1205
+ });
1206
+ }
1207
+ /**
1208
+ * Creates or upserts a grant for a user on a resource.
1209
+ */
1210
+ set(params, options) {
1211
+ return this.http.post("/admin/grants", params, options);
1212
+ }
1213
+ /**
1214
+ * Deletes a grant by ID.
1215
+ */
1216
+ delete(grantId, options) {
1217
+ return this.http.delete(route`/admin/grants/${grantId}`, options);
1218
+ }
1219
+ /**
1220
+ * Removes all grants associated with a resource.
1221
+ *
1222
+ * Use with caution — this will revoke access for all users on the resource.
1223
+ */
1224
+ deleteByResource(resource, options) {
1225
+ return this.http.delete(route`/admin/resources/${resource}/grants`, options);
1226
+ }
1227
+ };
1228
+ var AdminOAuthClientsClient = class {
1229
+ constructor(http) {
1230
+ this.http = http;
1231
+ }
1232
+ http;
1233
+ /**
1234
+ * Registers a new OAuth app. For confidential apps the response includes a
1235
+ * one-time `client_secret` — store it securely, it is never returned again.
1236
+ */
1237
+ create(params, options) {
1238
+ return this.http.post("/admin/oauth/clients", params, options);
1239
+ }
1240
+ /**
1241
+ * Lists all registered OAuth apps.
1242
+ */
1243
+ list(options) {
1244
+ return this.http.get("/admin/oauth/clients", options);
1245
+ }
1246
+ /**
1247
+ * Returns a single OAuth app by its internal id.
1248
+ */
1249
+ get(id, options) {
1250
+ return this.http.get(route`/admin/oauth/clients/${id}`, options);
1251
+ }
1252
+ /**
1253
+ * Updates an OAuth app. `client_type` is immutable and the secret rotates via
1254
+ * `regenerateSecret` — neither can be changed here.
1255
+ */
1256
+ update(id, params, options) {
1257
+ return this.http.put(route`/admin/oauth/clients/${id}`, params, options);
1258
+ }
1259
+ /**
1260
+ * Deletes an OAuth app.
1261
+ */
1262
+ delete(id, options) {
1263
+ return this.http.delete(route`/admin/oauth/clients/${id}`, options);
1264
+ }
1265
+ /**
1266
+ * Rotates the client secret (confidential apps only). The new `client_secret`
1267
+ * is returned once — the previous one stops working immediately.
1268
+ */
1269
+ regenerateSecret(id, options) {
1270
+ return this.http.post(route`/admin/oauth/clients/${id}/regenerate_secret`, void 0, options);
1271
+ }
1272
+ };
1273
+ var AdminClient = class {
1274
+ constructor(http) {
1275
+ this.http = http;
1276
+ this.users = new AdminUsersClient(http);
1277
+ this.roles = new AdminRolesClient(http);
1278
+ this.grants = new AdminGrantsClient(http);
1279
+ this.oauthClients = new AdminOAuthClientsClient(http);
1280
+ }
1281
+ http;
1282
+ users;
1283
+ roles;
1284
+ grants;
1285
+ oauthClients;
1286
+ /**
1287
+ * Returns basic platform information (id, name, slug).
1288
+ */
1289
+ getPlatform(options) {
1290
+ return this.http.get("/admin/platform", options);
1291
+ }
1292
+ };
1293
+
1294
+ // src/session/stored.ts
1295
+ var DEFAULT_EXPIRES_IN_SECONDS = 3600;
1296
+ function toStoredSession(auth) {
1297
+ return {
1298
+ access_token: auth.access_token,
1299
+ ...auth.refresh_token !== void 0 ? { refresh_token: auth.refresh_token } : {},
1300
+ expires_at: Date.now() + (auth.expires_in ?? DEFAULT_EXPIRES_IN_SECONDS) * 1e3,
1301
+ token_type: auth.token_type ?? "bearer",
1302
+ ...auth.user !== void 0 ? { user: auth.user } : {}
1303
+ };
1304
+ }
1305
+ function toAuthenticated(stored) {
1306
+ return {
1307
+ access_token: stored.access_token,
1308
+ ...stored.refresh_token !== void 0 ? { refresh_token: stored.refresh_token } : {},
1309
+ ...stored.token_type ? { token_type: stored.token_type } : {},
1310
+ expires_in: Math.round((stored.expires_at - Date.now()) / 1e3),
1311
+ ...stored.user !== void 0 ? { user: stored.user } : {}
1312
+ };
1313
+ }
1314
+
1315
+ // src/auth/accounts.ts
1316
+ function decodeBase64Url(segment) {
1317
+ const base64 = segment.replace(/-/g, "+").replace(/_/g, "/");
1318
+ const padded = base64.padEnd(base64.length + (4 - base64.length % 4) % 4, "=");
1319
+ const bytes = Uint8Array.from(atob(padded), (char) => char.charCodeAt(0));
1320
+ return new TextDecoder().decode(bytes);
1321
+ }
1322
+ function extractUserId(auth) {
1323
+ if (auth.user?.id) return auth.user.id;
1324
+ if (!auth.access_token) return null;
1325
+ try {
1326
+ const payload = JSON.parse(decodeBase64Url(auth.access_token.split(".")[1]));
1327
+ return typeof payload["sub"] === "string" ? payload["sub"] : null;
1328
+ } catch {
1329
+ return null;
1330
+ }
1331
+ }
1332
+ var AccountsManager = class {
1333
+ constructor(storage, accountsKey) {
1334
+ this.storage = storage;
1335
+ this.accountsKey = accountsKey;
1336
+ }
1337
+ storage;
1338
+ accountsKey;
1339
+ async readMap() {
1340
+ const raw = await this.storage.getItem(this.accountsKey);
1341
+ if (!raw) return {};
1342
+ try {
1343
+ return JSON.parse(raw);
1344
+ } catch {
1345
+ return {};
1346
+ }
1347
+ }
1348
+ async writeMap(map) {
1349
+ await this.storage.setItem(this.accountsKey, JSON.stringify(map));
1350
+ }
1351
+ async save(auth) {
1352
+ const userId = extractUserId(auth);
1353
+ if (!userId || !auth.access_token) return null;
1354
+ const map = await this.readMap();
1355
+ map[userId] = toStoredSession({ ...auth, access_token: auth.access_token });
1356
+ await this.writeMap(map);
1357
+ return userId;
1358
+ }
1359
+ async getSession(userId) {
1360
+ const map = await this.readMap();
1361
+ return map[userId] ?? null;
1362
+ }
1363
+ async list() {
1364
+ const map = await this.readMap();
1365
+ return Object.entries(map).map(([userId, session]) => ({
1366
+ userId,
1367
+ access_token: session.access_token,
1368
+ expires_in: Math.round((session.expires_at - Date.now()) / 1e3),
1369
+ ...session.refresh_token !== void 0 ? { refresh_token: session.refresh_token } : {},
1370
+ ...session.user !== void 0 ? { user: session.user } : {}
1371
+ }));
1372
+ }
1373
+ async remove(userId) {
1374
+ const map = await this.readMap();
1375
+ delete map[userId];
1376
+ await this.writeMap(map);
1377
+ }
1378
+ };
1379
+
1380
+ // src/auth/emitter.ts
1381
+ var AuthEventEmitter = class {
1382
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
1383
+ listeners = /* @__PURE__ */ new Map();
1384
+ on(event, handler) {
1385
+ let set = this.listeners.get(event);
1386
+ if (!set) {
1387
+ set = /* @__PURE__ */ new Set();
1388
+ this.listeners.set(event, set);
1389
+ }
1390
+ set.add(handler);
1391
+ return () => {
1392
+ set.delete(handler);
1393
+ };
1394
+ }
1395
+ emit(...args) {
1396
+ const [event, payload] = args;
1397
+ const set = this.listeners.get(event);
1398
+ if (!set) return;
1399
+ for (const handler of set) {
1400
+ try {
1401
+ ;
1402
+ handler(payload);
1403
+ } catch (error) {
1404
+ console.error(`[nulvless] '${event}' listener failed`, error);
1405
+ }
1406
+ }
1407
+ }
1408
+ removeAllListeners(event) {
1409
+ if (event) {
1410
+ this.listeners.delete(event);
1411
+ } else {
1412
+ this.listeners.clear();
1413
+ }
1414
+ }
1415
+ };
1416
+
1417
+ // src/auth/index.ts
1418
+ var AuthClient = class {
1419
+ constructor(http, session) {
1420
+ this.http = http;
1421
+ this.session = session;
1422
+ session?.setRefreshFn(
1423
+ (refresh_token) => this.http.post("/token", { refresh_token }, {
1424
+ query: { grant_type: "refresh_token" }
1425
+ })
1426
+ );
1427
+ this._getToken = () => this.session?.getValidToken() ?? Promise.resolve(null);
1428
+ this.factors = new FactorsClient(http, this._getToken);
1429
+ this.users = new UsersClient(http, this._getToken);
1430
+ this.sessions = new SessionsClient(
1431
+ http,
1432
+ this._getToken,
1433
+ (result) => this.persistSession(result)
1434
+ );
1435
+ this.security = new SecurityClient(http, this._getToken);
1436
+ this.challenges = new ChallengesClient(
1437
+ http,
1438
+ this._getToken,
1439
+ (result) => this.persistSession(result)
1440
+ );
1441
+ this.oauth = new OAuthClient(http, this._getToken);
1442
+ }
1443
+ http;
1444
+ session;
1445
+ /** MFA factor management — enroll, enable, disable, recovery codes. */
1446
+ factors;
1447
+ /** Current-user management — profile, identities, credential updates. */
1448
+ users;
1449
+ /** Active session management — list, revoke, sudo mode. */
1450
+ sessions;
1451
+ /** Account security — change password. */
1452
+ security;
1453
+ /** Challenge lifecycle — create, get, change, verify (MFA / OTP / step-up). */
1454
+ challenges;
1455
+ /** OAuth server (project as OIDC provider) — token, consent, userinfo, grants, discovery. */
1456
+ oauth;
1457
+ _getToken;
1458
+ /**
1459
+ * Returns the current active access token from storage.
1460
+ * Refreshes automatically if the token is expired or about to expire.
1461
+ * Returns `null` when there is no active session.
1462
+ */
1463
+ async getAccessToken() {
1464
+ return this.session?.getValidToken() ?? null;
1465
+ }
1466
+ // ─── Helpers ──────────────────────────────────────────────────────────────
1467
+ async persistSession(result) {
1468
+ if (!result.error && result.data.access_token && this.session) {
1469
+ await this.session.save(result.data);
1470
+ }
1471
+ return result;
1472
+ }
1473
+ // ─── Sign-up / Sign-in ─────────────────────────────────────────────────────
1474
+ /**
1475
+ * Creates a new user account.
1476
+ *
1477
+ * At least one of `email`, `phone`, or `username` must be provided.
1478
+ *
1479
+ * Check the response in this order:
1480
+ * 1. `data.access_token` → authenticated immediately — session stored automatically.
1481
+ * 2. `data.challenge` → MFA/OTP step required — call `verifyChallenge`.
1482
+ * 3. `data.user` only → pending email/phone confirmation.
1483
+ */
1484
+ async signUp(params, options) {
1485
+ return this.persistSession(
1486
+ await this.http.post("/signup", params, options)
1487
+ );
1488
+ }
1489
+ /**
1490
+ * Signs in with password (`grant_type=password`).
1491
+ *
1492
+ * At least one of `email`, `phone`, or `username` must be provided.
1493
+ *
1494
+ * Check the response in this order:
1495
+ * 1. `data.access_token` → signed in — session stored automatically.
1496
+ * 2. `data.challenge` → MFA step required — call `verifyChallenge`.
1497
+ */
1498
+ async signIn(params, options) {
1499
+ return this.persistSession(
1500
+ await this.http.post("/token", params, {
1501
+ ...options,
1502
+ query: { grant_type: "password" }
1503
+ })
1504
+ );
1505
+ }
1506
+ /**
1507
+ * Requests a one-time password sent to the user's email or phone.
1508
+ *
1509
+ * Returns a `Challenge` (`202 Accepted`). Pass the received code to `verifyChallenge`.
1510
+ *
1511
+ * At least one of `email` or `phone` must be provided.
1512
+ */
1513
+ signInWithOTP(params, options) {
1514
+ return this.http.post("/otp", params, options);
1515
+ }
1516
+ /**
1517
+ * Initiates an OAuth sign-in flow for the given provider.
1518
+ *
1519
+ * In a browser environment the user is redirected automatically to the
1520
+ * authorization URL. On the server the URL is returned for manual handling.
1521
+ *
1522
+ * After the redirect, the server-side callback exchanges the code automatically
1523
+ * and redirects back to `options.redirect_uri`. For server-side code exchange, use `exchangeCodeForToken`.
1524
+ *
1525
+ * @example
1526
+ * // Browser — redirects automatically
1527
+ * await nulvless.auth.authorize('google', { redirect_uri: 'https://myapp.com/callback' })
1528
+ *
1529
+ * // Server — use the returned URL
1530
+ * const { data } = await nulvless.auth.authorize('google', { redirect_uri: 'https://myapp.com/callback' })
1531
+ * return Response.redirect(data.url)
1532
+ */
1533
+ async authorize(provider, options) {
1534
+ const result = await this.http.get(
1535
+ route`/oauth/${provider}/authorize`,
1536
+ { ...options, query: { ...options?.redirect_uri ? { redirect_uri: options.redirect_uri } : {} } }
1537
+ );
1538
+ if (!result.error && typeof window !== "undefined") {
1539
+ window.location.href = result.data.url;
1540
+ }
1541
+ return result;
1542
+ }
1543
+ /**
1544
+ * Initiates an OAuth flow to connect a new provider to the authenticated user's account.
1545
+ *
1546
+ * Same behaviour as `authorize`, but for users who are already signed in — the resulting
1547
+ * provider identity is linked to the current account instead of starting a sign-in flow.
1548
+ *
1549
+ * In a browser environment the user is redirected automatically to the
1550
+ * authorization URL. On the server the URL is returned for manual handling.
1551
+ *
1552
+ * Requires authentication. Token is resolved automatically from storage
1553
+ * (browser) or from `options.headers.Authorization` (server).
1554
+ *
1555
+ * @example
1556
+ * await nulvless.auth.connect('github', { redirect_uri: 'https://myapp.com/settings/connections' })
1557
+ */
1558
+ async connect(provider, options) {
1559
+ const auth = await resolveBearer(this.http, this._getToken, options);
1560
+ if (!auth.ok) return { data: null, error: auth.error };
1561
+ const result = await this.http.get(
1562
+ route`/oauth/${provider}/connect`,
1563
+ {
1564
+ ...options,
1565
+ headers: { ...options?.headers, ...auth.headers },
1566
+ query: { ...options?.redirect_uri ? { redirect_uri: options.redirect_uri } : {} }
1567
+ }
1568
+ );
1569
+ if (!result.error && typeof window !== "undefined") {
1570
+ window.location.href = result.data.url;
1571
+ }
1572
+ return result;
1573
+ }
1574
+ /**
1575
+ * Signs in with an ID token issued by a third-party OAuth provider.
1576
+ *
1577
+ * Useful for mobile apps where you obtain the ID token natively (e.g. Google Sign-In SDK).
1578
+ *
1579
+ * Session is stored automatically when the response contains an access token.
1580
+ *
1581
+ * @example
1582
+ * const { data } = await nulvless.auth.signInWithIdToken('google', googleIdToken)
1583
+ */
1584
+ async signInWithIdToken(provider, idToken, options) {
1585
+ return this.persistSession(
1586
+ await this.http.get(route`/signin/${provider}/id_token`, {
1587
+ ...options,
1588
+ query: { id_token: idToken }
1589
+ })
1590
+ );
1591
+ }
1592
+ /**
1593
+ * Exchanges an OAuth authorization code for an access token
1594
+ * (`grant_type=authorization_code`).
1595
+ *
1596
+ * Must be called with `Content-Type: application/x-www-form-urlencoded`.
1597
+ * The `code` must be the authorization code received from the OAuth callback.
1598
+ *
1599
+ * Session is stored automatically when the response contains an access token.
1600
+ *
1601
+ * @example
1602
+ * const { data } = await nulvless.auth.exchangeCodeForToken('auth-code-from-callback')
1603
+ */
1604
+ async exchangeCodeForToken(code, options) {
1605
+ return this.persistSession(
1606
+ await this.http.post(
1607
+ "/token",
1608
+ { code },
1609
+ {
1610
+ ...options,
1611
+ query: { grant_type: "authorization_code" },
1612
+ headers: {
1613
+ ...options?.headers,
1614
+ "Content-Type": "application/x-www-form-urlencoded"
1615
+ }
1616
+ }
1617
+ )
1618
+ );
1619
+ }
1620
+ /**
1621
+ * Returns all sign-in methods, MFA factors, and OAuth providers configured
1622
+ * for the authenticated user on this platform.
1623
+ *
1624
+ * Requires authentication.
1625
+ */
1626
+ async getMethods(options) {
1627
+ const auth = await resolveBearer(this.http, this._getToken, options);
1628
+ if (!auth.ok) return { data: null, error: auth.error };
1629
+ return this.http.get("/methods", {
1630
+ ...options,
1631
+ headers: { ...options?.headers, ...auth.headers }
1632
+ });
1633
+ }
1634
+ // ─── Challenge ─────────────────────────────────────────────────────────────
1635
+ /**
1636
+ * Looks up whether an identifier exists and which sign-in method applies.
1637
+ *
1638
+ * Useful for adaptive sign-in UIs. May return a `challenge` directly
1639
+ * when the user's method is OTP-only.
1640
+ *
1641
+ * At least one of `email`, `phone`, or `username` must be provided.
1642
+ */
1643
+ lookup(params, options) {
1644
+ return this.http.post("/lookup", params, options);
1645
+ }
1646
+ /**
1647
+ * Verifies a challenge by submitting the token and the code entered by the user.
1648
+ *
1649
+ * Used for:
1650
+ * - OTP sign-in (`signInWithOTP` flow)
1651
+ * - MFA verification after `signIn`
1652
+ * - Sign-up confirmation (`signUp` challenge flow)
1653
+ *
1654
+ * Convenience alias for `auth.challenges.verify`. Session is stored
1655
+ * automatically when the response contains an access token. For the rest of
1656
+ * the challenge lifecycle (create, get, change) use `auth.challenges`.
1657
+ */
1658
+ async verifyChallenge(params, options) {
1659
+ return this.challenges.verify(params, options);
1660
+ }
1661
+ // ─── Account lifecycle ─────────────────────────────────────────────────────
1662
+ /**
1663
+ * Activates an account via the confirmation link sent to the user's email/phone.
1664
+ */
1665
+ activate(token, options) {
1666
+ return this.http.get("/activate", { ...options, query: { token } });
1667
+ }
1668
+ /**
1669
+ * Refreshes the access token using a refresh token (`grant_type=refresh_token`).
1670
+ * Session is updated automatically in storage.
1671
+ */
1672
+ async refreshToken(params, options) {
1673
+ return this.persistSession(
1674
+ await this.http.post("/token", params, {
1675
+ ...options,
1676
+ query: { grant_type: "refresh_token" }
1677
+ })
1678
+ );
1679
+ }
1680
+ /**
1681
+ * Sends a password-recovery email to the user.
1682
+ */
1683
+ recover(params, options) {
1684
+ return this.http.post("/recover", {
1685
+ ...params,
1686
+ ...options?.redirect_uri ? { redirect_uri: options.redirect_uri } : {}
1687
+ }, options);
1688
+ }
1689
+ /**
1690
+ * Resets the user's password using the token from the recovery email.
1691
+ */
1692
+ resetPassword(params, options) {
1693
+ return this.http.post("/recover/reset-password", params, options);
1694
+ }
1695
+ // ─── Session ───────────────────────────────────────────────────────────────
1696
+ /**
1697
+ * Validates the current session and optionally checks a grant.
1698
+ *
1699
+ * Requires authentication. Token is resolved automatically from storage
1700
+ * (browser) or from `options.headers.Authorization` (server).
1701
+ */
1702
+ async verify(grant, options) {
1703
+ const auth = await resolveBearer(this.http, this._getToken, options);
1704
+ if (!auth.ok) return { data: null, error: auth.error };
1705
+ return this.http.get("/session/validate", {
1706
+ ...options,
1707
+ headers: { ...options?.headers, ...auth.headers },
1708
+ query: {
1709
+ ...grant?.resource ? { resource: grant.resource } : {},
1710
+ ...grant?.scopes ? { scopes: grant.scopes.join(" ") } : {},
1711
+ ...grant?.roles ? { roles: grant.roles.join(" ") } : {},
1712
+ ...grant?.reject_assurance_level_error !== void 0 ? { reject_assurance_level_error: String(grant.reject_assurance_level_error) } : {}
1713
+ }
1714
+ });
1715
+ }
1716
+ /**
1717
+ * Returns the current session details.
1718
+ *
1719
+ * Requires authentication. Token is resolved automatically from storage
1720
+ * (browser) or from `options.headers.Authorization` (server).
1721
+ */
1722
+ async getSession(options) {
1723
+ const auth = await resolveBearer(this.http, this._getToken, options);
1724
+ if (!auth.ok) return { data: null, error: auth.error };
1725
+ return this.http.get("/session", {
1726
+ ...options,
1727
+ headers: { ...options?.headers, ...auth.headers }
1728
+ });
1729
+ }
1730
+ /**
1731
+ * Signs out the user and clears the stored session.
1732
+ *
1733
+ * - `'local'` (default) — invalidates only the current session.
1734
+ * - `'global'` — invalidates all sessions across all devices.
1735
+ * - `'others'` — invalidates all sessions except the current one.
1736
+ *
1737
+ * Requires authentication. Token is resolved automatically from storage
1738
+ * (browser) or from `options.headers.Authorization` (server).
1739
+ */
1740
+ async signOut(scope, options) {
1741
+ const auth = await resolveBearer(this.http, this._getToken, options);
1742
+ if (!auth.ok) return { data: null, error: auth.error };
1743
+ const result = await this.http.post("/signout", void 0, {
1744
+ ...options,
1745
+ headers: { ...options?.headers, ...auth.headers },
1746
+ ...scope ? { query: { scope } } : {}
1747
+ });
1748
+ if (!result.error) {
1749
+ await this.session?.clear();
1750
+ }
1751
+ return result;
1752
+ }
1753
+ };
1754
+ var AuthBrowserClient = class extends AuthClient {
1755
+ accountsManager;
1756
+ emitter;
1757
+ multiUser;
1758
+ tabs;
1759
+ constructor(http, session, storage, accountsKey, multiUser = false, autoRefresh = true) {
1760
+ super(http, session);
1761
+ this.multiUser = multiUser;
1762
+ this.accountsManager = new AccountsManager(storage, accountsKey);
1763
+ this.emitter = new AuthEventEmitter();
1764
+ this.tabs = new TabChannel(`nulvless.auth.${accountsKey}`, (message) => {
1765
+ this.emitter.emit(
1766
+ message.event,
1767
+ message.payload
1768
+ );
1769
+ });
1770
+ session.setOnTokenRefreshed((auth) => {
1771
+ if (this.multiUser) {
1772
+ void this.accountsManager.save(auth);
1773
+ }
1774
+ this.emitter.emit("token-refreshed", auth);
1775
+ this.tabs.post({ event: "token-refreshed", payload: auth });
1776
+ });
1777
+ session.setOnUnauthorized((failed) => {
1778
+ this.emitter.emit("unauthorized", failed ? toAuthenticated(failed) : {});
1779
+ });
1780
+ http.setOnSudoRequired(() => {
1781
+ this.emitter.emit("sudo-required");
1782
+ });
1783
+ if (autoRefresh) {
1784
+ session.startAutoRefresh();
1785
+ }
1786
+ }
1787
+ /**
1788
+ * Renews the access token now, sharing the attempt with any refresh already
1789
+ * in flight. Returns the new token, or `null` when the session could not be
1790
+ * recovered.
1791
+ */
1792
+ async refreshSession() {
1793
+ return this.session?.forceRefresh() ?? null;
1794
+ }
1795
+ /** Releases the refresh timer and the cross-tab channel. */
1796
+ dispose() {
1797
+ this.session?.stopAutoRefresh();
1798
+ this.tabs.close();
1799
+ }
1800
+ /**
1801
+ * Subscribes to an auth event.
1802
+ *
1803
+ * Returns an unsubscribe function — call it to stop listening.
1804
+ *
1805
+ * @example
1806
+ * const off = nulvless.auth.on('signed-in', (session) => {
1807
+ * console.log('signed in as', session.user?.id)
1808
+ * })
1809
+ * off() // unsubscribe
1810
+ */
1811
+ on(event, handler) {
1812
+ return this.emitter.on(event, handler);
1813
+ }
1814
+ // ─── Session persistence override ─────────────────────────────────────────
1815
+ async persistSession(result) {
1816
+ const out = await super.persistSession(result);
1817
+ if (!out.error) {
1818
+ if (out.data.access_token) {
1819
+ if (this.multiUser) {
1820
+ await this.accountsManager.save(out.data);
1821
+ }
1822
+ this.emitter.emit("signed-in", out.data);
1823
+ } else if (out.data.challenge) {
1824
+ this.emitter.emit("challenge", out.data);
1825
+ }
1826
+ }
1827
+ return out;
1828
+ }
1829
+ // ─── Active session helpers ────────────────────────────────────────────────
1830
+ /**
1831
+ * Returns the session currently stored in storage, or `null` if there is none.
1832
+ *
1833
+ * `expires_in` is the number of seconds remaining until expiry (may be negative
1834
+ * if the token is already expired).
1835
+ *
1836
+ * @example
1837
+ * const session = await nulvless.auth.get()
1838
+ * if (session) {
1839
+ * console.log(session.access_token, session.expires_in)
1840
+ * }
1841
+ */
1842
+ async get() {
1843
+ const stored = await this.session?.get();
1844
+ return stored ? toAuthenticated(stored) : null;
1845
+ }
1846
+ /**
1847
+ * Manually persists a session as the active session.
1848
+ *
1849
+ * Use this when you obtain an `Authenticated` outside the normal SDK flow
1850
+ * (e.g. server-side login, custom OAuth callback) and want the SDK to treat
1851
+ * it as the active session.
1852
+ *
1853
+ * @example
1854
+ * await nulvless.auth.set({ access_token: 'eyJ...', refresh_token: 'rt_...', expires_in: 3600 })
1855
+ */
1856
+ async set(data) {
1857
+ if (data.access_token) {
1858
+ await this.session?.save(data);
1859
+ }
1860
+ }
1861
+ // ─── Multi-account ─────────────────────────────────────────────────────────
1862
+ /**
1863
+ * Adds an account to the accounts store without switching the active session.
1864
+ *
1865
+ * The account is identified by the `user.id` in the response, or by the `sub`
1866
+ * claim in the access token JWT.
1867
+ *
1868
+ * Call this after a sign-in to register additional accounts. Accounts are
1869
+ * also saved automatically whenever a sign-in / sign-up succeeds.
1870
+ *
1871
+ * @example
1872
+ * const { data } = await nulvless.auth.signIn(params)
1873
+ * // already saved automatically — or add manually:
1874
+ * await nulvless.auth.add(data)
1875
+ */
1876
+ async add(auth) {
1877
+ if (!this.multiUser || !auth.access_token) return;
1878
+ await this.accountsManager.save(auth);
1879
+ }
1880
+ /**
1881
+ * Switches the active session to a previously saved account.
1882
+ *
1883
+ * Returns `true` on success, `false` if the account is not found.
1884
+ *
1885
+ * @example
1886
+ * const ok = await nulvless.auth.switch('user-id-123')
1887
+ */
1888
+ async switch(userId) {
1889
+ if (!this.multiUser) return false;
1890
+ const stored = await this.accountsManager.getSession(userId);
1891
+ if (!stored) return false;
1892
+ await this.session?.save(toAuthenticated(stored));
1893
+ this.emitter.emit("account-switched", { userId });
1894
+ return true;
1895
+ }
1896
+ /**
1897
+ * Returns all accounts currently saved in the accounts store.
1898
+ *
1899
+ * @example
1900
+ * const accounts = await nulvless.auth.list()
1901
+ * // [{ userId: '...', access_token: '...', expires_in: 3540 }]
1902
+ */
1903
+ list() {
1904
+ if (!this.multiUser) return Promise.resolve([]);
1905
+ return this.accountsManager.list();
1906
+ }
1907
+ /**
1908
+ * Removes a saved account from the accounts store.
1909
+ *
1910
+ * Does not affect the active session.
1911
+ *
1912
+ * @example
1913
+ * await nulvless.auth.remove('user-id-123')
1914
+ */
1915
+ remove(userId) {
1916
+ if (!this.multiUser) return Promise.resolve();
1917
+ return this.accountsManager.remove(userId);
1918
+ }
1919
+ /**
1920
+ * Drops the active session without ending it on the server.
1921
+ *
1922
+ * The account stays in the accounts store, so `switch` brings it back. Use
1923
+ * this when the user leaves the authenticated area to pick or add another
1924
+ * account: with no active session the automatic refresh goes quiet instead
1925
+ * of keeping a session warm that nothing is using.
1926
+ *
1927
+ * @example
1928
+ * await nulvless.auth.deselect()
1929
+ * // later
1930
+ * await nulvless.auth.switch('user-id-123')
1931
+ */
1932
+ async deselect() {
1933
+ const stored = await this.session?.get();
1934
+ if (!stored) return;
1935
+ if (this.multiUser) {
1936
+ await this.accountsManager.save(toAuthenticated(stored));
1937
+ }
1938
+ await this.session?.clear();
1939
+ }
1940
+ // ─── signOut override ──────────────────────────────────────────────────────
1941
+ async signOut(scope, options) {
1942
+ const stored = await this.session?.get();
1943
+ const userId = stored ? stored.user?.id ?? extractUserId({ access_token: stored.access_token }) : null;
1944
+ const result = await super.signOut(scope, options);
1945
+ if (!result.error) {
1946
+ if (this.multiUser && userId) {
1947
+ await this.accountsManager.remove(userId);
1948
+ }
1949
+ this.emitter.emit("signed-out", { ...userId ? { userId } : {} });
1950
+ }
1951
+ return result;
1952
+ }
1953
+ };
1954
+ var AuthServerClient = class extends AuthClient {
1955
+ /** User-facing grant management — invite, accept, revoke, etc. */
1956
+ grants;
1957
+ /** Platform administration — manage users, roles, and grants server-side. */
1958
+ admin;
1959
+ constructor(http) {
1960
+ super(http);
1961
+ this.grants = new GrantsClient(http, this._getToken);
1962
+ this.admin = new AdminClient(http);
1963
+ }
1964
+ };
1965
+
1966
+ // src/comms/templates/index.ts
1967
+ function templatePath(channel, slug, language) {
1968
+ return language ? route`/templates/${channel}/${slug}/${language}` : route`/templates/${channel}/${slug}`;
1969
+ }
1970
+ var TemplatesClient = class {
1971
+ constructor(http) {
1972
+ this.http = http;
1973
+ }
1974
+ http;
1975
+ /**
1976
+ * Creates a new notification template.
1977
+ *
1978
+ * - `email` channel: `subject` required, `url` not allowed.
1979
+ * - `sms` channel: `subject` and `url` not allowed.
1980
+ * - `webhook` channel: `url` required, `subject` not allowed.
1981
+ */
1982
+ create(params, options) {
1983
+ return this.http.post("/templates", params, options);
1984
+ }
1985
+ /**
1986
+ * Updates an existing template.
1987
+ *
1988
+ * `language` defaults to `'en-US'` when omitted.
1989
+ *
1990
+ * @example
1991
+ * await nulvless.comms.templates.update('email', 'welcome', { subject: 'Welcome aboard!' })
1992
+ */
1993
+ update(channel, slug, params, language, options) {
1994
+ return this.http.patch(templatePath(channel, slug, language), params, options);
1995
+ }
1996
+ /**
1997
+ * Deletes a template.
1998
+ *
1999
+ * `language` defaults to `'en-US'` when omitted.
2000
+ */
2001
+ delete(channel, slug, language, options) {
2002
+ return this.http.delete(templatePath(channel, slug, language), options);
2003
+ }
2004
+ /**
2005
+ * Returns a template by channel and slug, falling back to the common template
2006
+ * if no custom one exists.
2007
+ *
2008
+ * `language` defaults to `'en-US'` when omitted.
2009
+ *
2010
+ * The response includes `common: true` when the returned template is the
2011
+ * platform-level fallback rather than a custom override.
2012
+ */
2013
+ get(channel, slug, language, options) {
2014
+ return this.http.get(templatePath(channel, slug, language), options);
2015
+ }
2016
+ /**
2017
+ * Returns a paginated list of templates, optionally filtered by channel and language.
2018
+ *
2019
+ * @example
2020
+ * const { data } = await nulvless.comms.templates.list({ channel: 'email', page_size: 20 })
2021
+ */
2022
+ list(filters, options) {
2023
+ return this.http.get("/templates", {
2024
+ ...options,
2025
+ query: {
2026
+ ...filters?.page !== void 0 ? { page: filters.page } : {},
2027
+ ...filters?.page_size !== void 0 ? { page_size: filters.page_size } : {},
2028
+ ...filters?.sort ? { sort: filters.sort } : {},
2029
+ ...filters?.channel ? { channel: filters.channel } : {},
2030
+ ...filters?.language ? { language: filters.language } : {}
2031
+ }
2032
+ });
2033
+ }
2034
+ };
2035
+
2036
+ // src/comms/index.ts
2037
+ var EmailClient = class {
2038
+ constructor(http) {
2039
+ this.http = http;
2040
+ }
2041
+ http;
2042
+ /**
2043
+ * Sends a transactional email using a pre-configured template.
2044
+ *
2045
+ * Returns `null` on success — the API responds with `200` and no body.
2046
+ *
2047
+ * @example
2048
+ * const { error } = await nulvless.comms.email.send({
2049
+ * to: 'user@example.com',
2050
+ * template: 'welcome',
2051
+ * params: { name: 'Gleisson' },
2052
+ * })
2053
+ */
2054
+ send(params, options) {
2055
+ const { to, template, language, params: templateParams } = params;
2056
+ const path = language ? route`/notifications/email/${template}/${language}` : route`/notifications/email/${template}`;
2057
+ return this.http.post(path, { email: to, params: templateParams }, options);
2058
+ }
2059
+ /**
2060
+ * Sends a test email to validate the platform's email provider configuration.
2061
+ *
2062
+ * Returns `null` on success.
2063
+ *
2064
+ * @example
2065
+ * const { error } = await nulvless.comms.email.test('user@example.com')
2066
+ */
2067
+ test(email, options) {
2068
+ return this.http.post("/notifications/test/email", { email }, options);
2069
+ }
2070
+ };
2071
+ var SmsClient = class {
2072
+ constructor(http) {
2073
+ this.http = http;
2074
+ }
2075
+ http;
2076
+ /**
2077
+ * Sends a transactional SMS using a pre-configured template.
2078
+ *
2079
+ * Returns `null` on success — the API responds with `200` and no body.
2080
+ *
2081
+ * @example
2082
+ * const { error } = await nulvless.comms.sms.send({
2083
+ * phone: { country_code: '+55', number: '11999999999' },
2084
+ * template: 'verification-code',
2085
+ * params: { code: '123456' },
2086
+ * })
2087
+ */
2088
+ send(params, options) {
2089
+ const { phone, template, language, params: templateParams } = params;
2090
+ const path = language ? route`/notifications/sms/${template}/${language}` : route`/notifications/sms/${template}`;
2091
+ return this.http.post(path, { phone, params: templateParams }, options);
2092
+ }
2093
+ /**
2094
+ * Sends a test SMS to validate the platform's SMS provider configuration.
2095
+ *
2096
+ * Returns `null` on success.
2097
+ *
2098
+ * @example
2099
+ * const { error } = await nulvless.comms.sms.test('user@example.com')
2100
+ */
2101
+ test(email, options) {
2102
+ return this.http.post("/notifications/test/sms", { email }, options);
2103
+ }
2104
+ };
2105
+ var CommsClient = class {
2106
+ email;
2107
+ sms;
2108
+ templates;
2109
+ constructor(http) {
2110
+ this.email = new EmailClient(http);
2111
+ this.sms = new SmsClient(http);
2112
+ this.templates = new TemplatesClient(http);
2113
+ }
2114
+ };
2115
+
2116
+ // src/session/locks.ts
2117
+ var pendingByName = /* @__PURE__ */ new Map();
2118
+ function webLocks() {
2119
+ const manager = globalThis.navigator?.locks;
2120
+ return typeof manager?.request === "function" ? manager : null;
2121
+ }
2122
+ async function runExclusive(name, task, options = {}) {
2123
+ const manager = webLocks();
2124
+ if (manager) {
2125
+ return await manager.request(
2126
+ name,
2127
+ { mode: "exclusive", ...options.skipWhenBusy ? { ifAvailable: true } : {} },
2128
+ async (lock) => lock === null ? { ran: false } : { ran: true, value: await task() }
2129
+ );
2130
+ }
2131
+ const previous = pendingByName.get(name);
2132
+ if (previous && options.skipWhenBusy) {
2133
+ return { ran: false };
2134
+ }
2135
+ const run = (previous ?? Promise.resolve()).catch(() => void 0).then(task);
2136
+ const slot = run.catch(() => void 0);
2137
+ pendingByName.set(name, slot);
2138
+ try {
2139
+ return { ran: true, value: await run };
2140
+ } finally {
2141
+ if (pendingByName.get(name) === slot) {
2142
+ pendingByName.delete(name);
2143
+ }
2144
+ }
2145
+ }
2146
+
2147
+ // src/session/refreshFailure.ts
2148
+ var REFUSAL_CODES = /* @__PURE__ */ new Set([
2149
+ "unauthorized",
2150
+ "refresh_token_not_found",
2151
+ "refresh_token_already_used",
2152
+ "refresh_token_not_issued_here",
2153
+ "user_banned"
2154
+ ]);
2155
+ function isTemporaryFailure(error) {
2156
+ return error.status_code === 0 || error.status_code === 408 || error.status_code === 429 || error.status_code >= 500;
2157
+ }
2158
+ function isSessionRefused(error) {
2159
+ return REFUSAL_CODES.has(error.code) || error.status_code === 401;
2160
+ }
2161
+
2162
+ // src/session/manager.ts
2163
+ var REFRESH_MARGIN_IN_MS = 9e4;
2164
+ var TICK_INTERVAL_IN_MS = 3e4;
2165
+ var RETRY_BACKOFF_BASE_IN_MS = 200;
2166
+ var MAX_RETRIES_ON_TEMPORARY_FAILURE = 3;
2167
+ function sleep(ms) {
2168
+ return new Promise((resolve) => setTimeout(resolve, ms));
2169
+ }
2170
+ function documentIsHidden() {
2171
+ return globalThis.document?.hidden === true;
2172
+ }
2173
+ function isBrowser() {
2174
+ const scope = globalThis;
2175
+ return typeof scope.window !== "undefined" && typeof scope.document !== "undefined";
2176
+ }
2177
+ var SessionManager = class {
2178
+ constructor(storage, sessionKey) {
2179
+ this.storage = storage;
2180
+ this.sessionKey = sessionKey;
2181
+ this.lockName = `nulvless.session.${sessionKey}`;
2182
+ }
2183
+ storage;
2184
+ refreshFn;
2185
+ onTokenRefreshed;
2186
+ onUnauthorized;
2187
+ sessionKey;
2188
+ lockName;
2189
+ inFlight = null;
2190
+ ticker;
2191
+ onVisibilityChange;
2192
+ /**
2193
+ * Injected by `AuthClient` after construction to avoid circular dependency.
2194
+ * Called automatically when the access token is about to expire.
2195
+ */
2196
+ setRefreshFn(fn) {
2197
+ this.refreshFn = fn;
2198
+ }
2199
+ /** Called after a successful automatic token refresh. */
2200
+ setOnTokenRefreshed(cb) {
2201
+ this.onTokenRefreshed = cb;
2202
+ }
2203
+ /**
2204
+ * Called when the SDK cannot recover the session:
2205
+ * - no refresh token present, or
2206
+ * - the refresh request was refused by the server.
2207
+ */
2208
+ setOnUnauthorized(cb) {
2209
+ this.onUnauthorized = cb;
2210
+ }
2211
+ async save(auth) {
2212
+ if (!auth.access_token) return;
2213
+ const session = toStoredSession({ ...auth, access_token: auth.access_token });
2214
+ await this.storage.setItem(this.sessionKey, JSON.stringify(session));
2215
+ }
2216
+ async get() {
2217
+ const raw = await this.storage.getItem(this.sessionKey);
2218
+ if (!raw) return null;
2219
+ try {
2220
+ return JSON.parse(raw);
2221
+ } catch {
2222
+ return null;
2223
+ }
2224
+ }
2225
+ async clear() {
2226
+ await this.storage.removeItem(this.sessionKey);
2227
+ }
2228
+ expiresSoon(session) {
2229
+ return session.expires_at - Date.now() < REFRESH_MARGIN_IN_MS;
2230
+ }
2231
+ refresh(request) {
2232
+ if (this.inFlight) {
2233
+ return this.inFlight;
2234
+ }
2235
+ this.inFlight = this.runExclusiveRefresh(request).finally(() => {
2236
+ this.inFlight = null;
2237
+ });
2238
+ return this.inFlight;
2239
+ }
2240
+ async runExclusiveRefresh(request) {
2241
+ const outcome = await runExclusive(this.lockName, () => this.refreshHoldingLock(request));
2242
+ return outcome.ran ? outcome.value : null;
2243
+ }
2244
+ async refreshHoldingLock(request) {
2245
+ const session = await this.get();
2246
+ if (!session) {
2247
+ if (request.deauthOnFailure) this.onUnauthorized?.(null);
2248
+ return null;
2249
+ }
2250
+ const alreadyRenewed = request.expiredToken ? session.access_token !== request.expiredToken : !this.expiresSoon(session);
2251
+ if (alreadyRenewed) {
2252
+ return session.access_token;
2253
+ }
2254
+ if (!session.refresh_token || !this.refreshFn) {
2255
+ if (request.deauthOnFailure) this.onUnauthorized?.(session);
2256
+ return null;
2257
+ }
2258
+ return this.callRefreshFn(session, request);
2259
+ }
2260
+ async callRefreshFn(session, request) {
2261
+ for (let attempt = 0; attempt <= MAX_RETRIES_ON_TEMPORARY_FAILURE; attempt += 1) {
2262
+ if (attempt > 0) {
2263
+ await sleep(RETRY_BACKOFF_BASE_IN_MS * 2 ** (attempt - 1));
2264
+ }
2265
+ const result = await this.refreshFn(session.refresh_token);
2266
+ if (result.data?.access_token) {
2267
+ await this.save(result.data);
2268
+ this.onTokenRefreshed?.(result.data);
2269
+ return result.data.access_token;
2270
+ }
2271
+ const error = result.error;
2272
+ if (error && isTemporaryFailure(error)) {
2273
+ continue;
2274
+ }
2275
+ if (request.deauthOnFailure && (!error || isSessionRefused(error))) {
2276
+ await this.clear();
2277
+ this.onUnauthorized?.(session);
2278
+ }
2279
+ return null;
2280
+ }
2281
+ return null;
2282
+ }
2283
+ /**
2284
+ * Forces a token refresh regardless of expiry.
2285
+ * Used for reactive refresh after a 401/403 response.
2286
+ *
2287
+ * Returns the new access token, or `null` if refresh fails.
2288
+ *
2289
+ * `deauthOnFailure` (default `true`) controls side effects when the refresh
2290
+ * itself is refused: clearing the stored session and invoking `onUnauthorized`.
2291
+ * Pass `false` for soft refreshes (e.g. 403/forbidden_resource_access) where
2292
+ * the session must remain intact so the caller can surface the API error.
2293
+ */
2294
+ async forceRefresh(options = {}) {
2295
+ const expiredToken = options.expiredToken ?? (await this.get())?.access_token;
2296
+ return this.refresh({
2297
+ deauthOnFailure: options.deauthOnFailure ?? true,
2298
+ ...expiredToken !== void 0 ? { expiredToken } : {}
2299
+ });
2300
+ }
2301
+ /**
2302
+ * Returns a valid access token, refreshing it automatically if it is
2303
+ * expired or about to expire (within the refresh margin).
2304
+ *
2305
+ * Returns `null` when there is no session or the refresh fails.
2306
+ */
2307
+ async getValidToken() {
2308
+ const session = await this.get();
2309
+ if (!session) return null;
2310
+ if (!this.expiresSoon(session)) return session.access_token;
2311
+ return this.refresh({ deauthOnFailure: true });
2312
+ }
2313
+ startAutoRefresh() {
2314
+ if (this.ticker || !isBrowser()) return;
2315
+ this.ticker = setInterval(() => void this.tick(), TICK_INTERVAL_IN_MS);
2316
+ this.ticker.unref?.();
2317
+ this.onVisibilityChange = () => {
2318
+ if (!documentIsHidden()) void this.tick();
2319
+ };
2320
+ globalThis.document.addEventListener(
2321
+ "visibilitychange",
2322
+ this.onVisibilityChange
2323
+ );
2324
+ }
2325
+ stopAutoRefresh() {
2326
+ if (this.ticker) {
2327
+ clearInterval(this.ticker);
2328
+ this.ticker = void 0;
2329
+ }
2330
+ if (this.onVisibilityChange && isBrowser()) {
2331
+ globalThis.document.removeEventListener(
2332
+ "visibilitychange",
2333
+ this.onVisibilityChange
2334
+ );
2335
+ }
2336
+ this.onVisibilityChange = void 0;
2337
+ }
2338
+ async tick() {
2339
+ if (documentIsHidden() || this.inFlight) return;
2340
+ try {
2341
+ const session = await this.get();
2342
+ if (!session?.refresh_token || !this.expiresSoon(session)) return;
2343
+ await runExclusive(this.lockName, () => this.refreshHoldingLock({ deauthOnFailure: true }), {
2344
+ skipWhenBusy: true
2345
+ });
2346
+ } catch (error) {
2347
+ console.error("[nulvless] scheduled token renewal failed", error);
2348
+ }
2349
+ }
2350
+ };
2351
+
2352
+ // src/storage/adapters/local.ts
2353
+ var LocalStorageAdapter = class {
2354
+ async getItem(key) {
2355
+ return localStorage.getItem(key);
2356
+ }
2357
+ async setItem(key, value) {
2358
+ localStorage.setItem(key, value);
2359
+ }
2360
+ async removeItem(key) {
2361
+ localStorage.removeItem(key);
2362
+ }
2363
+ };
2364
+
2365
+ // src/storage/adapters/session.ts
2366
+ var SessionStorageAdapter = class {
2367
+ async getItem(key) {
2368
+ return sessionStorage.getItem(key);
2369
+ }
2370
+ async setItem(key, value) {
2371
+ sessionStorage.setItem(key, value);
2372
+ }
2373
+ async removeItem(key) {
2374
+ sessionStorage.removeItem(key);
2375
+ }
2376
+ };
2377
+
2378
+ // src/storage/adapters/memory.ts
2379
+ var MemoryStorageAdapter = class {
2380
+ store = /* @__PURE__ */ new Map();
2381
+ async getItem(key) {
2382
+ return this.store.get(key) ?? null;
2383
+ }
2384
+ async setItem(key, value) {
2385
+ this.store.set(key, value);
2386
+ }
2387
+ async removeItem(key) {
2388
+ this.store.delete(key);
2389
+ }
2390
+ };
2391
+
2392
+ // src/storage/adapters/cache.ts
2393
+ var CACHE_NAME = "nulvless";
2394
+ var CacheStorageAdapter = class {
2395
+ key(k) {
2396
+ return `https://nulvless.storage/${encodeURIComponent(k)}`;
2397
+ }
2398
+ async getItem(key) {
2399
+ const cache = await caches.open(CACHE_NAME);
2400
+ const response = await cache.match(this.key(key));
2401
+ return response ? response.text() : null;
2402
+ }
2403
+ async setItem(key, value) {
2404
+ const cache = await caches.open(CACHE_NAME);
2405
+ await cache.put(this.key(key), new Response(value));
2406
+ }
2407
+ async removeItem(key) {
2408
+ const cache = await caches.open(CACHE_NAME);
2409
+ await cache.delete(this.key(key));
2410
+ }
2411
+ };
2412
+
2413
+ // src/storage/index.ts
2414
+ var Storage = {
2415
+ /** `localStorage` — data survives page reloads and browser restarts. */
2416
+ Local: new LocalStorageAdapter(),
2417
+ /** `sessionStorage` — data is cleared when the tab or browser is closed. */
2418
+ Session: new SessionStorageAdapter(),
2419
+ /** In-memory `Map` — data is lost on page reload. Good for SSR and tests. */
2420
+ Memory: new MemoryStorageAdapter(),
2421
+ /** Cache Storage API — survives reloads, available in service workers. */
2422
+ Cache: new CacheStorageAdapter()
2423
+ };
2424
+
2425
+ // src/client.ts
2426
+ function defaultStorage() {
2427
+ return typeof window !== "undefined" ? Storage.Local : new MemoryStorageAdapter();
2428
+ }
2429
+ var NulvlessClient = class {
2430
+ auth;
2431
+ constructor(baseUrl, publicKey, options) {
2432
+ const base = baseUrl.replace(/\/+$/, "");
2433
+ const subdomain = new URL(base).hostname.split(".")[0] ?? "app";
2434
+ const tokenKey = options?.storageKeys?.token ?? `nlv-${subdomain}-token`;
2435
+ const accountsKey = options?.storageKeys?.accounts ?? `${tokenKey}-accounts`;
2436
+ const storage = options?.storage ?? defaultStorage();
2437
+ const session = new SessionManager(storage, tokenKey);
2438
+ this.auth = new AuthBrowserClient(
2439
+ new HttpClient({
2440
+ baseUrl: `${base}/auth`,
2441
+ apiKey: publicKey,
2442
+ ...options,
2443
+ onTokenExpired: ({ deauthOnFailure, expiredToken }) => session.forceRefresh({
2444
+ deauthOnFailure,
2445
+ ...expiredToken ? { expiredToken } : {}
2446
+ })
2447
+ }),
2448
+ session,
2449
+ storage,
2450
+ accountsKey,
2451
+ options?.multiUser ?? false,
2452
+ options?.autoRefresh ?? true
2453
+ );
2454
+ }
2455
+ };
2456
+ var NulvlessServerClient = class {
2457
+ auth;
2458
+ comms;
2459
+ constructor(baseUrl, privateKey, options) {
2460
+ const base = baseUrl.replace(/\/+$/, "");
2461
+ this.auth = new AuthServerClient(new HttpClient({ baseUrl: `${base}/auth`, apiKey: privateKey, ...options }));
2462
+ this.comms = new CommsClient(new HttpClient({ baseUrl: `${base}/notifications`, apiKey: privateKey, ...options }));
2463
+ }
2464
+ };
2465
+
2466
+ // src/helpers/forward-user-headers.ts
2467
+ var TRACKING_HEADERS = [
2468
+ "authorization",
2469
+ "user-agent",
2470
+ "host",
2471
+ "origin",
2472
+ "referer",
2473
+ "accept",
2474
+ "accept-language",
2475
+ "sec-fetch-dest",
2476
+ "x-forwarded-for",
2477
+ "x-forwarded-host",
2478
+ "x-forwarded-proto",
2479
+ "cloudfront-forwarded-proto"
2480
+ ];
2481
+ function forwardUserHeaders(req) {
2482
+ const result = {};
2483
+ if (typeof Request !== "undefined" && req instanceof Request) {
2484
+ for (const key of TRACKING_HEADERS) {
2485
+ const val = req.headers.get(key);
2486
+ if (val) result[key] = val;
2487
+ }
2488
+ return result;
2489
+ }
2490
+ const nodeHeaders = req.headers;
2491
+ for (const key of TRACKING_HEADERS) {
2492
+ const val = nodeHeaders[key];
2493
+ if (val) result[key] = Array.isArray(val) ? val[0] : val;
2494
+ }
2495
+ return result;
2496
+ }
2497
+
2498
+ // src/helpers/auth-cache-headers.ts
2499
+ var AUTH_CACHE_HEADERS = {
2500
+ "Cache-Control": "private, no-cache, no-store, must-revalidate, max-age=0",
2501
+ Expires: "0",
2502
+ Pragma: "no-cache"
2503
+ };
2504
+ function isNodeResponse(response) {
2505
+ return typeof response.setHeader === "function";
2506
+ }
2507
+ function isWebResponse(response) {
2508
+ const headers = response.headers;
2509
+ return typeof headers?.set === "function";
2510
+ }
2511
+ function applyAuthCacheHeaders(response) {
2512
+ const entries = Object.entries(AUTH_CACHE_HEADERS);
2513
+ if (isWebResponse(response)) {
2514
+ for (const [name, value] of entries) response.headers.set(name, value);
2515
+ } else if (isNodeResponse(response)) {
2516
+ for (const [name, value] of entries) response.setHeader(name, value);
2517
+ }
2518
+ return response;
2519
+ }
2520
+
2521
+ // src/index.ts
2522
+ function createClient(url, publicKey, options) {
2523
+ return new NulvlessClient(url, publicKey, options);
2524
+ }
2525
+ function createServerClient(url, privateKey, options) {
2526
+ return new NulvlessServerClient(url, privateKey, options);
2527
+ }
2528
+
2529
+ exports.AUTH_CACHE_HEADERS = AUTH_CACHE_HEADERS;
2530
+ exports.AdminClient = AdminClient;
2531
+ exports.AuthBrowserClient = AuthBrowserClient;
2532
+ exports.AuthClient = AuthClient;
2533
+ exports.AuthEventEmitter = AuthEventEmitter;
2534
+ exports.AuthServerClient = AuthServerClient;
2535
+ exports.CommsClient = CommsClient;
2536
+ exports.EmailClient = EmailClient;
2537
+ exports.FactorsClient = FactorsClient;
2538
+ exports.GrantsClient = GrantsClient;
2539
+ exports.HttpClient = HttpClient;
2540
+ exports.NulvlessClient = NulvlessClient;
2541
+ exports.NulvlessException = NulvlessException;
2542
+ exports.NulvlessServerClient = NulvlessServerClient;
2543
+ exports.OAuthClient = OAuthClient;
2544
+ exports.OAuthGrantsClient = OAuthGrantsClient;
2545
+ exports.SecurityClient = SecurityClient;
2546
+ exports.SessionManager = SessionManager;
2547
+ exports.SessionsClient = SessionsClient;
2548
+ exports.SmsClient = SmsClient;
2549
+ exports.Storage = Storage;
2550
+ exports.TemplatesClient = TemplatesClient;
2551
+ exports.UsersClient = UsersClient;
2552
+ exports.applyAuthCacheHeaders = applyAuthCacheHeaders;
2553
+ exports.createClient = createClient;
2554
+ exports.createServerClient = createServerClient;
2555
+ exports.forwardUserHeaders = forwardUserHeaders;
2556
+ //# sourceMappingURL=index.cjs.map
2557
+ //# sourceMappingURL=index.cjs.map