@quazardous/qdadm 2.17.2 → 2.17.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json
CHANGED
|
@@ -357,6 +357,61 @@ export class EntityManager<T extends EntityRecord = EntityRecord> {
|
|
|
357
357
|
return { storage: this.storage }
|
|
358
358
|
}
|
|
359
359
|
|
|
360
|
+
/**
|
|
361
|
+
* Announce an expired session, whatever the transport (#1905 lot A).
|
|
362
|
+
*
|
|
363
|
+
* `auth:expired` used to be emitted only by the kernel's axios response
|
|
364
|
+
* interceptor. But `SdkStorage` exists so a consumer can bring their own
|
|
365
|
+
* transport, and one that did — a fetch-based SDK — never triggered expiry
|
|
366
|
+
* detection at all: the screen kept requesting with a dead token and stacked
|
|
367
|
+
* one error toast per attempt, indefinitely.
|
|
368
|
+
*
|
|
369
|
+
* Watching here rather than in a storage adapter is deliberate. Storages
|
|
370
|
+
* have no access to the signal bus, and every call passes through this
|
|
371
|
+
* manager whichever adapter is underneath — so this covers ApiStorage,
|
|
372
|
+
* MockApiStorage, SdkStorage and whatever someone writes next.
|
|
373
|
+
*
|
|
374
|
+
* 401 only, never 403: a 403 says the session is valid and this door is
|
|
375
|
+
* closed. Logging the user out would send them to sign in again for a
|
|
376
|
+
* permission they will not have afterwards — a loop, not an error.
|
|
377
|
+
*/
|
|
378
|
+
protected _watchStorageAuth(storage: IStorage<T> | null): IStorage<T> | null {
|
|
379
|
+
if (!storage || !this._signals) return storage
|
|
380
|
+
|
|
381
|
+
const emitExpired = (error: unknown): void => {
|
|
382
|
+
const status = (error as { status?: number })?.status
|
|
383
|
+
if (status !== 401) return
|
|
384
|
+
this._signals?.emit('auth:expired', { status, entity: this.name })
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
return new Proxy(storage as IStorage<T> & object, {
|
|
388
|
+
get(target, prop, receiver) {
|
|
389
|
+
const value = Reflect.get(target, prop, receiver)
|
|
390
|
+
if (typeof value !== 'function') return value
|
|
391
|
+
|
|
392
|
+
return function (this: unknown, ...args: unknown[]) {
|
|
393
|
+
try {
|
|
394
|
+
const result = (value as (...a: unknown[]) => unknown).apply(
|
|
395
|
+
this === receiver ? target : this,
|
|
396
|
+
args
|
|
397
|
+
)
|
|
398
|
+
// Storage methods are async; a rejection is the normal error path.
|
|
399
|
+
if (result instanceof Promise) {
|
|
400
|
+
return result.catch((error: unknown) => {
|
|
401
|
+
emitExpired(error)
|
|
402
|
+
throw error
|
|
403
|
+
})
|
|
404
|
+
}
|
|
405
|
+
return result
|
|
406
|
+
} catch (error) {
|
|
407
|
+
emitExpired(error)
|
|
408
|
+
throw error
|
|
409
|
+
}
|
|
410
|
+
}
|
|
411
|
+
},
|
|
412
|
+
})
|
|
413
|
+
}
|
|
414
|
+
|
|
360
415
|
/**
|
|
361
416
|
* Normalize resolveStorage() return value to standard format
|
|
362
417
|
*/
|
|
@@ -367,7 +422,7 @@ export class EntityManager<T extends EntityRecord = EntityRecord> {
|
|
|
367
422
|
// Function = dynamic endpoint builder
|
|
368
423
|
if (typeof result === 'function') {
|
|
369
424
|
return {
|
|
370
|
-
storage: this.storage,
|
|
425
|
+
storage: this._watchStorageAuth(this.storage),
|
|
371
426
|
endpoint: result(context),
|
|
372
427
|
isDynamic: true,
|
|
373
428
|
}
|
|
@@ -375,18 +430,18 @@ export class EntityManager<T extends EntityRecord = EntityRecord> {
|
|
|
375
430
|
|
|
376
431
|
// String = endpoint with primary storage
|
|
377
432
|
if (typeof result === 'string') {
|
|
378
|
-
return { storage: this.storage, endpoint: result }
|
|
433
|
+
return { storage: this._watchStorageAuth(this.storage), endpoint: result }
|
|
379
434
|
}
|
|
380
435
|
|
|
381
436
|
// Null/undefined = primary storage
|
|
382
437
|
if (!result) {
|
|
383
|
-
return { storage: this.storage }
|
|
438
|
+
return { storage: this._watchStorageAuth(this.storage) }
|
|
384
439
|
}
|
|
385
440
|
|
|
386
441
|
// Object with endpoint function = dynamic endpoint builder
|
|
387
442
|
if (typeof result.endpoint === 'function') {
|
|
388
443
|
return {
|
|
389
|
-
storage: result.storage || this.storage,
|
|
444
|
+
storage: this._watchStorageAuth(result.storage || this.storage),
|
|
390
445
|
endpoint: result.endpoint(context),
|
|
391
446
|
params: result.params,
|
|
392
447
|
isDynamic: true,
|
|
@@ -396,14 +451,14 @@ export class EntityManager<T extends EntityRecord = EntityRecord> {
|
|
|
396
451
|
// Object without storage = use primary storage
|
|
397
452
|
if (!result.storage) {
|
|
398
453
|
return {
|
|
399
|
-
storage: this.storage,
|
|
454
|
+
storage: this._watchStorageAuth(this.storage),
|
|
400
455
|
endpoint: result.endpoint as string | undefined,
|
|
401
456
|
params: result.params,
|
|
402
457
|
}
|
|
403
458
|
}
|
|
404
459
|
|
|
405
460
|
return {
|
|
406
|
-
storage: result.storage,
|
|
461
|
+
storage: this._watchStorageAuth(result.storage ?? this.storage),
|
|
407
462
|
endpoint: result.endpoint as string | undefined,
|
|
408
463
|
params: result.params,
|
|
409
464
|
}
|
package/src/kernel/Kernel.api.ts
CHANGED
|
@@ -46,7 +46,14 @@ export function applyApiMethods(KernelClass: { prototype: Kernel }): void {
|
|
|
46
46
|
error: axiosError,
|
|
47
47
|
})
|
|
48
48
|
|
|
49
|
-
|
|
49
|
+
// 401 only, never 403 (#1905 lot F).
|
|
50
|
+
//
|
|
51
|
+
// A 403 says the session is valid and this door is closed. Logging the
|
|
52
|
+
// user out sends them to sign in again for a permission they will not
|
|
53
|
+
// have any more afterwards — a loop, not an error. The 403 still
|
|
54
|
+
// travels as api:error above, which is where a permission refusal
|
|
55
|
+
// belongs.
|
|
56
|
+
if (status === 401) {
|
|
50
57
|
if (debug) {
|
|
51
58
|
console.warn(
|
|
52
59
|
`[Kernel] API ${status} error on ${url}, emitting auth:expired`
|
|
@@ -51,8 +51,34 @@ export function applyAuthMethods(KernelClass: { prototype: Kernel }): void {
|
|
|
51
51
|
const { authAdapter, onAuthExpired } = this.options
|
|
52
52
|
if (!authAdapter) return
|
|
53
53
|
|
|
54
|
+
// A new session re-arms the guard below. Registered before the handler so
|
|
55
|
+
// the ordering is obvious to whoever reads this next.
|
|
56
|
+
this.signals!.on('auth:login', () => {
|
|
57
|
+
this._expiredHandled = false
|
|
58
|
+
})
|
|
59
|
+
|
|
54
60
|
this.signals!.on('auth:expired', async (payload: { data: unknown }) => {
|
|
55
61
|
const debug = this.options.debug ?? false
|
|
62
|
+
|
|
63
|
+
// One expiry per session (#1905 lot E).
|
|
64
|
+
//
|
|
65
|
+
// The handler logs out, emits auth:logout — which _setupAuthInvalidation
|
|
66
|
+
// turns into a full invalidateApp() — and redirects. Without this guard
|
|
67
|
+
// a page firing four requests that all 401 remounts the app four times
|
|
68
|
+
// while navigating to the login screen.
|
|
69
|
+
//
|
|
70
|
+
// Guarded on OUR OWN state, deliberately: an earlier draft tested
|
|
71
|
+
// `authAdapter.isAuthenticated()`, which would have skipped the very
|
|
72
|
+
// first signal for any consumer that clears its auth state before
|
|
73
|
+
// emitting — turning this fix into the silent no-op it exists to remove.
|
|
74
|
+
if (this._expiredHandled) {
|
|
75
|
+
if (debug) {
|
|
76
|
+
console.debug('[Kernel] auth:expired already handled for this session, ignoring')
|
|
77
|
+
}
|
|
78
|
+
return
|
|
79
|
+
}
|
|
80
|
+
this._expiredHandled = true
|
|
81
|
+
|
|
56
82
|
if (debug) {
|
|
57
83
|
console.warn('[Kernel] auth:expired received:', payload)
|
|
58
84
|
}
|
|
@@ -34,6 +34,12 @@ export function applyRegistryMethods(KernelClass: { prototype: Kernel }): void {
|
|
|
34
34
|
* mounted on the same Vue app.
|
|
35
35
|
*/
|
|
36
36
|
proto._createSignalBus = function (this: Self): void {
|
|
37
|
+
// Idempotent (#1905 lot B): the constructor builds the bus so consumers can
|
|
38
|
+
// wire it immediately. Replacing it here would orphan every listener
|
|
39
|
+
// registered between construction and createApp() — silently, which is
|
|
40
|
+
// exactly the failure mode this lot exists to remove.
|
|
41
|
+
if (this.signals) return
|
|
42
|
+
|
|
37
43
|
if (this.options.existingSignals) {
|
|
38
44
|
this.signals = this.options.existingSignals
|
|
39
45
|
return
|
package/src/kernel/Kernel.ts
CHANGED
|
@@ -84,6 +84,15 @@ export class Kernel {
|
|
|
84
84
|
options: KernelOptions
|
|
85
85
|
vueApp: App | null = null
|
|
86
86
|
router: Router | null = null
|
|
87
|
+
/**
|
|
88
|
+
* Available from construction, not from createApp() (#1905 lot B).
|
|
89
|
+
*
|
|
90
|
+
* It used to be null until createApp(), so wiring `kernel.signals` right
|
|
91
|
+
* after `new Kernel()` — the natural place — silently did nothing:
|
|
92
|
+
* `null?.emit?.()` does not throw. The bus has no dependencies, so the
|
|
93
|
+
* window existed for no reason. Removing it beats warning about it
|
|
94
|
+
* (ADR 0011).
|
|
95
|
+
*/
|
|
87
96
|
signals: SignalBus | null = null
|
|
88
97
|
orchestrator: Orchestrator | null = null
|
|
89
98
|
zoneRegistry: ZoneRegistry | null = null
|
|
@@ -100,6 +109,8 @@ export class Kernel {
|
|
|
100
109
|
activeStack: ActiveStack | null = null
|
|
101
110
|
stackHydrator: StackHydrator | null = null
|
|
102
111
|
notificationStore: NotificationStore | null = null
|
|
112
|
+
/** One auth:expired per session — see Kernel.auth.ts (#1905). */
|
|
113
|
+
_expiredHandled = false
|
|
103
114
|
i18nInstance: I18n | null = null
|
|
104
115
|
|
|
105
116
|
/** Pending provides from modules (applied after vueApp creation) */
|
|
@@ -149,6 +160,12 @@ export class Kernel {
|
|
|
149
160
|
}
|
|
150
161
|
|
|
151
162
|
this.options = options
|
|
163
|
+
|
|
164
|
+
// The bus exists from here on (#1905 lot B). It has no dependencies, and
|
|
165
|
+
// the natural place to wire it is right after `new Kernel()` — so it must
|
|
166
|
+
// be usable there. _createSignalBus() is idempotent, so the later call in
|
|
167
|
+
// createApp() keeps this instance rather than orphaning its listeners.
|
|
168
|
+
this._createSignalBus()
|
|
152
169
|
}
|
|
153
170
|
|
|
154
171
|
/**
|