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