@classic-homes/auth 1.42.1 → 1.46.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 +55 -0
- package/dist/auth.svelte-IWSWWF6U.js +3 -0
- package/dist/{chunk-QV3DUSP5.js → chunk-7SFVHDFP.js} +98 -4
- package/dist/{chunk-A6JJQCBU.js → chunk-BGQVBSXN.js} +9 -5
- package/dist/{chunk-K6TV3ZVS.js → chunk-EFS272S3.js} +127 -6
- package/dist/{chunk-IR6DVUUF.js → chunk-Q4Q3CJVC.js} +20 -5
- package/dist/{chunk-G6UH4HBI.js → chunk-TJRFP24M.js} +40 -23
- package/dist/core/index.d.ts +169 -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-FUCU4DYP.js +0 -3
package/README.md
CHANGED
|
@@ -337,6 +337,51 @@ 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
|
+
- While a probe is installed, requests to the API origin (never other hosts)
|
|
368
|
+
send `X-Access-Debug: 1`. CHAPI answers admin sessions with
|
|
369
|
+
`X-Access-Redacted: taxId=vendors:read:sensitive, notes=*`, reported as an
|
|
370
|
+
informational `source: 'redacted'` event (`allowed: true`, path only) with
|
|
371
|
+
`fields: { name, permission }[]`; `*` means no permission is configured
|
|
372
|
+
(default-deny). It reflects the real token, never "view as". No header, no
|
|
373
|
+
event. The custom header makes cross-origin API requests preflighted.
|
|
374
|
+
- `setAccessSubject(userId, { roles, permissions })` previews the UI as another
|
|
375
|
+
role set ("view as") for that user only. Checks for other users are
|
|
376
|
+
unaffected, `authStore.user` is never changed, and the preview ends on any
|
|
377
|
+
login, logout or session expiry. Sign-in role gating (`allowedRoles`) always
|
|
378
|
+
uses the real roles.
|
|
379
|
+
- `evaluateAccess(subject, requirement)` is the shared any/all decision
|
|
380
|
+
`checkAuth` uses; `checkAuth` results now include `missing` on denial.
|
|
381
|
+
|
|
382
|
+
These change UI decisions only. The API authorizes every request with the real
|
|
383
|
+
token, so a preview can never grant access.
|
|
384
|
+
|
|
340
385
|
## API Reference
|
|
341
386
|
|
|
342
387
|
### Core Exports
|
|
@@ -376,6 +421,16 @@ import {
|
|
|
376
421
|
hasAnyRole,
|
|
377
422
|
hasAllRoles,
|
|
378
423
|
|
|
424
|
+
// Errors
|
|
425
|
+
ApiError,
|
|
426
|
+
isApiError,
|
|
427
|
+
|
|
428
|
+
// Access probe (see "API errors and access debugging")
|
|
429
|
+
setAccessProbe,
|
|
430
|
+
setAccessSubject,
|
|
431
|
+
clearAccessSubject,
|
|
432
|
+
evaluateAccess,
|
|
433
|
+
|
|
379
434
|
// Types
|
|
380
435
|
type User,
|
|
381
436
|
type UserRolesResponse,
|
|
@@ -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, ACCESS_DEBUG_HEADER, ACCESS_REDACTED_HEADER, ACCESS_REDACTED_TRUNCATED_HEADER, parseRedactedHeader, reportAccess, requirementFromDenial, decodeJWT } from './chunk-EFS272S3.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-IWSWWF6U.js');
|
|
222
240
|
apply(authStore);
|
|
223
241
|
} catch {
|
|
224
242
|
}
|
|
@@ -371,6 +389,65 @@ 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 probePath(endpoint) {
|
|
396
|
+
let path = endpoint.split(/[?#]/, 1)[0];
|
|
397
|
+
try {
|
|
398
|
+
if (/^https?:\/\//i.test(path)) path = new URL(path).pathname;
|
|
399
|
+
} catch {
|
|
400
|
+
}
|
|
401
|
+
return path;
|
|
402
|
+
}
|
|
403
|
+
function reportRedacted(method, endpoint, response) {
|
|
404
|
+
let header;
|
|
405
|
+
let truncated;
|
|
406
|
+
try {
|
|
407
|
+
header = response.headers.get(ACCESS_REDACTED_HEADER);
|
|
408
|
+
truncated = response.headers.get(ACCESS_REDACTED_TRUNCATED_HEADER);
|
|
409
|
+
} catch {
|
|
410
|
+
return;
|
|
411
|
+
}
|
|
412
|
+
const { fields, omitted } = parseRedactedHeader(header);
|
|
413
|
+
if (fields.length === 0) return;
|
|
414
|
+
const serverOmitted = Number.parseInt(truncated ?? "", 10);
|
|
415
|
+
const fieldsOmitted = omitted + (Number.isFinite(serverOmitted) && serverOmitted > 0 ? Math.min(serverOmitted, 1e6) : 0);
|
|
416
|
+
const permissions = [...new Set(fields.map((f) => f.permission).filter((p) => p !== "*"))];
|
|
417
|
+
const path = probePath(endpoint);
|
|
418
|
+
reportAccess({
|
|
419
|
+
source: "redacted",
|
|
420
|
+
requirement: permissions.length ? { permissions } : {},
|
|
421
|
+
// Informational: the request succeeded, the fields say what was withheld.
|
|
422
|
+
allowed: true,
|
|
423
|
+
missing: { roles: [], permissions: [] },
|
|
424
|
+
// The server redacted for the real token, never the "view as" preview.
|
|
425
|
+
subjectId: null,
|
|
426
|
+
simulated: false,
|
|
427
|
+
label: `${method} ${path}`,
|
|
428
|
+
http: { method, endpoint: path, status: response.status },
|
|
429
|
+
fields,
|
|
430
|
+
...fieldsOmitted > 0 && { fieldsOmitted }
|
|
431
|
+
});
|
|
432
|
+
}
|
|
433
|
+
function reportForbidden(method, endpoint, code, details) {
|
|
434
|
+
const path = probePath(endpoint);
|
|
435
|
+
const requirement = requirementFromDenial(code, details) ?? {};
|
|
436
|
+
reportAccess({
|
|
437
|
+
source: "http",
|
|
438
|
+
requirement,
|
|
439
|
+
allowed: false,
|
|
440
|
+
missing: {
|
|
441
|
+
roles: [...requirement.roles ?? []],
|
|
442
|
+
permissions: [...requirement.permissions ?? []]
|
|
443
|
+
},
|
|
444
|
+
// The server decided against the real token; whose id is the caller's business.
|
|
445
|
+
subjectId: null,
|
|
446
|
+
simulated: false,
|
|
447
|
+
label: `${method} ${path}`,
|
|
448
|
+
http: { method, endpoint: path, status: 403, code }
|
|
449
|
+
});
|
|
450
|
+
}
|
|
374
451
|
function apiRequest(endpoint, options = {}) {
|
|
375
452
|
return apiRequestInternal(endpoint, options, false);
|
|
376
453
|
}
|
|
@@ -422,6 +499,8 @@ async function apiRequestInternal(endpoint, options, isRetry, startedIn) {
|
|
|
422
499
|
"Content-Type": "application/json",
|
|
423
500
|
...fetchOptions.headers
|
|
424
501
|
};
|
|
502
|
+
const accessDebug = isAccessProbeActive() && (!isAbsolute || isApiOrigin(url, config.baseUrl));
|
|
503
|
+
if (accessDebug) headers[ACCESS_DEBUG_HEADER] = "1";
|
|
425
504
|
if (authenticated) {
|
|
426
505
|
const accessToken = getAccessToken();
|
|
427
506
|
const sessionToken = getSessionToken();
|
|
@@ -455,6 +534,9 @@ async function apiRequestInternal(endpoint, options, isRetry, startedIn) {
|
|
|
455
534
|
}
|
|
456
535
|
throw new Error("Authentication service unavailable. Please try again.");
|
|
457
536
|
}
|
|
537
|
+
if (accessDebug && isAccessProbeActive()) {
|
|
538
|
+
reportRedacted((fetchOptions.method ?? "GET").toUpperCase(), endpoint, response);
|
|
539
|
+
}
|
|
458
540
|
let data;
|
|
459
541
|
const contentType = response.headers.get("content-type");
|
|
460
542
|
if (contentType?.includes("application/json")) {
|
|
@@ -496,7 +578,19 @@ async function apiRequestInternal(endpoint, options, isRetry, startedIn) {
|
|
|
496
578
|
if (!response.ok) {
|
|
497
579
|
const errorBody = data ?? {};
|
|
498
580
|
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})`;
|
|
499
|
-
const
|
|
581
|
+
const code = typeof errorBody.error?.code === "string" ? errorBody.error.code : void 0;
|
|
582
|
+
const details = isRecord(errorBody.error?.details) ? errorBody.error.details : void 0;
|
|
583
|
+
const method = (fetchOptions.method ?? "GET").toUpperCase();
|
|
584
|
+
const error = new ApiError(errorMessage, {
|
|
585
|
+
status: response.status,
|
|
586
|
+
code,
|
|
587
|
+
details,
|
|
588
|
+
method,
|
|
589
|
+
endpoint
|
|
590
|
+
});
|
|
591
|
+
if (response.status === 403 && isAccessProbeActive()) {
|
|
592
|
+
reportForbidden(method, endpoint, code, details);
|
|
593
|
+
}
|
|
500
594
|
config.onAuthError?.(error);
|
|
501
595
|
throw error;
|
|
502
596
|
}
|
|
@@ -1100,4 +1194,4 @@ var authApi = {
|
|
|
1100
1194
|
}
|
|
1101
1195
|
};
|
|
1102
1196
|
|
|
1103
|
-
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 };
|
|
1197
|
+
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,5 +1,5 @@
|
|
|
1
|
-
import { authApi, InvalidAuthResponseError, RoleDeniedError } from './chunk-
|
|
2
|
-
import {
|
|
1
|
+
import { authApi, InvalidAuthResponseError, RoleDeniedError } from './chunk-7SFVHDFP.js';
|
|
2
|
+
import { getUserRoles } from './chunk-EFS272S3.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-IWSWWF6U.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,121 @@ 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
|
+
var ACCESS_DEBUG_HEADER = "X-Access-Debug";
|
|
291
|
+
var ACCESS_REDACTED_HEADER = "X-Access-Redacted";
|
|
292
|
+
var ACCESS_REDACTED_TRUNCATED_HEADER = "X-Access-Redacted-Truncated";
|
|
293
|
+
var MAX_REDACTED_FIELDS = 100;
|
|
294
|
+
var MAX_REDACTED_HEADER = 16384;
|
|
295
|
+
var REDACTED_ENTRY = /^([A-Za-z0-9_.:*-]{1,128})=([A-Za-z0-9_.:*-]{1,128})$/;
|
|
296
|
+
function parseRedactedHeader(value) {
|
|
297
|
+
const fields = [];
|
|
298
|
+
let omitted = 0;
|
|
299
|
+
if (!value) return { fields, omitted };
|
|
300
|
+
const seen = /* @__PURE__ */ new Set();
|
|
301
|
+
for (const raw of value.slice(0, MAX_REDACTED_HEADER).split(",")) {
|
|
302
|
+
const match = REDACTED_ENTRY.exec(raw.trim());
|
|
303
|
+
if (!match || seen.has(match[1])) continue;
|
|
304
|
+
seen.add(match[1]);
|
|
305
|
+
if (fields.length >= MAX_REDACTED_FIELDS) omitted += 1;
|
|
306
|
+
else fields.push({ name: match[1], permission: match[2] });
|
|
307
|
+
}
|
|
308
|
+
return { fields, omitted };
|
|
309
|
+
}
|
|
310
|
+
|
|
196
311
|
// src/core/roles.ts
|
|
197
312
|
var ROLE_NAMES = [
|
|
198
313
|
"admin",
|
|
@@ -275,15 +390,21 @@ function getEffectiveLevel(roles) {
|
|
|
275
390
|
return max;
|
|
276
391
|
}
|
|
277
392
|
function hasRole(user, role) {
|
|
278
|
-
|
|
393
|
+
const allowed = effectiveSubject(user).roles.includes(role);
|
|
394
|
+
reportDecision("check", user, { roles: [role] }, allowed);
|
|
395
|
+
return allowed;
|
|
279
396
|
}
|
|
280
397
|
function hasAnyRole(user, roles) {
|
|
281
|
-
const held =
|
|
282
|
-
|
|
398
|
+
const held = effectiveSubject(user).roles;
|
|
399
|
+
const allowed = roles.some((role) => held.includes(role));
|
|
400
|
+
if (roles.length > 0) reportDecision("check", user, { roles }, allowed);
|
|
401
|
+
return allowed;
|
|
283
402
|
}
|
|
284
403
|
function hasAllRoles(user, roles) {
|
|
285
|
-
const held =
|
|
286
|
-
|
|
404
|
+
const held = effectiveSubject(user).roles;
|
|
405
|
+
const allowed = roles.every((role) => held.includes(role));
|
|
406
|
+
if (roles.length > 0) reportDecision("check", user, { roles, requireAllRoles: true }, allowed);
|
|
407
|
+
return allowed;
|
|
287
408
|
}
|
|
288
409
|
|
|
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 };
|
|
410
|
+
export { ACCESS_DEBUG_HEADER, ACCESS_REDACTED_HEADER, ACCESS_REDACTED_TRUNCATED_HEADER, 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, parseRedactedHeader, reportAccess, reportDecision, requirementFromDenial, resetConfig, setAccessProbe, setAccessSubject };
|
|
@@ -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-7SFVHDFP.js';
|
|
2
|
+
import { onAccessSubjectChange, isInitialized, getConfig, isValidSSOLogoutUrl, hasRole, hasAnyRole, hasAllRoles, effectiveSubject, reportDecision, decodeJWT, getStorage, getDefaultStorage } from './chunk-EFS272S3.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).
|
|
@@ -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-BGQVBSXN.js';
|
|
2
|
+
import { authStore, currentUser, isAuthenticated, authActions } from './chunk-Q4Q3CJVC.js';
|
|
3
|
+
import { evaluateAccess, effectiveSubject, isAccessProbeActive, reportDecision, isInitialized, initAuth } from './chunk-EFS272S3.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) {
|
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,187 @@ 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
|
+
/**
|
|
767
|
+
* Fields the API left out of a response (`X-Access-Redacted`). Informational,
|
|
768
|
+
* not a denial: `allowed` is true and `missing` is empty; see `fields`.
|
|
769
|
+
*/
|
|
770
|
+
| 'redacted';
|
|
771
|
+
/** A top-level response field the API withheld, and the permission that would reveal it. */
|
|
772
|
+
interface RedactedField {
|
|
773
|
+
name: string;
|
|
774
|
+
/** `*` = the field has no permission configured and is dropped by default-deny. */
|
|
775
|
+
permission: string;
|
|
776
|
+
}
|
|
777
|
+
interface AccessCheckEvent extends AccessEvaluation {
|
|
778
|
+
source: AccessCheckSource;
|
|
779
|
+
requirement: AccessRequirement;
|
|
780
|
+
/** Id of the user checked, or null for a guest. Checks also run for other users (admin lists). */
|
|
781
|
+
subjectId: string | null;
|
|
782
|
+
/** True when the decision used the "view as" override instead of the real user. */
|
|
783
|
+
simulated: boolean;
|
|
784
|
+
/** Human-readable context: a nav item id/href, or `METHOD /path` for HTTP. */
|
|
785
|
+
label?: string;
|
|
786
|
+
/** Present for `http` and `redacted` events. */
|
|
787
|
+
http?: {
|
|
788
|
+
method: string;
|
|
789
|
+
endpoint: string;
|
|
790
|
+
status: number;
|
|
791
|
+
code?: string;
|
|
792
|
+
};
|
|
793
|
+
/** Present for `redacted` events: the withheld fields, in the order the server listed them. */
|
|
794
|
+
fields?: RedactedField[];
|
|
795
|
+
/** `redacted` events: entries the server (or the parser's cap) left out of `fields`. */
|
|
796
|
+
fieldsOmitted?: number;
|
|
797
|
+
}
|
|
798
|
+
type AccessProbe = (event: AccessCheckEvent) => void;
|
|
799
|
+
type SubjectUser = {
|
|
800
|
+
id?: string;
|
|
801
|
+
roles?: readonly string[];
|
|
802
|
+
permissions?: readonly string[];
|
|
803
|
+
};
|
|
804
|
+
/**
|
|
805
|
+
* Install (or with `null`, remove) the function that receives every access
|
|
806
|
+
* decision. Only one probe at a time; returns the previous one. No-op on the server.
|
|
807
|
+
*/
|
|
808
|
+
declare function setAccessProbe(next: AccessProbe | null): AccessProbe | null;
|
|
809
|
+
/** Whether a probe is installed — check before building an event. */
|
|
810
|
+
declare function isAccessProbeActive(): boolean;
|
|
811
|
+
/** Hand an event to the probe, if any. A throwing probe never breaks the check. */
|
|
812
|
+
declare function reportAccess(event: AccessCheckEvent): void;
|
|
813
|
+
/**
|
|
814
|
+
* Make access checks for `userId` use `subject` instead of that user's real
|
|
815
|
+
* roles and permissions. Checks for any other user are unaffected, so admin
|
|
816
|
+
* screens listing other people keep working. No-op on the server.
|
|
817
|
+
*/
|
|
818
|
+
declare function setAccessSubject(userId: string, subject: AccessSubject): void;
|
|
819
|
+
/** Remove the "view as" override. */
|
|
820
|
+
declare function clearAccessSubject(): void;
|
|
821
|
+
/** The active override, if any. */
|
|
822
|
+
declare function getAccessSubjectOverride(): {
|
|
823
|
+
userId: string;
|
|
824
|
+
subject: AccessSubject;
|
|
825
|
+
} | null;
|
|
826
|
+
/** Called whenever the override changes, so stores can re-run their checks. */
|
|
827
|
+
declare function onAccessSubjectChange(listener: () => void): () => void;
|
|
828
|
+
/**
|
|
829
|
+
* The roles and permissions to decide access with for `user`: the override
|
|
830
|
+
* when it targets this user, otherwise the user's own.
|
|
831
|
+
*/
|
|
832
|
+
declare function effectiveSubject(user: SubjectUser | null | undefined): AccessSubject & {
|
|
833
|
+
simulated: boolean;
|
|
834
|
+
};
|
|
835
|
+
/**
|
|
836
|
+
* Decide a requirement against a subject. Roles and permissions must both
|
|
837
|
+
* pass when both are given; within each list `requireAll*` picks all vs any.
|
|
838
|
+
* Exact membership, like the API (`admin` does not imply `agent`).
|
|
839
|
+
*/
|
|
840
|
+
declare function evaluateAccess(subject: AccessSubject, requirement: AccessRequirement): AccessEvaluation;
|
|
841
|
+
/**
|
|
842
|
+
* Report a decision the caller already made. Recomputes `missing` against
|
|
843
|
+
* the same subject so every source reports it the same way.
|
|
844
|
+
*/
|
|
845
|
+
declare function reportDecision(source: AccessCheckSource, user: SubjectUser | null | undefined, requirement: AccessRequirement, allowed: boolean, label?: string): void;
|
|
846
|
+
/**
|
|
847
|
+
* Read what a CHAPI 403 says was required. `requirePermissions` sends
|
|
848
|
+
* `{ required: string[] }` with code `INSUFFICIENT_PERMISSIONS`;
|
|
849
|
+
* `requireRole` sends `{ required: string }` with code `FORBIDDEN`. An
|
|
850
|
+
* optional `mode: 'any' | 'all'` overrides the defaults (permissions: all).
|
|
851
|
+
*/
|
|
852
|
+
declare function requirementFromDenial(code: string | undefined, details: unknown): AccessRequirement | null;
|
|
853
|
+
/** Request header asking CHAPI to say which fields it redacted (admin sessions only). */
|
|
854
|
+
declare const ACCESS_DEBUG_HEADER = "X-Access-Debug";
|
|
855
|
+
/** Response header: `name=permission` entries joined by `", "`. */
|
|
856
|
+
declare const ACCESS_REDACTED_HEADER = "X-Access-Redacted";
|
|
857
|
+
/** Response header: how many entries the server left out of {@link ACCESS_REDACTED_HEADER}. */
|
|
858
|
+
declare const ACCESS_REDACTED_TRUNCATED_HEADER = "X-Access-Redacted-Truncated";
|
|
859
|
+
/**
|
|
860
|
+
* Parse an `X-Access-Redacted` value (`taxId=vendors:read:sensitive, notes=*`).
|
|
861
|
+
* Malformed entries are skipped, a repeated name keeps its first permission,
|
|
862
|
+
* and at most 100 fields are kept; `omitted` counts the valid ones dropped by
|
|
863
|
+
* that cap. A missing or empty header yields no fields.
|
|
864
|
+
*/
|
|
865
|
+
declare function parseRedactedHeader(value: string | null | undefined): {
|
|
866
|
+
fields: RedactedField[];
|
|
867
|
+
omitted: number;
|
|
868
|
+
};
|
|
705
869
|
|
|
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 };
|
|
870
|
+
export { ACCESS_DEBUG_HEADER, ACCESS_REDACTED_HEADER, ACCESS_REDACTED_TRUNCATED_HEADER, 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 RedactedField, 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, parseRedactedHeader, 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-BGQVBSXN.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-7SFVHDFP.js';
|
|
3
|
+
export { ACCESS_DEBUG_HEADER, ACCESS_REDACTED_HEADER, ACCESS_REDACTED_TRUNCATED_HEADER, 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, parseRedactedHeader, reportAccess, reportDecision, requirementFromDenial, resetConfig, setAccessProbe, setAccessSubject } from '../chunk-EFS272S3.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 { ACCESS_DEBUG_HEADER, ACCESS_REDACTED_HEADER, ACCESS_REDACTED_TRUNCATED_HEADER, AccessCheckEvent, AccessCheckSource, AccessEvaluation, AccessProbe, AccessRequirement, AccessSubject, ApiRequestOptions, ApiResponse, JWTPayload, ROLE_HIERARCHY, ROLE_NAMES, RedactedField, 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, parseRedactedHeader, 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-TJRFP24M.js';
|
|
2
|
+
export { AuthService, authService, formatUserRoles, getAvailableMethods, getAvatarFallback, getDisplayName, getGreeting, getMfaToken, getUserEmail, getUserInitials, isLoginSuccessResponse, isMfaChallengeResponse } from './chunk-BGQVBSXN.js';
|
|
3
|
+
export { authActions, authStore, currentUser, isAuthenticated } from './chunk-Q4Q3CJVC.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-7SFVHDFP.js';
|
|
5
|
+
export { ACCESS_DEBUG_HEADER, ACCESS_REDACTED_HEADER, ACCESS_REDACTED_TRUNCATED_HEADER, 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, parseRedactedHeader, reportAccess, reportDecision, requirementFromDenial, resetConfig, setAccessProbe, setAccessSubject } from './chunk-EFS272S3.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-TJRFP24M.js';
|
|
2
|
+
export { formatUserRoles, getAvatarFallback, getDisplayName, getGreeting, getUserEmail, getUserInitials } from '../chunk-BGQVBSXN.js';
|
|
3
|
+
export { authActions, authStore, currentUser, isAuthenticated } from '../chunk-Q4Q3CJVC.js';
|
|
4
|
+
export { InvalidAuthResponseError, RoleDeniedError, SessionChangedError, isRoleDeniedError, isSessionChangedError } from '../chunk-7SFVHDFP.js';
|
|
5
|
+
export { initAuth, isInitialized } from '../chunk-EFS272S3.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-EFS272S3.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