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