@corvushold/guard-sdk 0.5.3

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,1010 @@
1
+ 'use strict';
2
+
3
+ var otplib = require('otplib');
4
+
5
+ // src/errors.ts
6
+ var ApiError = class extends Error {
7
+ constructor(params) {
8
+ super(params.message || `HTTP ${params.status}`);
9
+ this.name = "ApiError";
10
+ this.status = params.status;
11
+ this.code = params.code;
12
+ this.requestId = params.requestId;
13
+ this.raw = params.raw;
14
+ this.headers = params.headers;
15
+ }
16
+ };
17
+ var RateLimitError = class extends ApiError {
18
+ constructor(params) {
19
+ super(params);
20
+ this.name = "RateLimitError";
21
+ this.retryAfter = params.retryAfter;
22
+ this.nextRetryAt = params.nextRetryAt;
23
+ }
24
+ };
25
+ function isApiError(e) {
26
+ return e instanceof ApiError;
27
+ }
28
+ function isRateLimitError(e) {
29
+ return e instanceof RateLimitError;
30
+ }
31
+
32
+ // src/rateLimit.ts
33
+ function parseRetryAfter(retryAfter) {
34
+ if (!retryAfter) return {};
35
+ const trimmed = retryAfter.trim();
36
+ const secs = Number(trimmed);
37
+ if (!Number.isNaN(secs)) {
38
+ return { seconds: secs > 0 ? secs : void 0, nextRetryAt: secs > 0 ? new Date(Date.now() + secs * 1e3) : void 0 };
39
+ }
40
+ const date = new Date(trimmed);
41
+ if (!Number.isNaN(date.getTime())) {
42
+ const ms = date.getTime() - Date.now();
43
+ const seconds = ms > 0 ? Math.ceil(ms / 1e3) : void 0;
44
+ return { seconds, nextRetryAt: ms > 0 ? date : void 0 };
45
+ }
46
+ return {};
47
+ }
48
+ function toHeadersMap(headers) {
49
+ const obj = {};
50
+ headers.forEach((v, k) => {
51
+ obj[k] = v;
52
+ });
53
+ return obj;
54
+ }
55
+ function buildRateLimitError(args) {
56
+ const { seconds, nextRetryAt } = parseRetryAfter(args.headers.get("retry-after"));
57
+ return new RateLimitError({
58
+ status: args.status,
59
+ message: args.message,
60
+ requestId: args.requestId,
61
+ headers: toHeadersMap(args.headers),
62
+ retryAfter: seconds,
63
+ nextRetryAt,
64
+ raw: args.raw
65
+ });
66
+ }
67
+
68
+ // src/tokens.ts
69
+ var noopStorage = {
70
+ getAccessToken: () => null,
71
+ setAccessToken: () => {
72
+ },
73
+ getRefreshToken: () => null,
74
+ setRefreshToken: () => {
75
+ },
76
+ clear: () => {
77
+ }
78
+ };
79
+
80
+ // src/storage/inMemory.ts
81
+ var InMemoryStorage = class {
82
+ constructor() {
83
+ this.accessToken = null;
84
+ this.refreshToken = null;
85
+ }
86
+ getAccessToken() {
87
+ return this.accessToken;
88
+ }
89
+ setAccessToken(token) {
90
+ this.accessToken = token ?? null;
91
+ }
92
+ getRefreshToken() {
93
+ return this.refreshToken;
94
+ }
95
+ setRefreshToken(token) {
96
+ this.refreshToken = token ?? null;
97
+ }
98
+ clear() {
99
+ this.accessToken = null;
100
+ this.refreshToken = null;
101
+ }
102
+ };
103
+
104
+ // src/storage/webLocalStorage.ts
105
+ var ACCESS_KEY = "guard_access_token";
106
+ var REFRESH_KEY = "guard_refresh_token";
107
+ function isBrowser() {
108
+ return typeof window !== "undefined" && typeof window.localStorage !== "undefined";
109
+ }
110
+ var WebLocalStorage = class {
111
+ constructor(prefix = "") {
112
+ this.prefix = prefix ? `${prefix}:` : "";
113
+ }
114
+ k(key) {
115
+ return `${this.prefix}${key}`;
116
+ }
117
+ getAccessToken() {
118
+ if (!isBrowser()) return null;
119
+ return window.localStorage.getItem(this.k(ACCESS_KEY));
120
+ }
121
+ setAccessToken(token) {
122
+ if (!isBrowser()) return;
123
+ if (token == null) window.localStorage.removeItem(this.k(ACCESS_KEY));
124
+ else window.localStorage.setItem(this.k(ACCESS_KEY), token);
125
+ }
126
+ getRefreshToken() {
127
+ if (!isBrowser()) return null;
128
+ return window.localStorage.getItem(this.k(REFRESH_KEY));
129
+ }
130
+ setRefreshToken(token) {
131
+ if (!isBrowser()) return;
132
+ if (token == null) window.localStorage.removeItem(this.k(REFRESH_KEY));
133
+ else window.localStorage.setItem(this.k(REFRESH_KEY), token);
134
+ }
135
+ clear() {
136
+ if (!isBrowser()) return;
137
+ window.localStorage.removeItem(this.k(ACCESS_KEY));
138
+ window.localStorage.removeItem(this.k(REFRESH_KEY));
139
+ }
140
+ };
141
+
142
+ // src/storage/reactNative.ts
143
+ var ACCESS_KEY2 = "guard_access_token";
144
+ var REFRESH_KEY2 = "guard_refresh_token";
145
+ function reactNativeStorageAdapter(AsyncStorage, prefix = "") {
146
+ const p = prefix ? `${prefix}:` : "";
147
+ const k = (key) => `${p}${key}`;
148
+ return {
149
+ async getAccessToken() {
150
+ return AsyncStorage.getItem(k(ACCESS_KEY2));
151
+ },
152
+ async setAccessToken(token) {
153
+ if (token == null) return AsyncStorage.removeItem(k(ACCESS_KEY2));
154
+ return AsyncStorage.setItem(k(ACCESS_KEY2), token);
155
+ },
156
+ async getRefreshToken() {
157
+ return AsyncStorage.getItem(k(REFRESH_KEY2));
158
+ },
159
+ async setRefreshToken(token) {
160
+ if (token == null) return AsyncStorage.removeItem(k(REFRESH_KEY2));
161
+ return AsyncStorage.setItem(k(REFRESH_KEY2), token);
162
+ },
163
+ async clear() {
164
+ await AsyncStorage.removeItem(k(ACCESS_KEY2));
165
+ await AsyncStorage.removeItem(k(REFRESH_KEY2));
166
+ }
167
+ };
168
+ }
169
+
170
+ // src/http/interceptors.ts
171
+ function applyRequestInterceptors(input, init, interceptors) {
172
+ if (!interceptors || interceptors.length === 0) return [input, init];
173
+ let chain = Promise.resolve([input, init]);
174
+ for (const fn of interceptors) {
175
+ chain = chain.then(([i, n]) => Promise.resolve(fn(i, n)));
176
+ }
177
+ return chain;
178
+ }
179
+ async function applyResponseInterceptors(response, interceptors) {
180
+ if (!interceptors || interceptors.length === 0) return response;
181
+ let res = response;
182
+ for (const fn of interceptors) {
183
+ res = await Promise.resolve(fn(res));
184
+ }
185
+ return res;
186
+ }
187
+
188
+ // src/http/transport.ts
189
+ var HttpClient = class {
190
+ constructor(opts) {
191
+ this.baseUrl = opts.baseUrl.replace(/\/$/, "");
192
+ const fiRaw = opts.fetchImpl ?? globalThis.fetch;
193
+ if (!fiRaw) throw new Error("No fetch implementation provided and global fetch is unavailable");
194
+ this.fetchImpl = ((input, init) => fiRaw.call(globalThis, input, init));
195
+ this.interceptors = opts.interceptors;
196
+ this.clientHeader = opts.clientHeader ?? "ts-sdk";
197
+ this.defaultHeaders = { ...opts.defaultHeaders ?? {} };
198
+ this.credentials = opts.credentials;
199
+ }
200
+ buildUrl(path) {
201
+ if (/^https?:\/\//i.test(path)) return path;
202
+ if (!path.startsWith("/")) path = `/${path}`;
203
+ return `${this.baseUrl}${path}`;
204
+ }
205
+ async request(path, init = {}) {
206
+ const url = this.buildUrl(path);
207
+ const headers = new Headers(init.headers || {});
208
+ if (!headers.has("content-type")) headers.set("content-type", "application/json");
209
+ if (!headers.has("accept")) headers.set("accept", "application/json");
210
+ if (this.clientHeader && !headers.has("x-guard-client")) {
211
+ headers.set("x-guard-client", this.clientHeader);
212
+ }
213
+ for (const [k, v] of Object.entries(this.defaultHeaders)) {
214
+ if (!headers.has(k)) headers.set(k, v);
215
+ }
216
+ const reqInit = { ...init, headers };
217
+ if (!("credentials" in reqInit) && this.credentials) {
218
+ reqInit.credentials = this.credentials;
219
+ }
220
+ const [finalUrl, finalInit] = await applyRequestInterceptors(url, reqInit, this.interceptors?.request);
221
+ const res = await this.fetchImpl(finalUrl, finalInit);
222
+ const res2 = await applyResponseInterceptors(res, this.interceptors?.response);
223
+ const requestId = res2.headers.get("x-request-id") || void 0;
224
+ const status = res2.status;
225
+ if (!res2.ok) {
226
+ let body = void 0;
227
+ try {
228
+ const ct2 = res2.headers.get("content-type") || "";
229
+ if (ct2.includes("application/json")) body = await res2.clone().json();
230
+ else body = await res2.clone().text();
231
+ } catch {
232
+ }
233
+ if (status === 429) {
234
+ throw buildRateLimitError({ status, message: body && body.message || "Too Many Requests", requestId, headers: res2.headers, raw: body });
235
+ }
236
+ throw new ApiError({
237
+ status,
238
+ message: body && body.message || res2.statusText || `HTTP ${status}`,
239
+ code: body && body.code ? String(body.code) : void 0,
240
+ requestId,
241
+ headers: toHeadersMap(res2.headers),
242
+ raw: body
243
+ });
244
+ }
245
+ let data = void 0;
246
+ const ct = res2.headers.get("content-type") || "";
247
+ if (status !== 204) {
248
+ if (ct.includes("application/json")) data = await res2.json();
249
+ else data = await res2.text();
250
+ }
251
+ return {
252
+ data,
253
+ meta: { status, requestId, headers: toHeadersMap(res2.headers) }
254
+ };
255
+ }
256
+ // Low-level raw request that returns the Response without throwing on non-2xx.
257
+ // Useful for endpoints like SSO start that intentionally return 3xx redirects.
258
+ async requestRaw(path, init = {}) {
259
+ const url = this.buildUrl(path);
260
+ const headers = new Headers(init.headers || {});
261
+ if (!headers.has("content-type")) headers.set("content-type", "application/json");
262
+ if (!headers.has("accept")) headers.set("accept", "application/json");
263
+ if (this.clientHeader && !headers.has("x-guard-client")) {
264
+ headers.set("x-guard-client", this.clientHeader);
265
+ }
266
+ for (const [k, v] of Object.entries(this.defaultHeaders)) {
267
+ if (!headers.has(k)) headers.set(k, v);
268
+ }
269
+ const reqInit = { ...init, headers };
270
+ if (!("credentials" in reqInit) && this.credentials) {
271
+ reqInit.credentials = this.credentials;
272
+ }
273
+ const [finalUrl, finalInit] = await applyRequestInterceptors(url, reqInit, this.interceptors?.request);
274
+ const res = await this.fetchImpl(finalUrl, finalInit);
275
+ const res2 = await applyResponseInterceptors(res, this.interceptors?.response);
276
+ return res2;
277
+ }
278
+ };
279
+
280
+ // package.json
281
+ var package_default = {
282
+ version: "0.5.3"};
283
+
284
+ // src/client.ts
285
+ function isTenantSelectionRequired(data) {
286
+ const d = data;
287
+ return !!d && d.error === "tenant_selection_required" && typeof d.message === "string" && Array.isArray(d.tenants);
288
+ }
289
+ function isTokensResp(data) {
290
+ const d = data;
291
+ return !!d && (typeof d.access_token === "string" || typeof d.refresh_token === "string");
292
+ }
293
+ function isMfaChallengeResp(data) {
294
+ const d = data;
295
+ return !!d && typeof d.challenge_token === "string";
296
+ }
297
+ var GuardClient = class {
298
+ constructor(opts) {
299
+ this.baseUrl = opts.baseUrl;
300
+ this.tenantId = opts.tenantId;
301
+ this.storage = opts.storage ?? new InMemoryStorage();
302
+ const mode = opts.authMode ?? "bearer";
303
+ const authHeaderInterceptor = async (input, init) => {
304
+ const headers = new Headers(init.headers || {});
305
+ if (mode === "bearer") {
306
+ const token = await Promise.resolve(this.storage.getAccessToken());
307
+ if (token && !headers.has("authorization")) {
308
+ headers.set("authorization", `Bearer ${token}`);
309
+ }
310
+ } else if (mode === "cookie") {
311
+ headers.set("X-Auth-Mode", "cookie");
312
+ }
313
+ return [input, { ...init, headers }];
314
+ };
315
+ const defaultHeaders = { ...opts.defaultHeaders ?? {} };
316
+ const clientHeader = `ts-sdk/${package_default.version}`;
317
+ let credentialsOpt = void 0;
318
+ if (mode === "cookie") {
319
+ credentialsOpt = "include";
320
+ }
321
+ this.http = new HttpClient({
322
+ baseUrl: opts.baseUrl,
323
+ fetchImpl: opts.fetchImpl,
324
+ clientHeader,
325
+ defaultHeaders,
326
+ interceptors: { request: [authHeaderInterceptor] },
327
+ credentials: credentialsOpt
328
+ });
329
+ }
330
+ // Low-level request passthrough (internal usage by methods)
331
+ async request(path, init) {
332
+ return this.http.request(path, init);
333
+ }
334
+ persistTokensFrom(data) {
335
+ try {
336
+ if (isTokensResp(data)) {
337
+ const access = data.access_token ?? null;
338
+ const refresh = data.refresh_token ?? null;
339
+ if (access !== void 0) {
340
+ void this.storage.setAccessToken(access);
341
+ }
342
+ if (refresh !== void 0) {
343
+ void this.storage.setRefreshToken(refresh);
344
+ }
345
+ }
346
+ } catch (_) {
347
+ }
348
+ }
349
+ buildQuery(params) {
350
+ const usp = new URLSearchParams();
351
+ for (const [k, v] of Object.entries(params)) {
352
+ if (v === void 0 || v === null) continue;
353
+ usp.set(k, String(v));
354
+ }
355
+ const qs = usp.toString();
356
+ return qs ? `?${qs}` : "";
357
+ }
358
+ // Auth: Password login -> returns tokens (200) or MFA challenge (202)
359
+ async passwordLogin(body) {
360
+ const res = await this.request("/v1/auth/password/login", {
361
+ method: "POST",
362
+ body: JSON.stringify({ ...body, tenant_id: body.tenant_id ?? this.tenantId })
363
+ });
364
+ if (res.meta.status === 200) this.persistTokensFrom(res.data);
365
+ return res;
366
+ }
367
+ // Auth: Password signup -> returns tokens (201 Created)
368
+ async passwordSignup(body) {
369
+ const res = await this.request("/v1/auth/password/signup", {
370
+ method: "POST",
371
+ body: JSON.stringify({ ...body, tenant_id: body.tenant_id ?? this.tenantId })
372
+ });
373
+ if (res.meta.status === 200 || res.meta.status === 201) this.persistTokensFrom(res.data);
374
+ return res;
375
+ }
376
+ // Auth: Verify MFA challenge -> tokens
377
+ async mfaVerify(body) {
378
+ const res = await this.request("/v1/auth/mfa/verify", {
379
+ method: "POST",
380
+ body: JSON.stringify(body)
381
+ });
382
+ if (res.meta.status === 200) this.persistTokensFrom(res.data);
383
+ return res;
384
+ }
385
+ // Auth: Refresh tokens
386
+ async refresh(body) {
387
+ let refreshToken = body?.refresh_token ?? null;
388
+ if (!refreshToken) refreshToken = await Promise.resolve(this.storage.getRefreshToken()) ?? null;
389
+ const res = await this.request("/v1/auth/refresh", {
390
+ method: "POST",
391
+ body: JSON.stringify({ refresh_token: refreshToken })
392
+ });
393
+ if (res.meta.status === 200) this.persistTokensFrom(res.data);
394
+ return res;
395
+ }
396
+ // Auth: Logout (revoke refresh token) -> 204
397
+ async logout(body) {
398
+ const b = body ?? {};
399
+ const res = await this.request("/v1/auth/logout", {
400
+ method: "POST",
401
+ body: JSON.stringify(b)
402
+ });
403
+ if (res.meta.status === 204) {
404
+ void this.storage.setRefreshToken(null);
405
+ }
406
+ return res;
407
+ }
408
+ // Auth: Current user profile
409
+ async me() {
410
+ return this.request("/v1/auth/me", { method: "GET" });
411
+ }
412
+ // Auth: Email discovery (progressive login)
413
+ async emailDiscover(body) {
414
+ const headers = {};
415
+ const tid = body.tenant_id ?? this.tenantId;
416
+ if (tid) headers["X-Tenant-ID"] = String(tid);
417
+ return this.request(`/v1/auth/email/discover`, {
418
+ method: "POST",
419
+ headers,
420
+ body: JSON.stringify({ email: body.email })
421
+ });
422
+ }
423
+ // Auth: Get login options - returns available auth methods for a tenant/email
424
+ async getLoginOptions(params) {
425
+ const tid = params?.tenant_id ?? this.tenantId;
426
+ const qs = this.buildQuery({ email: params?.email, tenant_id: tid });
427
+ return this.request(`/v1/auth/login-options${qs}`, { method: "GET" });
428
+ }
429
+ // --- MFA self-service ---
430
+ async mfaStartTotp() {
431
+ return this.request("/v1/auth/mfa/totp/start", { method: "POST" });
432
+ }
433
+ async mfaActivateTotp(body) {
434
+ return this.request("/v1/auth/mfa/totp/activate", { method: "POST", body: JSON.stringify(body) });
435
+ }
436
+ async mfaDisableTotp() {
437
+ return this.request("/v1/auth/mfa/totp/disable", { method: "POST" });
438
+ }
439
+ async mfaGenerateBackupCodes(body = {}) {
440
+ return this.request("/v1/auth/mfa/backup/generate", { method: "POST", body: JSON.stringify({ count: body.count ?? 5 }) });
441
+ }
442
+ async mfaCountBackupCodes() {
443
+ return this.request("/v1/auth/mfa/backup/count", { method: "GET" });
444
+ }
445
+ // Tenants: Discover tenants for a given email (used by login tenant selection)
446
+ async discoverTenants(params) {
447
+ const qs = this.buildQuery({ email: params.email });
448
+ return this.request(`/v1/auth/tenants${qs}`, { method: "GET" });
449
+ }
450
+ // Tenants: Create
451
+ async createTenant(body) {
452
+ return this.request(`/tenants`, { method: "POST", body: JSON.stringify({ name: body.name }) });
453
+ }
454
+ // Tenants: Get by ID
455
+ async getTenant(id) {
456
+ return this.request(`/tenants/${encodeURIComponent(id)}`, { method: "GET" });
457
+ }
458
+ // Tenants: List (admin)
459
+ async listTenants(params = {}) {
460
+ const qs = this.buildQuery({
461
+ q: params.q,
462
+ page: params.page,
463
+ page_size: params.page_size,
464
+ active: typeof params.active === "boolean" ? params.active ? 1 : 0 : params.active
465
+ });
466
+ return this.request(`/tenants${qs}`, { method: "GET" });
467
+ }
468
+ // Auth: Introspect token (from header or body)
469
+ async introspect(body) {
470
+ return this.request("/v1/auth/introspect", {
471
+ method: "POST",
472
+ body: JSON.stringify(body ?? {})
473
+ });
474
+ }
475
+ // Auth: Magic link send
476
+ async magicSend(body) {
477
+ return this.request("/v1/auth/magic/send", {
478
+ method: "POST",
479
+ body: JSON.stringify(body)
480
+ });
481
+ }
482
+ // Auth: Magic verify (token in query preferred)
483
+ async magicVerify(params = {}, body) {
484
+ const qs = this.buildQuery(params);
485
+ const res = await this.request(`/v1/auth/magic/verify${qs}`, {
486
+ method: "GET",
487
+ // Some servers accept body on GET per spec; include if provided
488
+ ...body ? { body: JSON.stringify(body) } : {}
489
+ });
490
+ if (res.meta.status === 200) this.persistTokensFrom(res.data);
491
+ return res;
492
+ }
493
+ // Auth: Request password reset -> 202 (always, to prevent email enumeration)
494
+ async passwordResetRequest(body) {
495
+ return this.request("/v1/auth/password/reset/request", {
496
+ method: "POST",
497
+ body: JSON.stringify({ ...body, tenant_id: body.tenant_id ?? this.tenantId })
498
+ });
499
+ }
500
+ // Auth: Confirm password reset -> 200 on success
501
+ async passwordResetConfirm(body) {
502
+ return this.request("/v1/auth/password/reset/confirm", {
503
+ method: "POST",
504
+ body: JSON.stringify({ ...body, tenant_id: body.tenant_id ?? this.tenantId })
505
+ });
506
+ }
507
+ // Auth: Change password (requires auth) -> 200 on success
508
+ async changePassword(body) {
509
+ return this.request("/v1/auth/password/change", {
510
+ method: "POST",
511
+ body: JSON.stringify(body)
512
+ });
513
+ }
514
+ // Auth: Update profile (first/last name) -> 200 on success
515
+ async updateProfile(body) {
516
+ return this.request("/v1/auth/profile", {
517
+ method: "PATCH",
518
+ body: JSON.stringify(body)
519
+ });
520
+ }
521
+ // Admin: List users (requires admin role). tenant_id from client or param.
522
+ async listUsers(params = {}) {
523
+ const tenant = params.tenant_id ?? this.tenantId;
524
+ const qs = this.buildQuery({ tenant_id: tenant });
525
+ return this.request(`/v1/auth/admin/users${qs}`, { method: "GET" });
526
+ }
527
+ // Admin: Update user names
528
+ async updateUserNames(id, body) {
529
+ const payload = {};
530
+ if (typeof body?.first_name === "string") payload.first_name = body.first_name;
531
+ if (typeof body?.last_name === "string") payload.last_name = body.last_name;
532
+ return this.request(`/v1/auth/admin/users/${encodeURIComponent(id)}`, {
533
+ method: "PATCH",
534
+ body: JSON.stringify(payload)
535
+ });
536
+ }
537
+ // Admin: Block user
538
+ async blockUser(id) {
539
+ return this.request(`/v1/auth/admin/users/${encodeURIComponent(id)}/block`, { method: "POST" });
540
+ }
541
+ // Admin: Unblock user
542
+ async unblockUser(id) {
543
+ return this.request(`/v1/auth/admin/users/${encodeURIComponent(id)}/unblock`, { method: "POST" });
544
+ }
545
+ // Admin: Verify user email (set email_verified=true)
546
+ async verifyUserEmail(id) {
547
+ return this.request(`/v1/auth/admin/users/${encodeURIComponent(id)}/verify-email`, { method: "POST" });
548
+ }
549
+ // Admin: Unverify user email (set email_verified=false)
550
+ async unverifyUserEmail(id) {
551
+ return this.request(`/v1/auth/admin/users/${encodeURIComponent(id)}/unverify-email`, { method: "POST" });
552
+ }
553
+ // Sessions: List sessions. When includeAll=false, filter to active (non-revoked, not expired) client-side to match example app UX.
554
+ async listSessions(options = {}) {
555
+ const res = await this.request("/v1/auth/sessions", { method: "GET", cache: "no-store" });
556
+ if (res.meta.status >= 200 && res.meta.status < 300) {
557
+ const includeAll = !!options.includeAll;
558
+ const sessions = Array.isArray(res.data?.sessions) ? res.data.sessions : [];
559
+ if (!includeAll) {
560
+ const now = Date.now();
561
+ const active = sessions.filter((s) => {
562
+ const revoked = !!s.revoked;
563
+ const expMs = s.expires_at ? Date.parse(s.expires_at) : 0;
564
+ return !revoked && expMs > now;
565
+ });
566
+ return { data: { sessions: active }, meta: res.meta };
567
+ }
568
+ }
569
+ return res;
570
+ }
571
+ // Sessions: Revoke session
572
+ async revokeSession(id) {
573
+ return this.request(`/v1/auth/sessions/${encodeURIComponent(id)}/revoke`, { method: "POST" });
574
+ }
575
+ // Tenants: Get settings
576
+ async getTenantSettings(tenantId) {
577
+ const id = tenantId ?? this.tenantId;
578
+ if (!id) throw new Error("tenantId is required");
579
+ return this.request(`/v1/tenants/${encodeURIComponent(id)}/settings`, { method: "GET" });
580
+ }
581
+ // Tenants: Update settings
582
+ async updateTenantSettings(tenantId, settings) {
583
+ return this.request(`/v1/tenants/${encodeURIComponent(tenantId)}/settings`, {
584
+ method: "PUT",
585
+ body: JSON.stringify(settings ?? {})
586
+ });
587
+ }
588
+ /**
589
+ * Start SSO flow with a provider slug.
590
+ * Uses V2 tenant-scoped URLs: /auth/sso/t/{tenant_id}/{slug}/login
591
+ *
592
+ * @param providerSlug - The provider slug (e.g., 'okta', 'azure-ad', 'google-saml')
593
+ * @param params - Optional parameters for the SSO flow
594
+ * @returns The redirect URL to send the user to for authentication
595
+ *
596
+ * @example
597
+ * ```ts
598
+ * const result = await client.startSso('okta', { redirect_url: 'https://myapp.com/callback' });
599
+ * window.location.href = result.data.redirect_url;
600
+ * ```
601
+ */
602
+ async startSso(providerSlug, params = {}) {
603
+ const tenant = params.tenant_id ?? this.tenantId;
604
+ if (!tenant) throw new Error("tenant_id is required for SSO; set via params or client constructor");
605
+ const qs = this.buildQuery({
606
+ redirect_url: params.redirect_url,
607
+ login_hint: params.login_hint,
608
+ force_authn: params.force_authn
609
+ });
610
+ const res = await this.http.requestRaw(
611
+ `/auth/sso/t/${encodeURIComponent(tenant)}/${encodeURIComponent(providerSlug)}/login${qs}`,
612
+ { method: "GET", redirect: "manual" }
613
+ );
614
+ const loc = res.headers.get("location");
615
+ const requestId = res.headers.get("x-request-id") || void 0;
616
+ if (res.status >= 400) {
617
+ let errorMsg = `SSO start failed with status ${res.status}`;
618
+ try {
619
+ const body = await res.json();
620
+ if (body?.error) errorMsg += `: ${body.error}`;
621
+ } catch {
622
+ }
623
+ throw new Error(`${errorMsg}${requestId ? ` (request: ${requestId})` : ""}`);
624
+ }
625
+ if (!loc) {
626
+ throw new Error(
627
+ `missing redirect location from SSO start${requestId ? ` (request: ${requestId})` : ""}`
628
+ );
629
+ }
630
+ return {
631
+ data: { redirect_url: loc },
632
+ meta: { status: res.status, requestId, headers: toHeadersMap(res.headers) }
633
+ };
634
+ }
635
+ /**
636
+ * Handle SSO callback and exchange code for tokens.
637
+ * Uses V2 tenant-scoped URLs: /auth/sso/t/{tenant_id}/{slug}/callback
638
+ *
639
+ * @param providerSlug - The provider slug used in startSso
640
+ * @param params - Callback parameters (code, state from IdP redirect)
641
+ * @returns Access and refresh tokens
642
+ */
643
+ async handleSsoCallback(providerSlug, params) {
644
+ const tenant = params.tenant_id ?? this.tenantId;
645
+ if (!tenant) throw new Error("tenant_id is required for SSO callback");
646
+ const qs = this.buildQuery({ code: params.code, state: params.state });
647
+ const res = await this.request(
648
+ `/auth/sso/t/${encodeURIComponent(tenant)}/${encodeURIComponent(providerSlug)}/callback${qs}`,
649
+ { method: "GET" }
650
+ );
651
+ if (res.meta.status === 200) this.persistTokensFrom(res.data);
652
+ return res;
653
+ }
654
+ /**
655
+ * Parse SSO callback tokens from URL query parameters or fragment.
656
+ * Use this when Guard redirects to your app's callback URL with tokens.
657
+ *
658
+ * **Note:** This method has a side effect: when tokens are successfully parsed,
659
+ * they are automatically persisted to the configured TokenStorage.
660
+ *
661
+ * @param url - The full callback URL or just the query/fragment string (e.g., window.location.search or window.location.hash)
662
+ * @returns Tokens if access_token is present in URL, null otherwise
663
+ *
664
+ * @remarks
665
+ * - access_token is required for a successful return
666
+ * - refresh_token is optional (some flows don't provide it)
667
+ * - When refresh_token is missing, token refresh via `refresh()` will not be available
668
+ * - Tokens are persisted to storage on successful parse (side effect)
669
+ */
670
+ parseSsoCallbackTokens(url) {
671
+ try {
672
+ let searchParams;
673
+ if (url.startsWith("#")) {
674
+ searchParams = new URLSearchParams(url.substring(1));
675
+ } else if (url.startsWith("?") || url.startsWith("/") || url.startsWith("http")) {
676
+ const needsBase = url.startsWith("?") || url.startsWith("/");
677
+ const parsed = new URL(needsBase ? `http://x${url}` : url);
678
+ searchParams = parsed.hash ? new URLSearchParams(parsed.hash.substring(1)) : parsed.searchParams;
679
+ } else {
680
+ searchParams = new URLSearchParams(url);
681
+ }
682
+ const accessToken = searchParams.get("access_token");
683
+ const refreshToken = searchParams.get("refresh_token");
684
+ if (!accessToken) {
685
+ return null;
686
+ }
687
+ const tokens = {
688
+ access_token: accessToken,
689
+ refresh_token: refreshToken ?? void 0
690
+ };
691
+ this.persistTokensFrom(tokens);
692
+ return tokens;
693
+ } catch {
694
+ return null;
695
+ }
696
+ }
697
+ // SSO: WorkOS Organization Portal link (admin-only on server)
698
+ async getSsoOrganizationPortalLink(provider, params) {
699
+ const tenant = params.tenant_id ?? this.tenantId;
700
+ if (!tenant) throw new Error("tenant_id is required");
701
+ if (!params?.organization_id) throw new Error("organization_id is required");
702
+ const qs = this.buildQuery({
703
+ tenant_id: tenant,
704
+ organization_id: params.organization_id,
705
+ intent: params.intent
706
+ });
707
+ return this.request(`/v1/auth/sso/${provider}/portal-link${qs}`, { method: "GET" });
708
+ }
709
+ // SSO: Portal token session exchange (public, portal-token gated)
710
+ async ssoPortalSession(token) {
711
+ if (!token || typeof token !== "string") {
712
+ throw new Error("token is required");
713
+ }
714
+ return this.request("/v1/sso/portal/session", {
715
+ method: "POST",
716
+ body: JSON.stringify({ token })
717
+ });
718
+ }
719
+ // SSO: Portal provider config (public, portal-token gated)
720
+ async ssoPortalProvider(token) {
721
+ if (!token || typeof token !== "string") {
722
+ throw new Error("token is required");
723
+ }
724
+ const headers = { "X-Portal-Token": token };
725
+ return this.request("/v1/sso/portal/provider", {
726
+ method: "GET",
727
+ headers
728
+ });
729
+ }
730
+ // High-level helper: load portal session and provider in one call
731
+ async loadSsoPortalContext(token) {
732
+ const sessionRes = await this.ssoPortalSession(token);
733
+ if (sessionRes.meta.status !== 200 || !sessionRes.data) {
734
+ const serverError = this.extractErrorDetails(sessionRes.data);
735
+ const requestId = sessionRes.meta.requestId;
736
+ throw new Error(
737
+ `portal session failed with status ${sessionRes.meta.status}` + (serverError ? `: ${serverError}` : "") + (requestId ? ` (request: ${requestId})` : "")
738
+ );
739
+ }
740
+ const providerRes = await this.ssoPortalProvider(token);
741
+ if (providerRes.meta.status !== 200 || !providerRes.data) {
742
+ const serverError = this.extractErrorDetails(providerRes.data);
743
+ const requestId = providerRes.meta.requestId;
744
+ throw new Error(
745
+ `portal provider failed with status ${providerRes.meta.status}` + (serverError ? `: ${serverError}` : "") + (requestId ? ` (request: ${requestId})` : "")
746
+ );
747
+ }
748
+ return { session: sessionRes.data, provider: providerRes.data };
749
+ }
750
+ // Helper to extract error details from server response
751
+ extractErrorDetails(data) {
752
+ if (!data || typeof data !== "object") return null;
753
+ const obj = data;
754
+ if (typeof obj.error === "string") return obj.error;
755
+ if (typeof obj.message === "string") return obj.message;
756
+ if (typeof obj.description === "string") return obj.description;
757
+ if (typeof obj.detail === "string") return obj.detail;
758
+ return null;
759
+ }
760
+ // ==============================
761
+ // SSO Provider Management (Admin-only endpoints)
762
+ // ==============================
763
+ // List SSO providers for a tenant
764
+ async ssoListProviders(params = {}) {
765
+ const tenant = params.tenant_id ?? this.tenantId;
766
+ const qs = this.buildQuery({ tenant_id: tenant });
767
+ return this.request(`/v1/sso/providers${qs}`, { method: "GET" });
768
+ }
769
+ // Create a new SSO provider
770
+ async ssoCreateProvider(body) {
771
+ return this.request("/v1/sso/providers", {
772
+ method: "POST",
773
+ body: JSON.stringify(body)
774
+ });
775
+ }
776
+ // Get a specific SSO provider by ID
777
+ async ssoGetProvider(id) {
778
+ return this.request(`/v1/sso/providers/${encodeURIComponent(id)}`, {
779
+ method: "GET"
780
+ });
781
+ }
782
+ // Update an existing SSO provider
783
+ async ssoUpdateProvider(id, body) {
784
+ return this.request(`/v1/sso/providers/${encodeURIComponent(id)}`, {
785
+ method: "PUT",
786
+ body: JSON.stringify(body)
787
+ });
788
+ }
789
+ // Delete an SSO provider
790
+ async ssoDeleteProvider(id) {
791
+ return this.request(`/v1/sso/providers/${encodeURIComponent(id)}`, {
792
+ method: "DELETE"
793
+ });
794
+ }
795
+ // Test SSO provider configuration
796
+ async ssoTestProvider(id) {
797
+ return this.request(`/v1/sso/providers/${encodeURIComponent(id)}/test`, {
798
+ method: "POST"
799
+ });
800
+ }
801
+ /**
802
+ * Get computed Service Provider (SP) URLs for SAML configuration.
803
+ * These URLs are needed by admins to configure their Identity Provider (IdP).
804
+ * URLs use V2 tenant-scoped format: /auth/sso/t/{tenant_id}/{slug}/*
805
+ *
806
+ * @param slug - The provider slug (e.g. 'okta', 'azure-ad')
807
+ * @param tenantId - Optional tenant ID (uses client's default if not provided)
808
+ * @returns SP info including Entity ID, ACS URL, SLO URL, Metadata URL, Login URL, and Tenant ID
809
+ *
810
+ * @example
811
+ * ```ts
812
+ * const spInfo = await client.ssoGetSPInfo('okta');
813
+ * console.log(spInfo.data.entity_id); // https://api.example.com/auth/sso/t/{tenant_id}/okta/metadata
814
+ * console.log(spInfo.data.acs_url); // https://api.example.com/auth/sso/t/{tenant_id}/okta/callback
815
+ * console.log(spInfo.data.tenant_id); // The tenant UUID used in the URLs
816
+ * ```
817
+ */
818
+ async ssoGetSPInfo(slug, tenantId) {
819
+ if (!slug) throw new Error("slug is required");
820
+ const tenant = tenantId ?? this.tenantId;
821
+ const params = { slug };
822
+ if (tenant) params.tenant_id = tenant;
823
+ const qs = this.buildQuery(params);
824
+ return this.request(`/v1/sso/sp-info${qs}`, { method: "GET" });
825
+ }
826
+ // ==============================
827
+ // RBAC v2 (Admin-only endpoints)
828
+ // ==============================
829
+ // RBAC: List all permissions (admin-only)
830
+ async rbacListPermissions() {
831
+ return this.request("/v1/auth/admin/rbac/permissions", { method: "GET" });
832
+ }
833
+ // RBAC: List roles for a tenant
834
+ async rbacListRoles(params = {}) {
835
+ const tenant = params.tenant_id ?? this.tenantId;
836
+ if (!tenant) throw new Error("tenant_id is required");
837
+ const qs = this.buildQuery({ tenant_id: tenant });
838
+ return this.request(`/v1/auth/admin/rbac/roles${qs}`, { method: "GET" });
839
+ }
840
+ // RBAC: Create role
841
+ async rbacCreateRole(body) {
842
+ const tenant = body.tenant_id ?? this.tenantId;
843
+ if (!tenant) throw new Error("tenant_id is required");
844
+ const payload = { tenant_id: tenant, name: body.name, description: body.description };
845
+ return this.request("/v1/auth/admin/rbac/roles", { method: "POST", body: JSON.stringify(payload) });
846
+ }
847
+ // RBAC: Update role
848
+ async rbacUpdateRole(id, body) {
849
+ const tenant = body.tenant_id ?? this.tenantId;
850
+ if (!tenant) throw new Error("tenant_id is required");
851
+ const payload = { tenant_id: tenant, name: body.name, description: body.description };
852
+ return this.request(`/v1/auth/admin/rbac/roles/${encodeURIComponent(id)}`, { method: "PATCH", body: JSON.stringify(payload) });
853
+ }
854
+ // RBAC: Delete role
855
+ async rbacDeleteRole(id, params = {}) {
856
+ const tenant = params.tenant_id ?? this.tenantId;
857
+ if (!tenant) throw new Error("tenant_id is required");
858
+ const qs = this.buildQuery({ tenant_id: tenant });
859
+ return this.request(`/v1/auth/admin/rbac/roles/${encodeURIComponent(id)}${qs}`, { method: "DELETE" });
860
+ }
861
+ // RBAC: List user roles
862
+ async rbacListUserRoles(userId, params = {}) {
863
+ const tenant = params.tenant_id ?? this.tenantId;
864
+ if (!tenant) throw new Error("tenant_id is required");
865
+ const qs = this.buildQuery({ tenant_id: tenant });
866
+ return this.request(`/v1/auth/admin/rbac/users/${encodeURIComponent(userId)}/roles${qs}`, { method: "GET" });
867
+ }
868
+ // RBAC: Add user role
869
+ async rbacAddUserRole(userId, body) {
870
+ const tenant = body.tenant_id ?? this.tenantId;
871
+ if (!tenant) throw new Error("tenant_id is required");
872
+ const payload = { tenant_id: tenant, role_id: body.role_id };
873
+ return this.request(`/v1/auth/admin/rbac/users/${encodeURIComponent(userId)}/roles`, { method: "POST", body: JSON.stringify(payload) });
874
+ }
875
+ // RBAC: Remove user role
876
+ async rbacRemoveUserRole(userId, body) {
877
+ const tenant = body.tenant_id ?? this.tenantId;
878
+ if (!tenant) throw new Error("tenant_id is required");
879
+ const payload = { tenant_id: tenant, role_id: body.role_id };
880
+ return this.request(`/v1/auth/admin/rbac/users/${encodeURIComponent(userId)}/roles`, { method: "DELETE", body: JSON.stringify(payload) });
881
+ }
882
+ // RBAC: Upsert role permission
883
+ async rbacUpsertRolePermission(roleId, body) {
884
+ return this.request(`/v1/auth/admin/rbac/roles/${encodeURIComponent(roleId)}/permissions`, { method: "POST", body: JSON.stringify(body) });
885
+ }
886
+ // RBAC: Delete role permission
887
+ async rbacDeleteRolePermission(roleId, body) {
888
+ return this.request(`/v1/auth/admin/rbac/roles/${encodeURIComponent(roleId)}/permissions`, { method: "DELETE", body: JSON.stringify(body) });
889
+ }
890
+ // RBAC: Resolve user permissions
891
+ async rbacResolveUserPermissions(userId, params) {
892
+ const tenant = params?.tenant_id ?? this.tenantId;
893
+ if (!tenant) throw new Error("tenant_id is required");
894
+ const qs = this.buildQuery({ tenant_id: tenant });
895
+ return this.request(`/v1/auth/admin/rbac/users/${encodeURIComponent(userId)}/permissions/resolve${qs}`, { method: "GET" });
896
+ }
897
+ // ==============================
898
+ // FGA (Admin-only endpoints)
899
+ // ==============================
900
+ // Groups: list
901
+ async fgaListGroups(params) {
902
+ const tenant = params?.tenant_id ?? this.tenantId;
903
+ if (!tenant) throw new Error("tenant_id is required");
904
+ const qs = this.buildQuery({ tenant_id: tenant });
905
+ return this.request(`/v1/auth/admin/fga/groups${qs}`, { method: "GET" });
906
+ }
907
+ // Groups: create
908
+ async fgaCreateGroup(body) {
909
+ const tenant = body?.tenant_id ?? this.tenantId;
910
+ if (!tenant) throw new Error("tenant_id is required");
911
+ const payload = { tenant_id: tenant, name: body.name, description: body?.description ?? null };
912
+ return this.request(`/v1/auth/admin/fga/groups`, { method: "POST", body: JSON.stringify(payload) });
913
+ }
914
+ // Groups: delete
915
+ async fgaDeleteGroup(id, params) {
916
+ const tenant = params?.tenant_id ?? this.tenantId;
917
+ if (!tenant) throw new Error("tenant_id is required");
918
+ const qs = this.buildQuery({ tenant_id: tenant });
919
+ return this.request(`/v1/auth/admin/fga/groups/${encodeURIComponent(id)}${qs}`, { method: "DELETE" });
920
+ }
921
+ // Group membership: add
922
+ async fgaAddGroupMember(groupId, body) {
923
+ const payload = { user_id: body.user_id };
924
+ return this.request(`/v1/auth/admin/fga/groups/${encodeURIComponent(groupId)}/members`, { method: "POST", body: JSON.stringify(payload) });
925
+ }
926
+ // Group membership: remove
927
+ async fgaRemoveGroupMember(groupId, body) {
928
+ const payload = { user_id: body.user_id };
929
+ return this.request(`/v1/auth/admin/fga/groups/${encodeURIComponent(groupId)}/members`, { method: "DELETE", body: JSON.stringify(payload) });
930
+ }
931
+ // ACL tuples: create
932
+ async fgaCreateAclTuple(body) {
933
+ const tenant = body?.tenant_id ?? this.tenantId;
934
+ if (!tenant) throw new Error("tenant_id is required");
935
+ const payload = { ...body, tenant_id: tenant };
936
+ return this.request(`/v1/auth/admin/fga/acl/tuples`, { method: "POST", body: JSON.stringify(payload) });
937
+ }
938
+ // ACL tuples: delete
939
+ async fgaDeleteAclTuple(body) {
940
+ const tenant = body?.tenant_id ?? this.tenantId;
941
+ if (!tenant) throw new Error("tenant_id is required");
942
+ const payload = { ...body, tenant_id: tenant };
943
+ return this.request(`/v1/auth/admin/fga/acl/tuples`, { method: "DELETE", body: JSON.stringify(payload) });
944
+ }
945
+ // ==============================
946
+ // OAuth2 Discovery (RFC 8414)
947
+ // ==============================
948
+ /**
949
+ * Fetch OAuth 2.0 Authorization Server Metadata (RFC 8414)
950
+ * Returns server capabilities including supported auth modes, endpoints, and grant types.
951
+ * This endpoint is public and does not require authentication.
952
+ */
953
+ async getOAuth2Metadata() {
954
+ return this.request("/.well-known/oauth-authorization-server", { method: "GET" });
955
+ }
956
+ /**
957
+ * Static helper to discover OAuth2 metadata from any Guard API base URL.
958
+ * Useful for auto-configuration before creating a GuardClient instance.
959
+ *
960
+ * @example
961
+ * ```ts
962
+ * const metadata = await GuardClient.discover('https://api.example.com');
963
+ * console.log(metadata.guard_auth_modes_supported); // ['bearer', 'cookie']
964
+ * console.log(metadata.guard_auth_mode_default); // 'bearer'
965
+ *
966
+ * // Create client with discovered default auth mode
967
+ * const client = new GuardClient({
968
+ * baseUrl: 'https://api.example.com',
969
+ * authMode: metadata.guard_auth_mode_default as 'bearer' | 'cookie'
970
+ * });
971
+ * ```
972
+ */
973
+ static async discover(baseUrl, fetchImpl) {
974
+ const fetch = fetchImpl ?? (typeof window !== "undefined" ? window.fetch.bind(window) : globalThis.fetch);
975
+ const url = `${baseUrl}/.well-known/oauth-authorization-server`;
976
+ const response = await fetch(url, { method: "GET" });
977
+ if (!response.ok) {
978
+ throw new Error(`Discovery failed: ${response.status} ${response.statusText}`);
979
+ }
980
+ return response.json();
981
+ }
982
+ };
983
+ function generateTOTPCode(base32Secret) {
984
+ if (!base32Secret || typeof base32Secret !== "string") {
985
+ throw new Error("TOTP secret must be a non-empty base32 string");
986
+ }
987
+ return otplib.authenticator.generate(base32Secret);
988
+ }
989
+
990
+ exports.ApiError = ApiError;
991
+ exports.GuardClient = GuardClient;
992
+ exports.HttpClient = HttpClient;
993
+ exports.InMemoryStorage = InMemoryStorage;
994
+ exports.RateLimitError = RateLimitError;
995
+ exports.WebLocalStorage = WebLocalStorage;
996
+ exports.applyRequestInterceptors = applyRequestInterceptors;
997
+ exports.applyResponseInterceptors = applyResponseInterceptors;
998
+ exports.buildRateLimitError = buildRateLimitError;
999
+ exports.generateTOTPCode = generateTOTPCode;
1000
+ exports.isApiError = isApiError;
1001
+ exports.isMfaChallengeResp = isMfaChallengeResp;
1002
+ exports.isRateLimitError = isRateLimitError;
1003
+ exports.isTenantSelectionRequired = isTenantSelectionRequired;
1004
+ exports.isTokensResp = isTokensResp;
1005
+ exports.noopStorage = noopStorage;
1006
+ exports.parseRetryAfter = parseRetryAfter;
1007
+ exports.reactNativeStorageAdapter = reactNativeStorageAdapter;
1008
+ exports.toHeadersMap = toHeadersMap;
1009
+ //# sourceMappingURL=index.cjs.map
1010
+ //# sourceMappingURL=index.cjs.map