@classic-homes/auth 1.42.0 → 1.45.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +48 -0
- package/dist/auth.svelte-AN7ZNZ2W.js +3 -0
- package/dist/{chunk-LPQKZRPC.js → chunk-42EO3FSU.js} +9 -5
- package/dist/{chunk-K6TV3ZVS.js → chunk-5X6E5MZG.js} +107 -6
- package/dist/{chunk-7AQYLLTA.js → chunk-ADCPJOPQ.js} +113 -39
- package/dist/{chunk-7JLCFRUU.js → chunk-J2WAFXA6.js} +40 -23
- package/dist/{chunk-MBVOLE5U.js → chunk-W2ASN4NE.js} +20 -5
- package/dist/core/index.d.ts +138 -5
- package/dist/core/index.js +3 -3
- package/dist/index.d.ts +2 -2
- package/dist/index.js +5 -5
- package/dist/svelte/index.d.ts +15 -2
- package/dist/svelte/index.js +5 -5
- package/dist/testing/index.js +1 -1
- package/dist/{user-utils-Bo3lMsuT.d.ts → user-utils-DYyljbJR.d.ts} +33 -1
- package/package.json +1 -1
- package/dist/auth.svelte-XY4FQFZQ.js +0 -3
package/README.md
CHANGED
|
@@ -337,6 +337,44 @@ token.
|
|
|
337
337
|
Call `initCrossTabSync()` once at app start so a refresh or sign-out in another
|
|
338
338
|
tab rehydrates this tab's Svelte store.
|
|
339
339
|
|
|
340
|
+
## API errors and access debugging
|
|
341
|
+
|
|
342
|
+
A non-2xx response from `apiRequest` / `api.*` throws an `ApiError`. Its
|
|
343
|
+
`message` is the server's message (as before), plus `status`, the CHAPI `code`,
|
|
344
|
+
`details`, `method` and `endpoint`:
|
|
345
|
+
|
|
346
|
+
```typescript
|
|
347
|
+
import { isApiError } from '@classic-homes/auth/core';
|
|
348
|
+
|
|
349
|
+
try {
|
|
350
|
+
await api.get('/homes', true);
|
|
351
|
+
} catch (e) {
|
|
352
|
+
if (isApiError(e) && e.status === 403) {
|
|
353
|
+
console.log(e.details?.required); // e.g. ['homes:read']
|
|
354
|
+
}
|
|
355
|
+
}
|
|
356
|
+
```
|
|
357
|
+
|
|
358
|
+
### Access probe
|
|
359
|
+
|
|
360
|
+
Hooks for troubleshooting tools such as the Access Inspector. They are inert
|
|
361
|
+
until installed and do nothing on the server.
|
|
362
|
+
|
|
363
|
+
- `setAccessProbe(fn)` receives every access decision: core `hasRole*`, store
|
|
364
|
+
`has*`, `checkAuth` (`source: 'route'`), `filterByAccess`/`canAccess`
|
|
365
|
+
(`'nav'`) and API 403s (`'http'`, path only, never the query string).
|
|
366
|
+
Each event carries the requirement, `allowed` and what was `missing`.
|
|
367
|
+
- `setAccessSubject(userId, { roles, permissions })` previews the UI as another
|
|
368
|
+
role set ("view as") for that user only. Checks for other users are
|
|
369
|
+
unaffected, `authStore.user` is never changed, and the preview ends on any
|
|
370
|
+
login, logout or session expiry. Sign-in role gating (`allowedRoles`) always
|
|
371
|
+
uses the real roles.
|
|
372
|
+
- `evaluateAccess(subject, requirement)` is the shared any/all decision
|
|
373
|
+
`checkAuth` uses; `checkAuth` results now include `missing` on denial.
|
|
374
|
+
|
|
375
|
+
These change UI decisions only. The API authorizes every request with the real
|
|
376
|
+
token, so a preview can never grant access.
|
|
377
|
+
|
|
340
378
|
## API Reference
|
|
341
379
|
|
|
342
380
|
### Core Exports
|
|
@@ -376,6 +414,16 @@ import {
|
|
|
376
414
|
hasAnyRole,
|
|
377
415
|
hasAllRoles,
|
|
378
416
|
|
|
417
|
+
// Errors
|
|
418
|
+
ApiError,
|
|
419
|
+
isApiError,
|
|
420
|
+
|
|
421
|
+
// Access probe (see "API errors and access debugging")
|
|
422
|
+
setAccessProbe,
|
|
423
|
+
setAccessSubject,
|
|
424
|
+
clearAccessSubject,
|
|
425
|
+
evaluateAccess,
|
|
426
|
+
|
|
379
427
|
// Types
|
|
380
428
|
type User,
|
|
381
429
|
type UserRolesResponse,
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { authApi, InvalidAuthResponseError, RoleDeniedError } from './chunk-
|
|
2
|
-
import {
|
|
1
|
+
import { authApi, InvalidAuthResponseError, RoleDeniedError } from './chunk-ADCPJOPQ.js';
|
|
2
|
+
import { getUserRoles } from './chunk-5X6E5MZG.js';
|
|
3
3
|
|
|
4
4
|
// src/core/guards.ts
|
|
5
5
|
function isMfaChallengeResponse(response) {
|
|
@@ -19,7 +19,7 @@ function getAvailableMethods(response) {
|
|
|
19
19
|
async function setStoreAuth(response) {
|
|
20
20
|
let store;
|
|
21
21
|
try {
|
|
22
|
-
store = (await import('./auth.svelte-
|
|
22
|
+
store = (await import('./auth.svelte-AN7ZNZ2W.js')).authStore;
|
|
23
23
|
} catch {
|
|
24
24
|
return;
|
|
25
25
|
}
|
|
@@ -44,7 +44,7 @@ var AuthService = class {
|
|
|
44
44
|
throw new InvalidAuthResponseError();
|
|
45
45
|
}
|
|
46
46
|
if (options?.allowedRoles?.length) {
|
|
47
|
-
if (!
|
|
47
|
+
if (!holdsAnyRole(response.user, options.allowedRoles)) {
|
|
48
48
|
throw new RoleDeniedError(
|
|
49
49
|
response.user,
|
|
50
50
|
options.allowedRoles,
|
|
@@ -224,7 +224,7 @@ var AuthService = class {
|
|
|
224
224
|
throw new InvalidAuthResponseError();
|
|
225
225
|
}
|
|
226
226
|
if (options?.allowedRoles?.length) {
|
|
227
|
-
if (!
|
|
227
|
+
if (!holdsAnyRole(response.user, options.allowedRoles)) {
|
|
228
228
|
throw new RoleDeniedError(
|
|
229
229
|
response.user,
|
|
230
230
|
options.allowedRoles,
|
|
@@ -332,6 +332,10 @@ var AuthService = class {
|
|
|
332
332
|
}
|
|
333
333
|
};
|
|
334
334
|
var authService = new AuthService();
|
|
335
|
+
function holdsAnyRole(user, allowed) {
|
|
336
|
+
const held = getUserRoles(user);
|
|
337
|
+
return allowed.some((role) => held.includes(role));
|
|
338
|
+
}
|
|
335
339
|
|
|
336
340
|
// src/core/user-utils.ts
|
|
337
341
|
function getDisplayName(user) {
|
|
@@ -193,6 +193,101 @@ function extractClaims(token, claims) {
|
|
|
193
193
|
return result;
|
|
194
194
|
}
|
|
195
195
|
|
|
196
|
+
// src/core/access-probe.ts
|
|
197
|
+
var isBrowser = typeof window !== "undefined";
|
|
198
|
+
var probe = null;
|
|
199
|
+
var override = null;
|
|
200
|
+
var overrideListeners = /* @__PURE__ */ new Set();
|
|
201
|
+
function setAccessProbe(next) {
|
|
202
|
+
const previous = probe;
|
|
203
|
+
if (isBrowser || next === null) probe = next;
|
|
204
|
+
return previous;
|
|
205
|
+
}
|
|
206
|
+
function isAccessProbeActive() {
|
|
207
|
+
return probe !== null;
|
|
208
|
+
}
|
|
209
|
+
function reportAccess(event) {
|
|
210
|
+
if (!probe) return;
|
|
211
|
+
try {
|
|
212
|
+
probe(event);
|
|
213
|
+
} catch {
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
function setAccessSubject(userId, subject) {
|
|
217
|
+
if (!isBrowser) return;
|
|
218
|
+
override = {
|
|
219
|
+
userId,
|
|
220
|
+
subject: { roles: [...subject.roles], permissions: [...subject.permissions] }
|
|
221
|
+
};
|
|
222
|
+
notifyOverride();
|
|
223
|
+
}
|
|
224
|
+
function clearAccessSubject() {
|
|
225
|
+
if (!override) return;
|
|
226
|
+
override = null;
|
|
227
|
+
notifyOverride();
|
|
228
|
+
}
|
|
229
|
+
function getAccessSubjectOverride() {
|
|
230
|
+
return override;
|
|
231
|
+
}
|
|
232
|
+
function onAccessSubjectChange(listener) {
|
|
233
|
+
overrideListeners.add(listener);
|
|
234
|
+
return () => overrideListeners.delete(listener);
|
|
235
|
+
}
|
|
236
|
+
function notifyOverride() {
|
|
237
|
+
for (const listener of overrideListeners) {
|
|
238
|
+
try {
|
|
239
|
+
listener();
|
|
240
|
+
} catch {
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
function effectiveSubject(user) {
|
|
245
|
+
if (override && user?.id !== void 0 && user.id === override.userId) {
|
|
246
|
+
return { ...override.subject, simulated: true };
|
|
247
|
+
}
|
|
248
|
+
return { roles: user?.roles ?? [], permissions: user?.permissions ?? [], simulated: false };
|
|
249
|
+
}
|
|
250
|
+
function evaluateList(required, held, requireAll) {
|
|
251
|
+
if (!required || required.length === 0) return { ok: true, missing: [] };
|
|
252
|
+
const absent = required.filter((item) => !held.includes(item));
|
|
253
|
+
const ok = requireAll ? absent.length === 0 : absent.length < required.length;
|
|
254
|
+
return { ok, missing: ok ? [] : requireAll ? absent : [...required] };
|
|
255
|
+
}
|
|
256
|
+
function evaluateAccess(subject, requirement) {
|
|
257
|
+
const roles = evaluateList(requirement.roles, subject.roles, requirement.requireAllRoles);
|
|
258
|
+
const permissions = evaluateList(
|
|
259
|
+
requirement.permissions,
|
|
260
|
+
subject.permissions,
|
|
261
|
+
requirement.requireAllPermissions
|
|
262
|
+
);
|
|
263
|
+
return {
|
|
264
|
+
allowed: roles.ok && permissions.ok,
|
|
265
|
+
missing: { roles: roles.missing, permissions: permissions.missing }
|
|
266
|
+
};
|
|
267
|
+
}
|
|
268
|
+
function reportDecision(source, user, requirement, allowed, label) {
|
|
269
|
+
if (!probe) return;
|
|
270
|
+
const subject = effectiveSubject(user);
|
|
271
|
+
reportAccess({
|
|
272
|
+
source,
|
|
273
|
+
requirement,
|
|
274
|
+
allowed,
|
|
275
|
+
missing: allowed ? { roles: [], permissions: [] } : evaluateAccess(subject, requirement).missing,
|
|
276
|
+
subjectId: user?.id ?? null,
|
|
277
|
+
simulated: subject.simulated,
|
|
278
|
+
label
|
|
279
|
+
});
|
|
280
|
+
}
|
|
281
|
+
function requirementFromDenial(code, details) {
|
|
282
|
+
if (!details || typeof details !== "object") return null;
|
|
283
|
+
const { required, mode } = details;
|
|
284
|
+
const list = typeof required === "string" ? [required] : Array.isArray(required) ? required.filter((item) => typeof item === "string") : [];
|
|
285
|
+
if (list.length === 0) return null;
|
|
286
|
+
const isPermission = code === "INSUFFICIENT_PERMISSIONS" || list.some((p) => p.includes(":"));
|
|
287
|
+
const requireAll = mode === "any" ? false : mode === "all" ? true : isPermission;
|
|
288
|
+
return isPermission ? { permissions: list, requireAllPermissions: requireAll } : { roles: list, requireAllRoles: requireAll };
|
|
289
|
+
}
|
|
290
|
+
|
|
196
291
|
// src/core/roles.ts
|
|
197
292
|
var ROLE_NAMES = [
|
|
198
293
|
"admin",
|
|
@@ -275,15 +370,21 @@ function getEffectiveLevel(roles) {
|
|
|
275
370
|
return max;
|
|
276
371
|
}
|
|
277
372
|
function hasRole(user, role) {
|
|
278
|
-
|
|
373
|
+
const allowed = effectiveSubject(user).roles.includes(role);
|
|
374
|
+
reportDecision("check", user, { roles: [role] }, allowed);
|
|
375
|
+
return allowed;
|
|
279
376
|
}
|
|
280
377
|
function hasAnyRole(user, roles) {
|
|
281
|
-
const held =
|
|
282
|
-
|
|
378
|
+
const held = effectiveSubject(user).roles;
|
|
379
|
+
const allowed = roles.some((role) => held.includes(role));
|
|
380
|
+
if (roles.length > 0) reportDecision("check", user, { roles }, allowed);
|
|
381
|
+
return allowed;
|
|
283
382
|
}
|
|
284
383
|
function hasAllRoles(user, roles) {
|
|
285
|
-
const held =
|
|
286
|
-
|
|
384
|
+
const held = effectiveSubject(user).roles;
|
|
385
|
+
const allowed = roles.every((role) => held.includes(role));
|
|
386
|
+
if (roles.length > 0) reportDecision("check", user, { roles, requireAllRoles: true }, allowed);
|
|
387
|
+
return allowed;
|
|
287
388
|
}
|
|
288
389
|
|
|
289
|
-
export { ROLE_HIERARCHY, ROLE_NAMES, computePrimaryRole, createMemoryStorage, createSessionStorage, decodeJWT, extractClaims, getConfig, getDefaultStorage, getEffectiveLevel, getFetch, getStorage, getTokenExpiration, getTokenRemainingTime, getUserRoles, hasAllRoles, hasAnyRole, hasRole, initAuth, isInitialized, isTokenExpired, isValidRedirectUrl, isValidSSOLogoutUrl, resetConfig };
|
|
390
|
+
export { ROLE_HIERARCHY, ROLE_NAMES, clearAccessSubject, computePrimaryRole, createMemoryStorage, createSessionStorage, decodeJWT, effectiveSubject, evaluateAccess, extractClaims, getAccessSubjectOverride, getConfig, getDefaultStorage, getEffectiveLevel, getFetch, getStorage, getTokenExpiration, getTokenRemainingTime, getUserRoles, hasAllRoles, hasAnyRole, hasRole, initAuth, isAccessProbeActive, isInitialized, isTokenExpired, isValidRedirectUrl, isValidSSOLogoutUrl, onAccessSubjectChange, reportAccess, reportDecision, requirementFromDenial, resetConfig, setAccessProbe, setAccessSubject };
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { isInitialized, getConfig, getUserRoles, getStorage, isTokenExpired, getFetch, isValidRedirectUrl, decodeJWT } from './chunk-
|
|
1
|
+
import { isInitialized, getConfig, getUserRoles, clearAccessSubject, getStorage, isTokenExpired, getFetch, isAccessProbeActive, isValidRedirectUrl, requirementFromDenial, reportAccess, decodeJWT } from './chunk-5X6E5MZG.js';
|
|
2
2
|
|
|
3
3
|
// src/core/sso-state.ts
|
|
4
4
|
var SSO_CLIENT_STATE_PARAM = "client_state";
|
|
@@ -119,11 +119,29 @@ var MFAChallengeEmailError = class _MFAChallengeEmailError extends Error {
|
|
|
119
119
|
}
|
|
120
120
|
}
|
|
121
121
|
};
|
|
122
|
+
var ApiError = class _ApiError extends Error {
|
|
123
|
+
constructor(message, init) {
|
|
124
|
+
super(message);
|
|
125
|
+
this.name = "ApiError";
|
|
126
|
+
this.status = init.status;
|
|
127
|
+
this.code = init.code;
|
|
128
|
+
this.details = init.details;
|
|
129
|
+
this.method = init.method;
|
|
130
|
+
this.endpoint = init.endpoint;
|
|
131
|
+
if (Error.captureStackTrace) {
|
|
132
|
+
Error.captureStackTrace(this, _ApiError);
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
};
|
|
136
|
+
function isApiError(error) {
|
|
137
|
+
return error instanceof ApiError || error?.name === "ApiError";
|
|
138
|
+
}
|
|
122
139
|
|
|
123
140
|
// src/core/session.ts
|
|
124
141
|
var generation = 0;
|
|
125
142
|
function bumpSessionGeneration() {
|
|
126
143
|
generation += 1;
|
|
144
|
+
clearAccessSubject();
|
|
127
145
|
}
|
|
128
146
|
function readIdentity() {
|
|
129
147
|
let parsed = {};
|
|
@@ -218,7 +236,7 @@ function writeStoredTokens(accessToken, refreshToken) {
|
|
|
218
236
|
}
|
|
219
237
|
async function syncSvelteStore(apply) {
|
|
220
238
|
try {
|
|
221
|
-
const { authStore } = await import('./auth.svelte-
|
|
239
|
+
const { authStore } = await import('./auth.svelte-AN7ZNZ2W.js');
|
|
222
240
|
apply(authStore);
|
|
223
241
|
} catch {
|
|
224
242
|
}
|
|
@@ -371,6 +389,31 @@ function isApiOrigin(url, baseUrl) {
|
|
|
371
389
|
return false;
|
|
372
390
|
}
|
|
373
391
|
}
|
|
392
|
+
function isRecord(value) {
|
|
393
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
394
|
+
}
|
|
395
|
+
function reportForbidden(method, endpoint, code, details) {
|
|
396
|
+
let path = endpoint.split(/[?#]/, 1)[0];
|
|
397
|
+
try {
|
|
398
|
+
if (/^https?:\/\//i.test(path)) path = new URL(path).pathname;
|
|
399
|
+
} catch {
|
|
400
|
+
}
|
|
401
|
+
const requirement = requirementFromDenial(code, details) ?? {};
|
|
402
|
+
reportAccess({
|
|
403
|
+
source: "http",
|
|
404
|
+
requirement,
|
|
405
|
+
allowed: false,
|
|
406
|
+
missing: {
|
|
407
|
+
roles: [...requirement.roles ?? []],
|
|
408
|
+
permissions: [...requirement.permissions ?? []]
|
|
409
|
+
},
|
|
410
|
+
// The server decided against the real token; whose id is the caller's business.
|
|
411
|
+
subjectId: null,
|
|
412
|
+
simulated: false,
|
|
413
|
+
label: `${method} ${path}`,
|
|
414
|
+
http: { method, endpoint: path, status: 403, code }
|
|
415
|
+
});
|
|
416
|
+
}
|
|
374
417
|
function apiRequest(endpoint, options = {}) {
|
|
375
418
|
return apiRequestInternal(endpoint, options, false);
|
|
376
419
|
}
|
|
@@ -439,7 +482,6 @@ async function apiRequestInternal(endpoint, options, isRetry, startedIn) {
|
|
|
439
482
|
body: body ? JSON.stringify(body) : void 0,
|
|
440
483
|
signal: controller.signal
|
|
441
484
|
});
|
|
442
|
-
clearTimeout(timeoutId);
|
|
443
485
|
if (response.status === 401 && authenticated && !noAuthRetry && typeof window !== "undefined") {
|
|
444
486
|
await assertSameSession();
|
|
445
487
|
if (isRetry) {
|
|
@@ -462,13 +504,17 @@ async function apiRequestInternal(endpoint, options, isRetry, startedIn) {
|
|
|
462
504
|
try {
|
|
463
505
|
data = await response.json();
|
|
464
506
|
} catch (parseError) {
|
|
507
|
+
if (controller.signal.aborted) throw parseError;
|
|
465
508
|
if (!response.ok) {
|
|
466
509
|
throw new Error(`Server error (${response.status}): Invalid JSON response`);
|
|
467
510
|
}
|
|
468
511
|
throw parseError;
|
|
469
512
|
}
|
|
470
513
|
} else {
|
|
471
|
-
const responseText = await response.text().catch(() =>
|
|
514
|
+
const responseText = await response.text().catch((readError) => {
|
|
515
|
+
if (controller.signal.aborted) throw readError;
|
|
516
|
+
return "";
|
|
517
|
+
});
|
|
472
518
|
if (responseText) {
|
|
473
519
|
try {
|
|
474
520
|
data = JSON.parse(responseText);
|
|
@@ -493,13 +539,24 @@ async function apiRequestInternal(endpoint, options, isRetry, startedIn) {
|
|
|
493
539
|
if (!response.ok) {
|
|
494
540
|
const errorBody = data ?? {};
|
|
495
541
|
const errorMessage = errorBody.error?.message || (typeof errorBody.message === "string" ? errorBody.message : void 0) || errorBody.meta?.message || (typeof errorBody.detail === "string" ? errorBody.detail : void 0) || `Request failed (${response.status})`;
|
|
496
|
-
const
|
|
542
|
+
const code = typeof errorBody.error?.code === "string" ? errorBody.error.code : void 0;
|
|
543
|
+
const details = isRecord(errorBody.error?.details) ? errorBody.error.details : void 0;
|
|
544
|
+
const method = (fetchOptions.method ?? "GET").toUpperCase();
|
|
545
|
+
const error = new ApiError(errorMessage, {
|
|
546
|
+
status: response.status,
|
|
547
|
+
code,
|
|
548
|
+
details,
|
|
549
|
+
method,
|
|
550
|
+
endpoint
|
|
551
|
+
});
|
|
552
|
+
if (response.status === 403 && isAccessProbeActive()) {
|
|
553
|
+
reportForbidden(method, endpoint, code, details);
|
|
554
|
+
}
|
|
497
555
|
config.onAuthError?.(error);
|
|
498
556
|
throw error;
|
|
499
557
|
}
|
|
500
558
|
return data;
|
|
501
559
|
} catch (error) {
|
|
502
|
-
clearTimeout(timeoutId);
|
|
503
560
|
if (error instanceof DOMException && error.name === "AbortError") {
|
|
504
561
|
if (signal?.aborted) throw error;
|
|
505
562
|
throw new Error(`Request timed out after ${timeoutMs}ms`);
|
|
@@ -509,6 +566,7 @@ async function apiRequestInternal(endpoint, options, isRetry, startedIn) {
|
|
|
509
566
|
}
|
|
510
567
|
throw new Error("API request failed");
|
|
511
568
|
} finally {
|
|
569
|
+
clearTimeout(timeoutId);
|
|
512
570
|
signal?.removeEventListener("abort", onCallerAbort);
|
|
513
571
|
}
|
|
514
572
|
}
|
|
@@ -553,16 +611,19 @@ var api = {
|
|
|
553
611
|
};
|
|
554
612
|
|
|
555
613
|
// src/core/api.ts
|
|
556
|
-
async function fetchWithTimeout(url, init) {
|
|
614
|
+
async function fetchWithTimeout(url, init, read) {
|
|
557
615
|
const config = getConfig();
|
|
558
616
|
const fetchFn = config.fetch ?? fetch;
|
|
559
617
|
const timeoutMs = config.requestTimeout ?? 3e4;
|
|
560
618
|
const controller = new AbortController();
|
|
561
619
|
const timeoutId = setTimeout(() => controller.abort(), timeoutMs);
|
|
562
620
|
try {
|
|
563
|
-
|
|
621
|
+
const response = await fetchFn(url, { ...init, signal: controller.signal });
|
|
622
|
+
const result = await read(response);
|
|
623
|
+
if (controller.signal.aborted) throw new Error("aborted");
|
|
624
|
+
return result;
|
|
564
625
|
} catch (error) {
|
|
565
|
-
if (
|
|
626
|
+
if (controller.signal.aborted) {
|
|
566
627
|
throw new Error(`Request timed out after ${timeoutMs}ms`);
|
|
567
628
|
}
|
|
568
629
|
throw error;
|
|
@@ -814,19 +875,24 @@ var authApi = {
|
|
|
814
875
|
type: data.method === "backup" || data.method === "email" ? data.method : "totp",
|
|
815
876
|
trustDevice: data.trustDevice
|
|
816
877
|
};
|
|
817
|
-
const
|
|
818
|
-
|
|
819
|
-
|
|
820
|
-
|
|
821
|
-
|
|
878
|
+
const result = await fetchWithTimeout(
|
|
879
|
+
`${config.baseUrl}/auth/mfa/challenge`,
|
|
880
|
+
{
|
|
881
|
+
method: "POST",
|
|
882
|
+
headers: {
|
|
883
|
+
"Content-Type": "application/json",
|
|
884
|
+
Authorization: `Bearer ${data.mfaToken}`
|
|
885
|
+
},
|
|
886
|
+
body: JSON.stringify(requestBody)
|
|
822
887
|
},
|
|
823
|
-
|
|
824
|
-
|
|
825
|
-
|
|
826
|
-
|
|
827
|
-
|
|
828
|
-
|
|
829
|
-
|
|
888
|
+
async (response) => {
|
|
889
|
+
if (!response.ok) {
|
|
890
|
+
const error = await response.json().catch(() => ({ message: "Verification failed" }));
|
|
891
|
+
throw new Error(error.error?.message || error.message || "Invalid verification code");
|
|
892
|
+
}
|
|
893
|
+
return response.json();
|
|
894
|
+
}
|
|
895
|
+
);
|
|
830
896
|
return extractData(result);
|
|
831
897
|
},
|
|
832
898
|
/**
|
|
@@ -837,11 +903,14 @@ var authApi = {
|
|
|
837
903
|
*/
|
|
838
904
|
async sendMFAChallengeEmail(mfaToken) {
|
|
839
905
|
const config = getConfig();
|
|
840
|
-
const response = await fetchWithTimeout(
|
|
841
|
-
|
|
842
|
-
|
|
843
|
-
|
|
844
|
-
|
|
906
|
+
const { response, body } = await fetchWithTimeout(
|
|
907
|
+
`${config.baseUrl}/auth/mfa/challenge/email/send`,
|
|
908
|
+
{
|
|
909
|
+
method: "POST",
|
|
910
|
+
headers: { Authorization: `Bearer ${mfaToken}` }
|
|
911
|
+
},
|
|
912
|
+
async (response2) => ({ response: response2, body: await response2.json().catch(() => ({})) })
|
|
913
|
+
);
|
|
845
914
|
if (!response.ok) {
|
|
846
915
|
const details = body?.error?.details;
|
|
847
916
|
const retryAfter = typeof details?.retryAfterSeconds === "number" ? details.retryAfterSeconds : typeof body?.retryAfterSeconds === "number" ? body.retryAfterSeconds : void 0;
|
|
@@ -969,19 +1038,24 @@ var authApi = {
|
|
|
969
1038
|
throw new Error("Not authenticated");
|
|
970
1039
|
}
|
|
971
1040
|
const config = getConfig();
|
|
972
|
-
const
|
|
973
|
-
|
|
974
|
-
|
|
975
|
-
|
|
976
|
-
|
|
1041
|
+
const result = await fetchWithTimeout(
|
|
1042
|
+
`${config.baseUrl}/auth/sso/link`,
|
|
1043
|
+
{
|
|
1044
|
+
method: "POST",
|
|
1045
|
+
headers: {
|
|
1046
|
+
"Content-Type": "application/json",
|
|
1047
|
+
Authorization: `Bearer ${accessToken}`
|
|
1048
|
+
},
|
|
1049
|
+
body: JSON.stringify({ provider })
|
|
977
1050
|
},
|
|
978
|
-
|
|
979
|
-
|
|
980
|
-
|
|
981
|
-
|
|
982
|
-
|
|
983
|
-
|
|
984
|
-
|
|
1051
|
+
async (response) => {
|
|
1052
|
+
if (!response.ok) {
|
|
1053
|
+
const error = await response.json().catch(() => ({ message: "Failed to link SSO account" }));
|
|
1054
|
+
throw new Error(error.error?.message || error.message || "Failed to link SSO account");
|
|
1055
|
+
}
|
|
1056
|
+
return response.json();
|
|
1057
|
+
}
|
|
1058
|
+
);
|
|
985
1059
|
const authorizationUrl = extractData(result).authorizationUrl;
|
|
986
1060
|
if (authorizationUrl) {
|
|
987
1061
|
if (!isValidRedirectUrl(authorizationUrl)) {
|
|
@@ -1081,4 +1155,4 @@ var authApi = {
|
|
|
1081
1155
|
}
|
|
1082
1156
|
};
|
|
1083
1157
|
|
|
1084
|
-
export { InvalidAuthResponseError, MFAChallengeEmailError, RoleDeniedError, SSO_CLIENT_STATE_PARAM, SSO_CLIENT_STATE_TTL_MS, SessionChangedError, api, apiRequest, authApi, bumpSessionGeneration, clearStoredAuth, consumeSsoClientState, createSsoClientState, extractData, getAccessToken, getRefreshToken, getSessionToken, initCrossTabSync, isRoleDeniedError, isSessionChangedError, refreshSession, updateStoredTokens, verifySsoClientState };
|
|
1158
|
+
export { ApiError, InvalidAuthResponseError, MFAChallengeEmailError, RoleDeniedError, SSO_CLIENT_STATE_PARAM, SSO_CLIENT_STATE_TTL_MS, SessionChangedError, api, apiRequest, authApi, bumpSessionGeneration, clearStoredAuth, consumeSsoClientState, createSsoClientState, extractData, getAccessToken, getRefreshToken, getSessionToken, initCrossTabSync, isApiError, isRoleDeniedError, isSessionChangedError, refreshSession, updateStoredTokens, verifySsoClientState };
|
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
import { authService } from './chunk-
|
|
2
|
-
import { authStore, currentUser, isAuthenticated, authActions } from './chunk-
|
|
3
|
-
import { isInitialized, initAuth
|
|
1
|
+
import { authService } from './chunk-42EO3FSU.js';
|
|
2
|
+
import { authStore, currentUser, isAuthenticated, authActions } from './chunk-W2ASN4NE.js';
|
|
3
|
+
import { evaluateAccess, effectiveSubject, isAccessProbeActive, reportDecision, isInitialized, initAuth } from './chunk-5X6E5MZG.js';
|
|
4
4
|
|
|
5
5
|
// src/svelte/guards/auth-guard.ts
|
|
6
6
|
function checkAuth(options = {}) {
|
|
@@ -12,28 +12,26 @@ function checkAuth(options = {}) {
|
|
|
12
12
|
redirectTo: "/login"
|
|
13
13
|
};
|
|
14
14
|
}
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
reason: "missing_role",
|
|
21
|
-
redirectTo: "/unauthorized"
|
|
22
|
-
};
|
|
23
|
-
}
|
|
15
|
+
const requirement = { roles, permissions, requireAllRoles, requireAllPermissions };
|
|
16
|
+
const user = authStore.user;
|
|
17
|
+
const { allowed, missing } = evaluateAccess(effectiveSubject(user), requirement);
|
|
18
|
+
if (isAccessProbeActive() && ((roles?.length ?? 0) > 0 || (permissions?.length ?? 0) > 0)) {
|
|
19
|
+
reportDecision("route", user, requirement, allowed, routeLabel());
|
|
24
20
|
}
|
|
25
|
-
if (
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
}
|
|
21
|
+
if (!allowed) {
|
|
22
|
+
return {
|
|
23
|
+
allowed: false,
|
|
24
|
+
// Roles are checked first: a role miss is reported even if permissions also fail.
|
|
25
|
+
reason: missing.roles.length > 0 ? "missing_role" : "missing_permission",
|
|
26
|
+
redirectTo: "/unauthorized",
|
|
27
|
+
missing
|
|
28
|
+
};
|
|
34
29
|
}
|
|
35
30
|
return { allowed: true };
|
|
36
31
|
}
|
|
32
|
+
function routeLabel() {
|
|
33
|
+
return typeof window !== "undefined" ? window.location?.pathname : void 0;
|
|
34
|
+
}
|
|
37
35
|
function createAuthGuard(options = {}) {
|
|
38
36
|
return (onDenied) => {
|
|
39
37
|
const result = checkAuth(options);
|
|
@@ -92,8 +90,9 @@ function filterByAccess(items, user, options = {}) {
|
|
|
92
90
|
childrenKey,
|
|
93
91
|
pruneEmptyParents = true
|
|
94
92
|
} = options;
|
|
95
|
-
const
|
|
96
|
-
const
|
|
93
|
+
const subject = effectiveSubject(user);
|
|
94
|
+
const userRoles = subject.roles;
|
|
95
|
+
const userPerms = subject.permissions;
|
|
97
96
|
function isVisible(item) {
|
|
98
97
|
const hasRoleRestriction = item.roles && item.roles.length > 0;
|
|
99
98
|
const hasPermRestriction = item.permissions && item.permissions.length > 0;
|
|
@@ -106,6 +105,24 @@ function filterByAccess(items, user, options = {}) {
|
|
|
106
105
|
if (!user) {
|
|
107
106
|
return false;
|
|
108
107
|
}
|
|
108
|
+
const allowed = isAllowed(item);
|
|
109
|
+
reportDecision(
|
|
110
|
+
"nav",
|
|
111
|
+
user,
|
|
112
|
+
{
|
|
113
|
+
roles: item.roles,
|
|
114
|
+
permissions: item.permissions,
|
|
115
|
+
requireAllRoles,
|
|
116
|
+
requireAllPermissions
|
|
117
|
+
},
|
|
118
|
+
allowed,
|
|
119
|
+
item.id ?? item.href
|
|
120
|
+
);
|
|
121
|
+
return allowed;
|
|
122
|
+
}
|
|
123
|
+
function isAllowed(item) {
|
|
124
|
+
const hasRoleRestriction = item.roles && item.roles.length > 0;
|
|
125
|
+
const hasPermRestriction = item.permissions && item.permissions.length > 0;
|
|
109
126
|
if (hasRoleRestriction) {
|
|
110
127
|
const hasRoles = requireAllRoles ? item.roles.every((r) => userRoles.includes(r)) : item.roles.some((r) => userRoles.includes(r));
|
|
111
128
|
if (!hasRoles) {
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { InvalidAuthResponseError, bumpSessionGeneration, authApi } from './chunk-
|
|
2
|
-
import { isInitialized, getConfig, isValidSSOLogoutUrl, hasRole, hasAnyRole, hasAllRoles, decodeJWT, getStorage, getDefaultStorage } from './chunk-
|
|
1
|
+
import { InvalidAuthResponseError, bumpSessionGeneration, authApi } from './chunk-ADCPJOPQ.js';
|
|
2
|
+
import { onAccessSubjectChange, isInitialized, getConfig, isValidSSOLogoutUrl, hasRole, hasAnyRole, hasAllRoles, effectiveSubject, reportDecision, decodeJWT, getStorage, getDefaultStorage } from './chunk-5X6E5MZG.js';
|
|
3
3
|
|
|
4
4
|
// src/svelte/stores/auth.svelte.ts
|
|
5
5
|
var STORAGE_VERSION = 2;
|
|
@@ -72,6 +72,7 @@ var AuthStore = class {
|
|
|
72
72
|
constructor() {
|
|
73
73
|
this.subscribers = /* @__PURE__ */ new Set();
|
|
74
74
|
this.state = loadAuthFromStorage();
|
|
75
|
+
onAccessSubjectChange(() => this.notify());
|
|
75
76
|
}
|
|
76
77
|
/**
|
|
77
78
|
* Subscribe to state changes (Svelte store contract).
|
|
@@ -249,7 +250,7 @@ var AuthStore = class {
|
|
|
249
250
|
* Check if user has a specific permission.
|
|
250
251
|
*/
|
|
251
252
|
hasPermission(permission) {
|
|
252
|
-
return this.
|
|
253
|
+
return this.checkPermissions([permission], false);
|
|
253
254
|
}
|
|
254
255
|
/**
|
|
255
256
|
* Check if the user holds a specific role.
|
|
@@ -275,13 +276,27 @@ var AuthStore = class {
|
|
|
275
276
|
* Check if user has any of the specified permissions.
|
|
276
277
|
*/
|
|
277
278
|
hasAnyPermission(permissions) {
|
|
278
|
-
return
|
|
279
|
+
return this.checkPermissions(permissions, false);
|
|
279
280
|
}
|
|
280
281
|
/**
|
|
281
282
|
* Check if user has all of the specified permissions.
|
|
282
283
|
*/
|
|
283
284
|
hasAllPermissions(permissions) {
|
|
284
|
-
return
|
|
285
|
+
return this.checkPermissions(permissions, true);
|
|
286
|
+
}
|
|
287
|
+
/**
|
|
288
|
+
* Permission checks share one path so they honor the access probe's
|
|
289
|
+
* "view as" override and report to it. A guest holds no permissions.
|
|
290
|
+
*/
|
|
291
|
+
checkPermissions(permissions, requireAll) {
|
|
292
|
+
const user = this.state.user;
|
|
293
|
+
const held = effectiveSubject(user).permissions;
|
|
294
|
+
const allowed = requireAll ? permissions.every((perm) => held.includes(perm)) : permissions.some((perm) => held.includes(perm));
|
|
295
|
+
if (permissions.length > 0) {
|
|
296
|
+
const requirement = { permissions, requireAllPermissions: requireAll };
|
|
297
|
+
reportDecision("check", user, requirement, allowed);
|
|
298
|
+
}
|
|
299
|
+
return allowed;
|
|
285
300
|
}
|
|
286
301
|
/**
|
|
287
302
|
* Re-hydrate state from storage (useful after config changes).
|
package/dist/core/index.d.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { e as LoginCredentials, f as LoginResponse, g as LogoutResponse, R as RegisterData, l as RegisterResponse, q as User, k as ProfileUpdateData, C as ChangePasswordData, o as Session, A as ApiKey, c as CreateApiKeyRequest, d as CreateApiKeyResponse, U as UpdateApiKeyRequest, j as MFAStatus, i as MFASetupResponse, M as MFAChallengeData, h as MFAChallengeEmailResponse, D as Device, r as UserPreferences, L as LinkedAccount, n as SecurityEvent, P as Pagination, s as UserRolesResponse } from '../types-DF5Ny2IA.js';
|
|
2
2
|
export { a as AuthConfig, b as AuthState, m as ResetPasswordData, S as SSOConfig, p as StorageAdapter, t as createMemoryStorage, u as createSessionStorage, v as getConfig, w as getDefaultStorage, x as getFetch, y as getStorage, z as initAuth, B as isInitialized, E as resetConfig } from '../types-DF5Ny2IA.js';
|
|
3
|
-
export { A as AuthService, I as InvalidAuthResponseError, L as LoginOptions, M as MFAChallengeEmailError,
|
|
3
|
+
export { A as ApiError, a as AuthService, I as InvalidAuthResponseError, L as LoginOptions, M as MFAChallengeEmailError, b as MFAVerifyOptions, R as RoleDeniedError, S as SessionChangedError, c as authService, f as formatUserRoles, g as getAvatarFallback, d as getDisplayName, e as getGreeting, h as getUserEmail, i as getUserInitials, j as isApiError, k as isRoleDeniedError, l as isSessionChangedError } from '../user-utils-DYyljbJR.js';
|
|
4
4
|
|
|
5
5
|
/**
|
|
6
6
|
* SSO client state
|
|
@@ -684,23 +684,156 @@ declare function computePrimaryRole(roles: readonly string[]): RoleName;
|
|
|
684
684
|
* ```
|
|
685
685
|
*/
|
|
686
686
|
declare function getEffectiveLevel(roles: readonly string[]): number;
|
|
687
|
+
/** A user as the role checks accept it; `id` lets "view as" target the session user. */
|
|
688
|
+
type RoleSubject = (Pick<User, 'roles'> & {
|
|
689
|
+
id?: string;
|
|
690
|
+
permissions?: string[];
|
|
691
|
+
}) | null | undefined;
|
|
687
692
|
/**
|
|
688
693
|
* Whether a user holds a specific role.
|
|
689
694
|
*
|
|
690
695
|
* Exact membership — holding `admin` does NOT imply `agent`. This matches the
|
|
691
696
|
* API's `requireRole`, so the UI can't offer something the server will then
|
|
692
697
|
* reject with a 403.
|
|
698
|
+
*
|
|
699
|
+
* Honors the access probe's "view as" override for the user it targets.
|
|
693
700
|
*/
|
|
694
|
-
declare function hasRole(user:
|
|
701
|
+
declare function hasRole(user: RoleSubject, role: string): boolean;
|
|
695
702
|
/**
|
|
696
703
|
* Whether a user holds at least one of the given roles.
|
|
697
704
|
* An empty `roles` list matches nothing.
|
|
698
705
|
*/
|
|
699
|
-
declare function hasAnyRole(user:
|
|
706
|
+
declare function hasAnyRole(user: RoleSubject, roles: readonly string[]): boolean;
|
|
700
707
|
/**
|
|
701
708
|
* Whether a user holds every one of the given roles.
|
|
702
709
|
* An empty `roles` list matches vacuously (nothing was required).
|
|
703
710
|
*/
|
|
704
|
-
declare function hasAllRoles(user:
|
|
711
|
+
declare function hasAllRoles(user: RoleSubject, roles: readonly string[]): boolean;
|
|
712
|
+
|
|
713
|
+
/**
|
|
714
|
+
* Access probe
|
|
715
|
+
*
|
|
716
|
+
* Hooks that let a troubleshooting tool (the Access Inspector) see every
|
|
717
|
+
* role/permission decision the UI makes, and preview the UI as another
|
|
718
|
+
* role set ("view as").
|
|
719
|
+
*
|
|
720
|
+
* Both hooks are inert by default: with no probe installed a check costs one
|
|
721
|
+
* null test, and with no subject override every check reads the real user.
|
|
722
|
+
* They are browser-only: on the server the module state would be shared by
|
|
723
|
+
* every request, so the setters are no-ops there.
|
|
724
|
+
*
|
|
725
|
+
* SECURITY: this changes UI decisions only. The API still authorizes every
|
|
726
|
+
* request with the real token, so "view as" can never grant access.
|
|
727
|
+
*/
|
|
728
|
+
/** What a piece of UI or a request requires. Empty or absent lists are no restriction. */
|
|
729
|
+
interface AccessRequirement {
|
|
730
|
+
roles?: readonly string[];
|
|
731
|
+
permissions?: readonly string[];
|
|
732
|
+
/** Require every listed role (default: any one of them). */
|
|
733
|
+
requireAllRoles?: boolean;
|
|
734
|
+
/** Require every listed permission (default: any one of them). */
|
|
735
|
+
requireAllPermissions?: boolean;
|
|
736
|
+
}
|
|
737
|
+
/** The roles and permissions a decision was made against. */
|
|
738
|
+
interface AccessSubject {
|
|
739
|
+
roles: readonly string[];
|
|
740
|
+
permissions: readonly string[];
|
|
741
|
+
}
|
|
742
|
+
/** Outcome of {@link evaluateAccess}. */
|
|
743
|
+
interface AccessEvaluation {
|
|
744
|
+
allowed: boolean;
|
|
745
|
+
/**
|
|
746
|
+
* What the subject lacks. In "any" mode a failed list is reported whole
|
|
747
|
+
* (holding any one of them would do); in "all" mode only the absent items.
|
|
748
|
+
*/
|
|
749
|
+
missing: {
|
|
750
|
+
roles: string[];
|
|
751
|
+
permissions: string[];
|
|
752
|
+
};
|
|
753
|
+
}
|
|
754
|
+
/** Where a reported decision came from. */
|
|
755
|
+
type AccessCheckSource =
|
|
756
|
+
/** `hasRole` / `hasPermission` and friends (core helpers and the store). */
|
|
757
|
+
'check'
|
|
758
|
+
/** `checkAuth` and the route guards built on it. */
|
|
759
|
+
| 'route'
|
|
760
|
+
/** `filterByAccess` / `canAccess` (navigation). */
|
|
761
|
+
| 'nav'
|
|
762
|
+
/** A 403 from the API. */
|
|
763
|
+
| 'http'
|
|
764
|
+
/** A UI element (the inspector's own `RequireAccess` / `requires`). */
|
|
765
|
+
| 'element';
|
|
766
|
+
interface AccessCheckEvent extends AccessEvaluation {
|
|
767
|
+
source: AccessCheckSource;
|
|
768
|
+
requirement: AccessRequirement;
|
|
769
|
+
/** Id of the user checked, or null for a guest. Checks also run for other users (admin lists). */
|
|
770
|
+
subjectId: string | null;
|
|
771
|
+
/** True when the decision used the "view as" override instead of the real user. */
|
|
772
|
+
simulated: boolean;
|
|
773
|
+
/** Human-readable context: a nav item id/href, or `METHOD /path` for HTTP. */
|
|
774
|
+
label?: string;
|
|
775
|
+
/** Present for `http` events. */
|
|
776
|
+
http?: {
|
|
777
|
+
method: string;
|
|
778
|
+
endpoint: string;
|
|
779
|
+
status: number;
|
|
780
|
+
code?: string;
|
|
781
|
+
};
|
|
782
|
+
}
|
|
783
|
+
type AccessProbe = (event: AccessCheckEvent) => void;
|
|
784
|
+
type SubjectUser = {
|
|
785
|
+
id?: string;
|
|
786
|
+
roles?: readonly string[];
|
|
787
|
+
permissions?: readonly string[];
|
|
788
|
+
};
|
|
789
|
+
/**
|
|
790
|
+
* Install (or with `null`, remove) the function that receives every access
|
|
791
|
+
* decision. Only one probe at a time; returns the previous one. No-op on the server.
|
|
792
|
+
*/
|
|
793
|
+
declare function setAccessProbe(next: AccessProbe | null): AccessProbe | null;
|
|
794
|
+
/** Whether a probe is installed — check before building an event. */
|
|
795
|
+
declare function isAccessProbeActive(): boolean;
|
|
796
|
+
/** Hand an event to the probe, if any. A throwing probe never breaks the check. */
|
|
797
|
+
declare function reportAccess(event: AccessCheckEvent): void;
|
|
798
|
+
/**
|
|
799
|
+
* Make access checks for `userId` use `subject` instead of that user's real
|
|
800
|
+
* roles and permissions. Checks for any other user are unaffected, so admin
|
|
801
|
+
* screens listing other people keep working. No-op on the server.
|
|
802
|
+
*/
|
|
803
|
+
declare function setAccessSubject(userId: string, subject: AccessSubject): void;
|
|
804
|
+
/** Remove the "view as" override. */
|
|
805
|
+
declare function clearAccessSubject(): void;
|
|
806
|
+
/** The active override, if any. */
|
|
807
|
+
declare function getAccessSubjectOverride(): {
|
|
808
|
+
userId: string;
|
|
809
|
+
subject: AccessSubject;
|
|
810
|
+
} | null;
|
|
811
|
+
/** Called whenever the override changes, so stores can re-run their checks. */
|
|
812
|
+
declare function onAccessSubjectChange(listener: () => void): () => void;
|
|
813
|
+
/**
|
|
814
|
+
* The roles and permissions to decide access with for `user`: the override
|
|
815
|
+
* when it targets this user, otherwise the user's own.
|
|
816
|
+
*/
|
|
817
|
+
declare function effectiveSubject(user: SubjectUser | null | undefined): AccessSubject & {
|
|
818
|
+
simulated: boolean;
|
|
819
|
+
};
|
|
820
|
+
/**
|
|
821
|
+
* Decide a requirement against a subject. Roles and permissions must both
|
|
822
|
+
* pass when both are given; within each list `requireAll*` picks all vs any.
|
|
823
|
+
* Exact membership, like the API (`admin` does not imply `agent`).
|
|
824
|
+
*/
|
|
825
|
+
declare function evaluateAccess(subject: AccessSubject, requirement: AccessRequirement): AccessEvaluation;
|
|
826
|
+
/**
|
|
827
|
+
* Report a decision the caller already made. Recomputes `missing` against
|
|
828
|
+
* the same subject so every source reports it the same way.
|
|
829
|
+
*/
|
|
830
|
+
declare function reportDecision(source: AccessCheckSource, user: SubjectUser | null | undefined, requirement: AccessRequirement, allowed: boolean, label?: string): void;
|
|
831
|
+
/**
|
|
832
|
+
* Read what a CHAPI 403 says was required. `requirePermissions` sends
|
|
833
|
+
* `{ required: string[] }` with code `INSUFFICIENT_PERMISSIONS`;
|
|
834
|
+
* `requireRole` sends `{ required: string }` with code `FORBIDDEN`. An
|
|
835
|
+
* optional `mode: 'any' | 'all'` overrides the defaults (permissions: all).
|
|
836
|
+
*/
|
|
837
|
+
declare function requirementFromDenial(code: string | undefined, details: unknown): AccessRequirement | null;
|
|
705
838
|
|
|
706
|
-
export { ApiKey, type ApiRequestOptions, type ApiResponse, ChangePasswordData, CreateApiKeyRequest, CreateApiKeyResponse, Device, type JWTPayload, LinkedAccount, LoginCredentials, LoginResponse, LogoutResponse, MFAChallengeData, MFAChallengeEmailResponse, MFASetupResponse, MFAStatus, Pagination, ProfileUpdateData, ROLE_HIERARCHY, ROLE_NAMES, type RefreshOutcome, type RefreshSessionOptions, RegisterData, RegisterResponse, type RoleName, SSO_CLIENT_STATE_PARAM, SSO_CLIENT_STATE_TTL_MS, SecurityEvent, Session, type SsoClientStateResult, type SsoClientStateVerification, UpdateApiKeyRequest, User, UserPreferences, UserRolesResponse, api, apiRequest, authApi, clearStoredAuth, computePrimaryRole, consumeSsoClientState, createSsoClientState, decodeJWT, extractClaims, extractData, getAccessToken, getAvailableMethods, getEffectiveLevel, getMfaToken, getRefreshToken, getSessionToken, getTokenExpiration, getTokenRemainingTime, getUserRoles, hasAllRoles, hasAnyRole, hasRole, initCrossTabSync, isLoginSuccessResponse, isMfaChallengeResponse, isTokenExpired, refreshSession, updateStoredTokens, verifySsoClientState };
|
|
839
|
+
export { type AccessCheckEvent, type AccessCheckSource, type AccessEvaluation, type AccessProbe, type AccessRequirement, type AccessSubject, ApiKey, type ApiRequestOptions, type ApiResponse, ChangePasswordData, CreateApiKeyRequest, CreateApiKeyResponse, Device, type JWTPayload, LinkedAccount, LoginCredentials, LoginResponse, LogoutResponse, MFAChallengeData, MFAChallengeEmailResponse, MFASetupResponse, MFAStatus, Pagination, ProfileUpdateData, ROLE_HIERARCHY, ROLE_NAMES, type RefreshOutcome, type RefreshSessionOptions, RegisterData, RegisterResponse, type RoleName, SSO_CLIENT_STATE_PARAM, SSO_CLIENT_STATE_TTL_MS, SecurityEvent, Session, type SsoClientStateResult, type SsoClientStateVerification, UpdateApiKeyRequest, User, UserPreferences, UserRolesResponse, api, apiRequest, authApi, clearAccessSubject, clearStoredAuth, computePrimaryRole, consumeSsoClientState, createSsoClientState, decodeJWT, effectiveSubject, evaluateAccess, extractClaims, extractData, getAccessSubjectOverride, getAccessToken, getAvailableMethods, getEffectiveLevel, getMfaToken, getRefreshToken, getSessionToken, getTokenExpiration, getTokenRemainingTime, getUserRoles, hasAllRoles, hasAnyRole, hasRole, initCrossTabSync, isAccessProbeActive, isLoginSuccessResponse, isMfaChallengeResponse, isTokenExpired, onAccessSubjectChange, refreshSession, reportAccess, reportDecision, requirementFromDenial, setAccessProbe, setAccessSubject, updateStoredTokens, verifySsoClientState };
|
package/dist/core/index.js
CHANGED
|
@@ -1,3 +1,3 @@
|
|
|
1
|
-
export { AuthService, authService, formatUserRoles, getAvailableMethods, getAvatarFallback, getDisplayName, getGreeting, getMfaToken, getUserEmail, getUserInitials, isLoginSuccessResponse, isMfaChallengeResponse } from '../chunk-
|
|
2
|
-
export { InvalidAuthResponseError, MFAChallengeEmailError, RoleDeniedError, SSO_CLIENT_STATE_PARAM, SSO_CLIENT_STATE_TTL_MS, SessionChangedError, api, apiRequest, authApi, clearStoredAuth, consumeSsoClientState, createSsoClientState, extractData, getAccessToken, getRefreshToken, getSessionToken, initCrossTabSync, isRoleDeniedError, isSessionChangedError, refreshSession, updateStoredTokens, verifySsoClientState } from '../chunk-
|
|
3
|
-
export { ROLE_HIERARCHY, ROLE_NAMES, computePrimaryRole, createMemoryStorage, createSessionStorage, decodeJWT, extractClaims, getConfig, getDefaultStorage, getEffectiveLevel, getFetch, getStorage, getTokenExpiration, getTokenRemainingTime, getUserRoles, hasAllRoles, hasAnyRole, hasRole, initAuth, isInitialized, isTokenExpired, resetConfig } from '../chunk-
|
|
1
|
+
export { AuthService, authService, formatUserRoles, getAvailableMethods, getAvatarFallback, getDisplayName, getGreeting, getMfaToken, getUserEmail, getUserInitials, isLoginSuccessResponse, isMfaChallengeResponse } from '../chunk-42EO3FSU.js';
|
|
2
|
+
export { ApiError, InvalidAuthResponseError, MFAChallengeEmailError, RoleDeniedError, SSO_CLIENT_STATE_PARAM, SSO_CLIENT_STATE_TTL_MS, SessionChangedError, api, apiRequest, authApi, clearStoredAuth, consumeSsoClientState, createSsoClientState, extractData, getAccessToken, getRefreshToken, getSessionToken, initCrossTabSync, isApiError, isRoleDeniedError, isSessionChangedError, refreshSession, updateStoredTokens, verifySsoClientState } from '../chunk-ADCPJOPQ.js';
|
|
3
|
+
export { ROLE_HIERARCHY, ROLE_NAMES, clearAccessSubject, computePrimaryRole, createMemoryStorage, createSessionStorage, decodeJWT, effectiveSubject, evaluateAccess, extractClaims, getAccessSubjectOverride, getConfig, getDefaultStorage, getEffectiveLevel, getFetch, getStorage, getTokenExpiration, getTokenRemainingTime, getUserRoles, hasAllRoles, hasAnyRole, hasRole, initAuth, isAccessProbeActive, isInitialized, isTokenExpired, onAccessSubjectChange, reportAccess, reportDecision, requirementFromDenial, resetConfig, setAccessProbe, setAccessSubject } from '../chunk-5X6E5MZG.js';
|
package/dist/index.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
1
|
export { A as ApiKey, a as AuthConfig, b as AuthState, C as ChangePasswordData, c as CreateApiKeyRequest, d as CreateApiKeyResponse, D as Device, L as LinkedAccount, e as LoginCredentials, f as LoginResponse, g as LogoutResponse, M as MFAChallengeData, h as MFAChallengeEmailResponse, i as MFASetupResponse, j as MFAStatus, P as Pagination, k as ProfileUpdateData, R as RegisterData, l as RegisterResponse, m as ResetPasswordData, S as SSOConfig, n as SecurityEvent, o as Session, p as StorageAdapter, U as UpdateApiKeyRequest, q as User, r as UserPreferences, s as UserRolesResponse, t as createMemoryStorage, u as createSessionStorage, v as getConfig, w as getDefaultStorage, x as getFetch, y as getStorage, z as initAuth, B as isInitialized, E as resetConfig } from './types-DF5Ny2IA.js';
|
|
2
|
-
export { ApiRequestOptions, ApiResponse, JWTPayload, ROLE_HIERARCHY, ROLE_NAMES, RefreshOutcome, RefreshSessionOptions, RoleName, SSO_CLIENT_STATE_PARAM, SSO_CLIENT_STATE_TTL_MS, SsoClientStateResult, SsoClientStateVerification, api, apiRequest, authApi, clearStoredAuth, computePrimaryRole, consumeSsoClientState, createSsoClientState, decodeJWT, extractClaims, extractData, getAccessToken, getAvailableMethods, getEffectiveLevel, getMfaToken, getRefreshToken, getSessionToken, getTokenExpiration, getTokenRemainingTime, getUserRoles, hasAllRoles, hasAnyRole, hasRole, initCrossTabSync, isLoginSuccessResponse, isMfaChallengeResponse, isTokenExpired, refreshSession, updateStoredTokens, verifySsoClientState } from './core/index.js';
|
|
3
|
-
export { A as AuthService, I as InvalidAuthResponseError, L as LoginOptions, M as MFAChallengeEmailError,
|
|
2
|
+
export { AccessCheckEvent, AccessCheckSource, AccessEvaluation, AccessProbe, AccessRequirement, AccessSubject, ApiRequestOptions, ApiResponse, JWTPayload, ROLE_HIERARCHY, ROLE_NAMES, RefreshOutcome, RefreshSessionOptions, RoleName, SSO_CLIENT_STATE_PARAM, SSO_CLIENT_STATE_TTL_MS, SsoClientStateResult, SsoClientStateVerification, api, apiRequest, authApi, clearAccessSubject, clearStoredAuth, computePrimaryRole, consumeSsoClientState, createSsoClientState, decodeJWT, effectiveSubject, evaluateAccess, extractClaims, extractData, getAccessSubjectOverride, getAccessToken, getAvailableMethods, getEffectiveLevel, getMfaToken, getRefreshToken, getSessionToken, getTokenExpiration, getTokenRemainingTime, getUserRoles, hasAllRoles, hasAnyRole, hasRole, initCrossTabSync, isAccessProbeActive, isLoginSuccessResponse, isMfaChallengeResponse, isTokenExpired, onAccessSubjectChange, refreshSession, reportAccess, reportDecision, requirementFromDenial, setAccessProbe, setAccessSubject, updateStoredTokens, verifySsoClientState } from './core/index.js';
|
|
3
|
+
export { A as ApiError, a as AuthService, I as InvalidAuthResponseError, L as LoginOptions, M as MFAChallengeEmailError, b as MFAVerifyOptions, R as RoleDeniedError, S as SessionChangedError, c as authService, f as formatUserRoles, g as getAvatarFallback, d as getDisplayName, e as getGreeting, h as getUserEmail, i as getUserInitials, j as isApiError, k as isRoleDeniedError, l as isSessionChangedError } from './user-utils-DYyljbJR.js';
|
|
4
4
|
export { AuthClient, AuthGuardOptions, AuthGuardResult, AuthHookOptions, CreateAuthClientOptions, NavFilterOptions, RoleRestrictedItem, authActions, authStore, canAccess, checkAuth, createAuthClient, createAuthGuard, createAuthHook, createNavFilter, currentUser, filterByAccess, filterNavSections, isAuthenticated, matchesRoute, protectedLoad, requireAuth, requirePermission, requireRole, routePatterns } from './svelte/index.js';
|
package/dist/index.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
export { canAccess, checkAuth, createAuthClient, createAuthGuard, createAuthHook, createNavFilter, filterByAccess, filterNavSections, matchesRoute, protectedLoad, requireAuth, requirePermission, requireRole, routePatterns } from './chunk-
|
|
2
|
-
export { AuthService, authService, formatUserRoles, getAvailableMethods, getAvatarFallback, getDisplayName, getGreeting, getMfaToken, getUserEmail, getUserInitials, isLoginSuccessResponse, isMfaChallengeResponse } from './chunk-
|
|
3
|
-
export { authActions, authStore, currentUser, isAuthenticated } from './chunk-
|
|
4
|
-
export { InvalidAuthResponseError, MFAChallengeEmailError, RoleDeniedError, SSO_CLIENT_STATE_PARAM, SSO_CLIENT_STATE_TTL_MS, SessionChangedError, api, apiRequest, authApi, clearStoredAuth, consumeSsoClientState, createSsoClientState, extractData, getAccessToken, getRefreshToken, getSessionToken, initCrossTabSync, isRoleDeniedError, isSessionChangedError, refreshSession, updateStoredTokens, verifySsoClientState } from './chunk-
|
|
5
|
-
export { ROLE_HIERARCHY, ROLE_NAMES, computePrimaryRole, createMemoryStorage, createSessionStorage, decodeJWT, extractClaims, getConfig, getDefaultStorage, getEffectiveLevel, getFetch, getStorage, getTokenExpiration, getTokenRemainingTime, getUserRoles, hasAllRoles, hasAnyRole, hasRole, initAuth, isInitialized, isTokenExpired, resetConfig } from './chunk-
|
|
1
|
+
export { canAccess, checkAuth, createAuthClient, createAuthGuard, createAuthHook, createNavFilter, filterByAccess, filterNavSections, matchesRoute, protectedLoad, requireAuth, requirePermission, requireRole, routePatterns } from './chunk-J2WAFXA6.js';
|
|
2
|
+
export { AuthService, authService, formatUserRoles, getAvailableMethods, getAvatarFallback, getDisplayName, getGreeting, getMfaToken, getUserEmail, getUserInitials, isLoginSuccessResponse, isMfaChallengeResponse } from './chunk-42EO3FSU.js';
|
|
3
|
+
export { authActions, authStore, currentUser, isAuthenticated } from './chunk-W2ASN4NE.js';
|
|
4
|
+
export { ApiError, InvalidAuthResponseError, MFAChallengeEmailError, RoleDeniedError, SSO_CLIENT_STATE_PARAM, SSO_CLIENT_STATE_TTL_MS, SessionChangedError, api, apiRequest, authApi, clearStoredAuth, consumeSsoClientState, createSsoClientState, extractData, getAccessToken, getRefreshToken, getSessionToken, initCrossTabSync, isApiError, isRoleDeniedError, isSessionChangedError, refreshSession, updateStoredTokens, verifySsoClientState } from './chunk-ADCPJOPQ.js';
|
|
5
|
+
export { ROLE_HIERARCHY, ROLE_NAMES, clearAccessSubject, computePrimaryRole, createMemoryStorage, createSessionStorage, decodeJWT, effectiveSubject, evaluateAccess, extractClaims, getAccessSubjectOverride, getConfig, getDefaultStorage, getEffectiveLevel, getFetch, getStorage, getTokenExpiration, getTokenRemainingTime, getUserRoles, hasAllRoles, hasAnyRole, hasRole, initAuth, isAccessProbeActive, isInitialized, isTokenExpired, onAccessSubjectChange, reportAccess, reportDecision, requirementFromDenial, resetConfig, setAccessProbe, setAccessSubject } from './chunk-5X6E5MZG.js';
|
package/dist/svelte/index.d.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { b as AuthState, q as User, a as AuthConfig } from '../types-DF5Ny2IA.js';
|
|
2
2
|
export { z as initAuth, B as isInitialized } from '../types-DF5Ny2IA.js';
|
|
3
|
-
import {
|
|
4
|
-
export { I as InvalidAuthResponseError, R as RoleDeniedError, S as SessionChangedError, f as formatUserRoles, g as getAvatarFallback,
|
|
3
|
+
import { c as authService } from '../user-utils-DYyljbJR.js';
|
|
4
|
+
export { I as InvalidAuthResponseError, R as RoleDeniedError, S as SessionChangedError, f as formatUserRoles, g as getAvatarFallback, d as getDisplayName, e as getGreeting, h as getUserEmail, i as getUserInitials, k as isRoleDeniedError, l as isSessionChangedError } from '../user-utils-DYyljbJR.js';
|
|
5
5
|
|
|
6
6
|
/**
|
|
7
7
|
* Auth Store
|
|
@@ -90,6 +90,11 @@ declare class AuthStore {
|
|
|
90
90
|
* Check if user has all of the specified permissions.
|
|
91
91
|
*/
|
|
92
92
|
hasAllPermissions(permissions: string[]): boolean;
|
|
93
|
+
/**
|
|
94
|
+
* Permission checks share one path so they honor the access probe's
|
|
95
|
+
* "view as" override and report to it. A guest holds no permissions.
|
|
96
|
+
*/
|
|
97
|
+
private checkPermissions;
|
|
93
98
|
/**
|
|
94
99
|
* Re-hydrate state from storage (useful after config changes).
|
|
95
100
|
*/
|
|
@@ -174,6 +179,14 @@ interface AuthGuardResult {
|
|
|
174
179
|
reason?: 'not_authenticated' | 'missing_role' | 'missing_permission';
|
|
175
180
|
/** Redirect URL suggestion */
|
|
176
181
|
redirectTo?: string;
|
|
182
|
+
/**
|
|
183
|
+
* What the user lacks when denied for a role or permission: the whole
|
|
184
|
+
* list in "any" mode, only the absent items in "all" mode.
|
|
185
|
+
*/
|
|
186
|
+
missing?: {
|
|
187
|
+
roles: string[];
|
|
188
|
+
permissions: string[];
|
|
189
|
+
};
|
|
177
190
|
}
|
|
178
191
|
/**
|
|
179
192
|
* Check if the current user passes the auth guard.
|
package/dist/svelte/index.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
export { canAccess, checkAuth, createAuthClient, createAuthGuard, createAuthHook, createNavFilter, filterByAccess, filterNavSections, matchesRoute, protectedLoad, requireAuth, requirePermission, requireRole, routePatterns } from '../chunk-
|
|
2
|
-
export { formatUserRoles, getAvatarFallback, getDisplayName, getGreeting, getUserEmail, getUserInitials } from '../chunk-
|
|
3
|
-
export { authActions, authStore, currentUser, isAuthenticated } from '../chunk-
|
|
4
|
-
export { InvalidAuthResponseError, RoleDeniedError, SessionChangedError, isRoleDeniedError, isSessionChangedError } from '../chunk-
|
|
5
|
-
export { initAuth, isInitialized } from '../chunk-
|
|
1
|
+
export { canAccess, checkAuth, createAuthClient, createAuthGuard, createAuthHook, createNavFilter, filterByAccess, filterNavSections, matchesRoute, protectedLoad, requireAuth, requirePermission, requireRole, routePatterns } from '../chunk-J2WAFXA6.js';
|
|
2
|
+
export { formatUserRoles, getAvatarFallback, getDisplayName, getGreeting, getUserEmail, getUserInitials } from '../chunk-42EO3FSU.js';
|
|
3
|
+
export { authActions, authStore, currentUser, isAuthenticated } from '../chunk-W2ASN4NE.js';
|
|
4
|
+
export { InvalidAuthResponseError, RoleDeniedError, SessionChangedError, isRoleDeniedError, isSessionChangedError } from '../chunk-ADCPJOPQ.js';
|
|
5
|
+
export { initAuth, isInitialized } from '../chunk-5X6E5MZG.js';
|
package/dist/testing/index.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { hasRole, hasAnyRole, hasAllRoles, initAuth, resetConfig, getUserRoles, isTokenExpired, decodeJWT } from '../chunk-
|
|
1
|
+
import { hasRole, hasAnyRole, hasAllRoles, initAuth, resetConfig, getUserRoles, isTokenExpired, decodeJWT } from '../chunk-5X6E5MZG.js';
|
|
2
2
|
|
|
3
3
|
// src/testing/fixtures/users.ts
|
|
4
4
|
var mockUser = {
|
|
@@ -356,6 +356,38 @@ declare class MFAChallengeEmailError extends Error {
|
|
|
356
356
|
readonly retryAfterSeconds?: number;
|
|
357
357
|
constructor(message: string, status: number, retryAfterSeconds?: number);
|
|
358
358
|
}
|
|
359
|
+
/**
|
|
360
|
+
* Error thrown by `apiRequest` (and the `api.*` helpers) when the API answers
|
|
361
|
+
* with a non-2xx status. `message` is the server's message, exactly as the
|
|
362
|
+
* plain `Error` used to carry, so existing `catch (e) { e.message }` code is
|
|
363
|
+
* unaffected; the status, CHAPI error code and `details` are kept alongside.
|
|
364
|
+
*
|
|
365
|
+
* For a 403, `details.required` lists what CHAPI said was missing.
|
|
366
|
+
*/
|
|
367
|
+
declare class ApiError extends Error {
|
|
368
|
+
readonly name = "ApiError";
|
|
369
|
+
/** HTTP status. */
|
|
370
|
+
readonly status: number;
|
|
371
|
+
/** CHAPI error code (e.g. `INSUFFICIENT_PERMISSIONS`), when sent. */
|
|
372
|
+
readonly code?: string;
|
|
373
|
+
/** CHAPI `error.details`, when sent. */
|
|
374
|
+
readonly details?: Record<string, unknown>;
|
|
375
|
+
/** Request method. */
|
|
376
|
+
readonly method: string;
|
|
377
|
+
/** The endpoint as passed to `apiRequest`. */
|
|
378
|
+
readonly endpoint: string;
|
|
379
|
+
constructor(message: string, init: {
|
|
380
|
+
status: number;
|
|
381
|
+
code?: string;
|
|
382
|
+
details?: Record<string, unknown>;
|
|
383
|
+
method: string;
|
|
384
|
+
endpoint: string;
|
|
385
|
+
});
|
|
386
|
+
}
|
|
387
|
+
/**
|
|
388
|
+
* Type guard to check if an error is an ApiError.
|
|
389
|
+
*/
|
|
390
|
+
declare function isApiError(error: unknown): error is ApiError;
|
|
359
391
|
|
|
360
392
|
/**
|
|
361
393
|
* User Display Utilities
|
|
@@ -460,4 +492,4 @@ declare function formatUserRoles(user: User | null | undefined, options?: {
|
|
|
460
492
|
capitalize?: boolean;
|
|
461
493
|
}): string;
|
|
462
494
|
|
|
463
|
-
export {
|
|
495
|
+
export { ApiError as A, InvalidAuthResponseError as I, type LoginOptions as L, MFAChallengeEmailError as M, RoleDeniedError as R, SessionChangedError as S, AuthService as a, type MFAVerifyOptions as b, authService as c, getDisplayName as d, getGreeting as e, formatUserRoles as f, getAvatarFallback as g, getUserEmail as h, getUserInitials as i, isApiError as j, isRoleDeniedError as k, isSessionChangedError as l };
|
package/package.json
CHANGED