@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/README.md +126 -0
- package/dist/index.cjs +1010 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +1157 -0
- package/dist/index.d.ts +1157 -0
- package/dist/index.js +990 -0
- package/dist/index.js.map +1 -0
- package/package.json +78 -0
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
|