@web-ts-toolkit/oidc-vault-dpop-client 0.53.0 → 0.55.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 +91 -15
- package/index.d.mts +42 -3
- package/index.d.ts +42 -3
- package/index.js +62 -1
- package/index.mjs +62 -1
- package/llms.txt +14 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -179,17 +179,80 @@ fresh proof. Web Locks coordinate tabs of one frontend origin, storage
|
|
|
179
179
|
partition, and vault scope — they are not a backend refresh lease, so other
|
|
180
180
|
frontends, devices, or processes can still race upstream refresh families.
|
|
181
181
|
|
|
182
|
+
## Provider logout navigation
|
|
183
|
+
|
|
184
|
+
With server `logoutNavigation` enabled, call **`logoutAndNavigate(): Promise<void>`**
|
|
185
|
+
from a user action. It confirms local logout, clears old frontend auth and invokes
|
|
186
|
+
the synchronous `navigate(url)` adapter (default `location.assign`) inside lifecycle
|
|
187
|
+
authority. `logout()` remains local-only. No return-URL/prepare API is needed.
|
|
188
|
+
|
|
189
|
+
```ts
|
|
190
|
+
import {
|
|
191
|
+
createOidcVaultDpopSession,
|
|
192
|
+
type OidcVaultLogoutNavigationInput,
|
|
193
|
+
type OidcVaultLogoutNavigationResult,
|
|
194
|
+
} from '@web-ts-toolkit/oidc-vault-dpop-client';
|
|
195
|
+
|
|
196
|
+
const session = createOidcVaultDpopSession({
|
|
197
|
+
backendOrigin: 'https://api.example.com',
|
|
198
|
+
basePath: '/auth/oidc',
|
|
199
|
+
sessionTransport: 'body', // Or the matching cookie mount and 'cookie'.
|
|
200
|
+
});
|
|
201
|
+
export const signOutThroughProvider = (): Promise<void> => session.logoutAndNavigate();
|
|
202
|
+
// Pure wire types are also bundled at the client root; no backend install needed.
|
|
203
|
+
export type NavigationRequest = OidcVaultLogoutNavigationInput;
|
|
204
|
+
export type NavigationResponse = OidcVaultLogoutNavigationResult;
|
|
205
|
+
```
|
|
206
|
+
|
|
207
|
+
The existing persistent key plus body handle or **active matching cookie metadata**
|
|
208
|
+
suffices after reload, with an absent or expired memory JWT. It does not refresh,
|
|
209
|
+
create a replacement key, send fingerprint/Authorization/`ath`, or put API tokens
|
|
210
|
+
in query strings. POST uses the same finite HTTP bounds and nonce cache, including
|
|
211
|
+
learned success nonces: at most one recognized challenge retry with a fresh proof/JTI.
|
|
212
|
+
Body credentials are `omit` with `{ sessionId }`; cookie credentials are `include`
|
|
213
|
+
with `{}` under the scope Web Lock. Strict server Origin/Referer checks apply in
|
|
214
|
+
both transports; allow the frontend via `trustedOrigins` and expose `DPoP-Nonce` in CORS.
|
|
215
|
+
|
|
216
|
+
The client accepts only the exact absolute pinned backend continuation path,
|
|
217
|
+
bare or followed by one literal `?ticket=lnt_<43 canonical base64url characters>`.
|
|
218
|
+
No upstream URL/hint enters initiation JSON. A confirmed `loggedOut: true` clears
|
|
219
|
+
token, handle, pending login, nonces and peer adoption, and invalidates cookie
|
|
220
|
+
metadata/notifies peers even if the URL is invalid; that case rejects with fixed
|
|
221
|
+
nonterminal **`INVALID_LOGOUT_NAVIGATION_RESPONSE`** and never navigates. Unconfirmed,
|
|
222
|
+
malformed, oversized, timed-out or lost responses leave the server commit uncertain,
|
|
223
|
+
retain the existing failure policy and never automatically retry or navigate.
|
|
224
|
+
Cleanup/adapter failure rejects after memory clear, without credential restoration.
|
|
225
|
+
|
|
226
|
+
Concurrent clicks share the **same pending Promise** through FIFO, nonce retry,
|
|
227
|
+
cleanup and one adapter invocation. An earlier refresh publishes rotation first;
|
|
228
|
+
a later refresh cannot resurrect credentials. Refresh does not split that click
|
|
229
|
+
batch. Later login/exchange/local logout/clear/resetKey/dispose immediately withdraws
|
|
230
|
+
navigation intent, including a login still queued for the FIFO/lock. A started
|
|
231
|
+
POST may finish old-state cleanup before that login, but rejects as superseded
|
|
232
|
+
instead of navigating. No completed ticket URL is cached; another click after
|
|
233
|
+
cleanup requires login. Cancellation cannot undo server commit or navigation
|
|
234
|
+
already handed to the browser.
|
|
235
|
+
|
|
236
|
+
The server revokes locally before optional provider work. Missing endpoints,
|
|
237
|
+
discovery/ticket failures and missing/spent/expired tickets select its fixed
|
|
238
|
+
configured fallback. Tickets default to 60 seconds (server range 1–120 seconds),
|
|
239
|
+
are single-use, and retain only the final upstream hint URL server-side until
|
|
240
|
+
consume/expiry/cleanup. Navigation/history/proxy logs and the IdP request still
|
|
241
|
+
expose their respective URLs; avoid logging them. After upstream handoff, provider
|
|
242
|
+
availability/termination/return is not guaranteed. Stateless JWTs remain valid
|
|
243
|
+
until expiry unless the application adds revocation.
|
|
244
|
+
|
|
182
245
|
## Lifecycle concurrency and cleanup
|
|
183
246
|
|
|
184
247
|
Use one session instance per tab and vault mount. In **both transports**,
|
|
185
|
-
`login`, `exchange`, `refresh`, and `
|
|
248
|
+
`login`, `exchange`, `refresh`, `logout`, and `logoutAndNavigate` run in invocation order. A logout
|
|
186
249
|
queued behind refresh uses the rotated handle/cookie. Cookie operations also
|
|
187
250
|
hold the scope's Web Lock across their HTTP work and credential publication.
|
|
188
251
|
Body queues belong to the instance; they do not coordinate other instances or
|
|
189
252
|
copied tab handles.
|
|
190
253
|
|
|
191
254
|
Concurrent `refresh()` callers receive the **same Promise** until a call to
|
|
192
|
-
`login`, `exchange`, `logout`, `clear`, or `resetKey` starts a new batch. The first caller's
|
|
255
|
+
`login`, `exchange`, `logout`, `logoutAndNavigate`, `clear`, or `resetKey` starts a new batch. The first caller's
|
|
193
256
|
`rejectedToken` applies to that batch. A refresh invoked after a queued logout
|
|
194
257
|
waits behind it instead of joining the earlier refresh. Read-only `getKey()`
|
|
195
258
|
checks can run alongside the queue and one another; only their still-current
|
|
@@ -208,12 +271,13 @@ newer auth state is published. A new `login()` intent additionally supersedes
|
|
|
208
271
|
older peer callbacks immediately, including while that login is waiting in the
|
|
209
272
|
lifecycle queue or for the cookie Web Lock.
|
|
210
273
|
|
|
211
|
-
| Method
|
|
212
|
-
|
|
|
213
|
-
| `logout()`
|
|
214
|
-
| `
|
|
215
|
-
| `
|
|
216
|
-
| `
|
|
274
|
+
| Method | Effect |
|
|
275
|
+
| --------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
276
|
+
| `logout()` | Ordered server logout, then frontend cleanup. Revokes vault refresh state. |
|
|
277
|
+
| `logoutAndNavigate()` | Ordered proof-authorized server logout, confirmed cleanup, then provider/fallback navigation; coalesced clicks and withdrawable navigation intent. Requires server opt-in. |
|
|
278
|
+
| `clear()` | Immediately drops this context's memory token, vault nonces, body handle and pending-login marker, and aborts its in-flight vault HTTP. Keeps the persistent key. In cookie mode, its Promise waits for the Web Lock, inactive-generation write and peer notification. It does not call the backend or remove the HttpOnly cookie. |
|
|
279
|
+
| `resetKey()` | Explicit local key recovery. Immediately invalidates the same local work/auth as `clear`, then atomically deletes **this scope's key and cookie-session metadata**. Cookie mode uses the shared Web Lock and notifies affected peers. Repeating on absent records is harmless. Creates no key and makes no backend request; await it, then invoke `login()` explicitly. |
|
|
280
|
+
| `dispose()` | Immediately closes this context, drops its memory token/vault nonces, aborts owned vault HTTP, settles peer waits, removes listeners/timers and closes its channel. Keeps the previously persisted key, handle, pending-login marker and committed cookie metadata so a replacement context can bootstrap normally. Later calls reject. |
|
|
217
281
|
|
|
218
282
|
Clear/reset/dispose invalidate pending cookie-version and session-owned key
|
|
219
283
|
transactions as well as memory publication. After clear or reset, unsolicited peer messages cannot restore this
|
|
@@ -247,7 +311,7 @@ partition/scope.
|
|
|
247
311
|
|
|
248
312
|
## HTTP bounds and cancellation
|
|
249
313
|
|
|
250
|
-
All vault `login`/`exchange`/`refresh`/`logout` attempts use these session options:
|
|
314
|
+
All vault `login`/`exchange`/`refresh`/`logout`/`logoutAndNavigate` attempts use these session options:
|
|
251
315
|
|
|
252
316
|
| Option | Default | Valid values / scope |
|
|
253
317
|
| ------------------ | --------------------------- | ------------------------------------------------------------------------------------------------------------------------------------ |
|
|
@@ -343,6 +407,9 @@ Backend options that must agree with this client (authoritative wire contract:
|
|
|
343
407
|
- `deviceBinding.mode: 'required'` for bound flows, with a shared nonce secret
|
|
344
408
|
when nonces are enabled (`nonce: { secret, lifetimeSeconds }`, ≥32 random
|
|
345
409
|
bytes shared by instances of that protection space).
|
|
410
|
+
- `logoutNavigation: { fallbackRedirectUri }` enables provider navigation logout;
|
|
411
|
+
configure/register `postLogoutRedirectUri` for provider return. Built-in stores
|
|
412
|
+
supply ticket capabilities; custom stores need both optional ticket methods.
|
|
346
413
|
- The API's `replayNamespace` must equal the `replayNamespace` configured in
|
|
347
414
|
`fetchWithDpop` `apis` entries.
|
|
348
415
|
|
|
@@ -576,11 +643,12 @@ error or automatically start login for a non-terminal failure.
|
|
|
576
643
|
|
|
577
644
|
`credentials` is owned by the helpers; callers must not set it.
|
|
578
645
|
|
|
579
|
-
| Operation
|
|
580
|
-
|
|
|
581
|
-
| `login` / `exchange`
|
|
582
|
-
| `refresh` / `logout`
|
|
583
|
-
| `
|
|
646
|
+
| Operation | Body transport | Cookie transport |
|
|
647
|
+
| ------------------------ | ---------------------------------------- | ------------------------------------------------------------------ |
|
|
648
|
+
| `login` / `exchange` | `include` (temporary transaction cookie) | `include` (temporary transaction cookie) |
|
|
649
|
+
| `refresh` / `logout` | `omit` + `{ sessionId }` JSON body | `include` + `{}` JSON body (no `sessionId`) |
|
|
650
|
+
| `logoutAndNavigate` POST | `omit` + `{ sessionId }` JSON body | `include` + `{}` JSON body (no `sessionId`) |
|
|
651
|
+
| `fetchWithDpop` API | `omit` default | `omit` default (`include` only for an explicitly cookie-using API) |
|
|
584
652
|
|
|
585
653
|
Cookie JSON always omits `sessionId`. Every request uses `redirect: 'error'`
|
|
586
654
|
and `cache: 'no-store'`. Vault POSTs present no access token; API calls send
|
|
@@ -768,7 +836,7 @@ singleton, and no client router — the callback is the same page reading
|
|
|
768
836
|
## API reference
|
|
769
837
|
|
|
770
838
|
- `createOidcVaultDpopSession(options)` → `OidcVaultDpopSession`
|
|
771
|
-
(`login`/`exchange`/`refresh`/`logout`/`getAccessToken`/`getKey`/`clear`/`resetKey`/`dispose`).
|
|
839
|
+
(`login`/`exchange`/`refresh`/`logout`/`logoutAndNavigate`/`getAccessToken`/`getKey`/`clear`/`resetKey`/`dispose`).
|
|
772
840
|
Options: `backendOrigin`, `basePath` (default `/auth/oidc`),
|
|
773
841
|
`sessionTransport` (`'body'` default | `'cookie'`), optional `fingerprint`,
|
|
774
842
|
`requestTimeoutMs` (default 15,000 ms), `maxResponseBytes` (default 1 MiB),
|
|
@@ -785,9 +853,17 @@ pathPrefix?, credentials? }`; longest normalized segment-prefix match wins.
|
|
|
785
853
|
`DeviceFingerprintSignalSource`, `FingerprintJsAgent`. Wire DTOs stay
|
|
786
854
|
single-sourced from the pure `@web-ts-toolkit/express-oidc-vault/wire` entry
|
|
787
855
|
at build time; no backend import survives in client declarations/JavaScript.
|
|
856
|
+
- Pure root DTOs: `OidcVaultLogoutNavigationInput`, `OidcVaultLogoutNavigationResult`.
|
|
788
857
|
|
|
789
858
|
## Upgrade notes
|
|
790
859
|
|
|
860
|
+
Provider logout is additive: enable the paired server's `logoutNavigation` and
|
|
861
|
+
replace custom logout proof/fetch/redirect handling with `session.logoutAndNavigate()`.
|
|
862
|
+
Keep `logout()` for local-only behavior. Add `logoutAndNavigate` to typed session
|
|
863
|
+
mocks. `navigate` now handles both login and logout URLs synchronously; do not
|
|
864
|
+
defer its actual navigation beyond the call, where lifecycle authority no longer
|
|
865
|
+
protects it. Existing persistent keys and the database schema are retained.
|
|
866
|
+
|
|
791
867
|
| Area | Consumer action / delivered contract |
|
|
792
868
|
| ---------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
|
|
793
869
|
| Node and types | Use **Node >=22.12.0** for CJS `require(esm)`. Browser declarations now need only DOM/ES libs; remove compensating Express/Node ambient types if your app has no other use for them. Backend root type imports and `req.auth` remain available. |
|
package/index.d.mts
CHANGED
|
@@ -85,6 +85,37 @@ interface OidcVaultUserProfile {
|
|
|
85
85
|
preferredUsername?: string;
|
|
86
86
|
[key: string]: unknown;
|
|
87
87
|
}
|
|
88
|
+
/** Successful local logout; stateless application JWTs require separate revocation or expiry. */
|
|
89
|
+
interface OidcVaultLogoutResult {
|
|
90
|
+
/**
|
|
91
|
+
* Successful logout commits revocation before upstream work and clears the
|
|
92
|
+
* session cookie under cookie transport. Built-in live handles and unexpired
|
|
93
|
+
* aliases authenticate the surviving lineage's provider/original binding;
|
|
94
|
+
* wrong identity, missing/wrong/stale/replayed proof, or disabled acceptance
|
|
95
|
+
* of a bound lineage fails without deleting or clearing. No currently live
|
|
96
|
+
* target is idempotent success without deletion or proof reservation. Aliases
|
|
97
|
+
* grant revocation only, not refresh authentication or upstream redirects.
|
|
98
|
+
* Old custom bearer stores without revocation context retain legacy alias
|
|
99
|
+
* deletion behavior. Stateless application access tokens are not invalidated.
|
|
100
|
+
* Local-only logout (`redirect` unset or `false`) never contacts the provider.
|
|
101
|
+
* Redirected logout (`redirect: true`)
|
|
102
|
+
* treats the upstream end-session redirect as best-effort: when provider
|
|
103
|
+
* discovery fails or no `endSessionEndpoint` is available, the route still
|
|
104
|
+
* returns this local success. Discovery errors reach `onError` without
|
|
105
|
+
* undoing revocation or skipping the live session's `onLogout` notification.
|
|
106
|
+
*/
|
|
107
|
+
loggedOut: true;
|
|
108
|
+
}
|
|
109
|
+
/** JSON-only POST <basePath>/logout/navigation. Proof belongs in DPoP, with no access token or ath. */
|
|
110
|
+
interface OidcVaultLogoutNavigationInput {
|
|
111
|
+
/** Required nonempty handle in body transport; ignored in cookie transport, which selects only the configured cookie. */
|
|
112
|
+
sessionId?: string;
|
|
113
|
+
}
|
|
114
|
+
/** Confirmed local logout, not an IdP delivery receipt. Never contains an upstream token-bearing URL. */
|
|
115
|
+
interface OidcVaultLogoutNavigationResult extends OidcVaultLogoutResult {
|
|
116
|
+
/** Absolute pinned backend continuation URL, bare or with one opaque one-use ticket. Navigate at top level; do not fetch the provider redirect. */
|
|
117
|
+
navigationUrl: string;
|
|
118
|
+
}
|
|
88
119
|
/** Vault handle transport; independent of DPoP mode. Default: body. */
|
|
89
120
|
type OidcVaultSessionTransport = 'body' | 'cookie';
|
|
90
121
|
|
|
@@ -157,8 +188,8 @@ interface OidcVaultDpopSessionOptions {
|
|
|
157
188
|
maxResponseBytes?: number;
|
|
158
189
|
/** Default global fetch. Injected responses must expose a standard streaming body. */
|
|
159
190
|
fetch?: typeof globalThis.fetch;
|
|
160
|
-
/**
|
|
161
|
-
navigate?: (
|
|
191
|
+
/** Synchronous login/logout navigation inside lifecycle authority; default location.assign. */
|
|
192
|
+
navigate?: (url: string) => void;
|
|
162
193
|
/** Epoch-millisecond clock; default Date.now. Credential deadline arithmetic must remain safe. */
|
|
163
194
|
now?: () => number;
|
|
164
195
|
}
|
|
@@ -181,6 +212,14 @@ interface OidcVaultDpopSession {
|
|
|
181
212
|
}): Promise<DpopAccessToken>;
|
|
182
213
|
/** FIFO: an earlier refresh publishes its rotated handle before logout uses it. Revokes server refresh state. */
|
|
183
214
|
logout(): Promise<void>;
|
|
215
|
+
/**
|
|
216
|
+
* Proof-authorized local logout followed by top-level provider/fallback navigation.
|
|
217
|
+
* Requires server logoutNavigation opt-in and the existing key/handle or active
|
|
218
|
+
* cookie metadata, not a live access JWT. Concurrent clicks share one Promise.
|
|
219
|
+
* Later login/exchange/logout/clear/resetKey/dispose withdraw navigation intent.
|
|
220
|
+
* Invalid navigation URLs still clear confirmed logout state, then reject.
|
|
221
|
+
*/
|
|
222
|
+
logoutAndNavigate(): Promise<void>;
|
|
184
223
|
/** Memory snapshot only; may already be expired, including a valid expiresIn: 0 response. */
|
|
185
224
|
getAccessToken(): DpopAccessToken | undefined;
|
|
186
225
|
/**
|
|
@@ -367,4 +406,4 @@ declare class OidcVaultDpopClientError extends Error {
|
|
|
367
406
|
constructor(code: string, message: string, requiresLogin?: boolean, status?: number);
|
|
368
407
|
}
|
|
369
408
|
|
|
370
|
-
export { type DeviceFingerprint, type DeviceFingerprintOptions, type DeviceFingerprintSignalSource, type DpopAccessToken, type DpopApi, type DpopFetchContext, type DpopFetchOptions, type DpopKey, type DpopKeyScope, DpopNonceCache, type DpopProofInput, type FingerprintJsAgent, OidcVaultDpopClientError, type OidcVaultDpopSession, type OidcVaultDpopSessionOptions, createDeviceFingerprint, createDpopProof, createOidcVaultDpopSession, fetchWithDpop, fingerprintJsSignalSource, getOrCreateDpopKey };
|
|
409
|
+
export { type DeviceFingerprint, type DeviceFingerprintOptions, type DeviceFingerprintSignalSource, type DpopAccessToken, type DpopApi, type DpopFetchContext, type DpopFetchOptions, type DpopKey, type DpopKeyScope, DpopNonceCache, type DpopProofInput, type FingerprintJsAgent, OidcVaultDpopClientError, type OidcVaultDpopSession, type OidcVaultDpopSessionOptions, type OidcVaultLogoutNavigationInput, type OidcVaultLogoutNavigationResult, createDeviceFingerprint, createDpopProof, createOidcVaultDpopSession, fetchWithDpop, fingerprintJsSignalSource, getOrCreateDpopKey };
|
package/index.d.ts
CHANGED
|
@@ -85,6 +85,37 @@ interface OidcVaultUserProfile {
|
|
|
85
85
|
preferredUsername?: string;
|
|
86
86
|
[key: string]: unknown;
|
|
87
87
|
}
|
|
88
|
+
/** Successful local logout; stateless application JWTs require separate revocation or expiry. */
|
|
89
|
+
interface OidcVaultLogoutResult {
|
|
90
|
+
/**
|
|
91
|
+
* Successful logout commits revocation before upstream work and clears the
|
|
92
|
+
* session cookie under cookie transport. Built-in live handles and unexpired
|
|
93
|
+
* aliases authenticate the surviving lineage's provider/original binding;
|
|
94
|
+
* wrong identity, missing/wrong/stale/replayed proof, or disabled acceptance
|
|
95
|
+
* of a bound lineage fails without deleting or clearing. No currently live
|
|
96
|
+
* target is idempotent success without deletion or proof reservation. Aliases
|
|
97
|
+
* grant revocation only, not refresh authentication or upstream redirects.
|
|
98
|
+
* Old custom bearer stores without revocation context retain legacy alias
|
|
99
|
+
* deletion behavior. Stateless application access tokens are not invalidated.
|
|
100
|
+
* Local-only logout (`redirect` unset or `false`) never contacts the provider.
|
|
101
|
+
* Redirected logout (`redirect: true`)
|
|
102
|
+
* treats the upstream end-session redirect as best-effort: when provider
|
|
103
|
+
* discovery fails or no `endSessionEndpoint` is available, the route still
|
|
104
|
+
* returns this local success. Discovery errors reach `onError` without
|
|
105
|
+
* undoing revocation or skipping the live session's `onLogout` notification.
|
|
106
|
+
*/
|
|
107
|
+
loggedOut: true;
|
|
108
|
+
}
|
|
109
|
+
/** JSON-only POST <basePath>/logout/navigation. Proof belongs in DPoP, with no access token or ath. */
|
|
110
|
+
interface OidcVaultLogoutNavigationInput {
|
|
111
|
+
/** Required nonempty handle in body transport; ignored in cookie transport, which selects only the configured cookie. */
|
|
112
|
+
sessionId?: string;
|
|
113
|
+
}
|
|
114
|
+
/** Confirmed local logout, not an IdP delivery receipt. Never contains an upstream token-bearing URL. */
|
|
115
|
+
interface OidcVaultLogoutNavigationResult extends OidcVaultLogoutResult {
|
|
116
|
+
/** Absolute pinned backend continuation URL, bare or with one opaque one-use ticket. Navigate at top level; do not fetch the provider redirect. */
|
|
117
|
+
navigationUrl: string;
|
|
118
|
+
}
|
|
88
119
|
/** Vault handle transport; independent of DPoP mode. Default: body. */
|
|
89
120
|
type OidcVaultSessionTransport = 'body' | 'cookie';
|
|
90
121
|
|
|
@@ -157,8 +188,8 @@ interface OidcVaultDpopSessionOptions {
|
|
|
157
188
|
maxResponseBytes?: number;
|
|
158
189
|
/** Default global fetch. Injected responses must expose a standard streaming body. */
|
|
159
190
|
fetch?: typeof globalThis.fetch;
|
|
160
|
-
/**
|
|
161
|
-
navigate?: (
|
|
191
|
+
/** Synchronous login/logout navigation inside lifecycle authority; default location.assign. */
|
|
192
|
+
navigate?: (url: string) => void;
|
|
162
193
|
/** Epoch-millisecond clock; default Date.now. Credential deadline arithmetic must remain safe. */
|
|
163
194
|
now?: () => number;
|
|
164
195
|
}
|
|
@@ -181,6 +212,14 @@ interface OidcVaultDpopSession {
|
|
|
181
212
|
}): Promise<DpopAccessToken>;
|
|
182
213
|
/** FIFO: an earlier refresh publishes its rotated handle before logout uses it. Revokes server refresh state. */
|
|
183
214
|
logout(): Promise<void>;
|
|
215
|
+
/**
|
|
216
|
+
* Proof-authorized local logout followed by top-level provider/fallback navigation.
|
|
217
|
+
* Requires server logoutNavigation opt-in and the existing key/handle or active
|
|
218
|
+
* cookie metadata, not a live access JWT. Concurrent clicks share one Promise.
|
|
219
|
+
* Later login/exchange/logout/clear/resetKey/dispose withdraw navigation intent.
|
|
220
|
+
* Invalid navigation URLs still clear confirmed logout state, then reject.
|
|
221
|
+
*/
|
|
222
|
+
logoutAndNavigate(): Promise<void>;
|
|
184
223
|
/** Memory snapshot only; may already be expired, including a valid expiresIn: 0 response. */
|
|
185
224
|
getAccessToken(): DpopAccessToken | undefined;
|
|
186
225
|
/**
|
|
@@ -367,4 +406,4 @@ declare class OidcVaultDpopClientError extends Error {
|
|
|
367
406
|
constructor(code: string, message: string, requiresLogin?: boolean, status?: number);
|
|
368
407
|
}
|
|
369
408
|
|
|
370
|
-
export { type DeviceFingerprint, type DeviceFingerprintOptions, type DeviceFingerprintSignalSource, type DpopAccessToken, type DpopApi, type DpopFetchContext, type DpopFetchOptions, type DpopKey, type DpopKeyScope, DpopNonceCache, type DpopProofInput, type FingerprintJsAgent, OidcVaultDpopClientError, type OidcVaultDpopSession, type OidcVaultDpopSessionOptions, createDeviceFingerprint, createDpopProof, createOidcVaultDpopSession, fetchWithDpop, fingerprintJsSignalSource, getOrCreateDpopKey };
|
|
409
|
+
export { type DeviceFingerprint, type DeviceFingerprintOptions, type DeviceFingerprintSignalSource, type DpopAccessToken, type DpopApi, type DpopFetchContext, type DpopFetchOptions, type DpopKey, type DpopKeyScope, DpopNonceCache, type DpopProofInput, type FingerprintJsAgent, OidcVaultDpopClientError, type OidcVaultDpopSession, type OidcVaultDpopSessionOptions, type OidcVaultLogoutNavigationInput, type OidcVaultLogoutNavigationResult, createDeviceFingerprint, createDpopProof, createOidcVaultDpopSession, fetchWithDpop, fingerprintJsSignalSource, getOrCreateDpopKey };
|
package/index.js
CHANGED
|
@@ -690,11 +690,17 @@ var createOidcVaultDpopSession = (options) => {
|
|
|
690
690
|
const cookieBindings = /* @__PURE__ */ new WeakMap();
|
|
691
691
|
let refreshPromise;
|
|
692
692
|
let resetPromise;
|
|
693
|
+
let navigationPromise;
|
|
694
|
+
let navigationIntent = 0;
|
|
693
695
|
const lifecycle = createSessionLifecycle();
|
|
694
696
|
let publication = 0;
|
|
695
697
|
let loginIntent = 0;
|
|
696
698
|
let acceptPeerTokens = true;
|
|
697
699
|
const peerWaiters = /* @__PURE__ */ new Set();
|
|
700
|
+
const withdrawNavigation = () => {
|
|
701
|
+
navigationIntent++;
|
|
702
|
+
navigationPromise = void 0;
|
|
703
|
+
};
|
|
698
704
|
const readStorage = (key) => {
|
|
699
705
|
try {
|
|
700
706
|
const value = sessionStorage.getItem(key);
|
|
@@ -732,6 +738,7 @@ var createOidcVaultDpopSession = (options) => {
|
|
|
732
738
|
};
|
|
733
739
|
const invalidateLocal = (clearPendingLogin = true) => {
|
|
734
740
|
const authority = lifecycle.invalidate();
|
|
741
|
+
withdrawNavigation();
|
|
735
742
|
refreshPromise = void 0;
|
|
736
743
|
resetPromise = void 0;
|
|
737
744
|
clearLocal(clearPendingLogin);
|
|
@@ -811,7 +818,7 @@ var createOidcVaultDpopSession = (options) => {
|
|
|
811
818
|
let nonceRetried = false;
|
|
812
819
|
for (; ; ) {
|
|
813
820
|
authority.assertCurrent();
|
|
814
|
-
const recognition = route === "logout" ? {} : await fingerprint?.headers() ?? {};
|
|
821
|
+
const recognition = route === "logout" || route === "logout/navigation" ? {} : await fingerprint?.headers() ?? {};
|
|
815
822
|
const currentKey = await existingKey(authority, jkt);
|
|
816
823
|
authority.assertCurrent();
|
|
817
824
|
const proof = await createDpopProof(currentKey, {
|
|
@@ -1055,6 +1062,7 @@ var createOidcVaultDpopSession = (options) => {
|
|
|
1055
1062
|
sessionTransport,
|
|
1056
1063
|
async login(returnTo) {
|
|
1057
1064
|
const authority = lifecycle.capture();
|
|
1065
|
+
withdrawNavigation();
|
|
1058
1066
|
loginIntent++;
|
|
1059
1067
|
refreshPromise = void 0;
|
|
1060
1068
|
resetPromise = void 0;
|
|
@@ -1100,6 +1108,7 @@ var createOidcVaultDpopSession = (options) => {
|
|
|
1100
1108
|
},
|
|
1101
1109
|
async exchange(code) {
|
|
1102
1110
|
const authority = lifecycle.capture();
|
|
1111
|
+
withdrawNavigation();
|
|
1103
1112
|
refreshPromise = void 0;
|
|
1104
1113
|
resetPromise = void 0;
|
|
1105
1114
|
return operation(authority, async () => {
|
|
@@ -1166,6 +1175,7 @@ var createOidcVaultDpopSession = (options) => {
|
|
|
1166
1175
|
},
|
|
1167
1176
|
async logout() {
|
|
1168
1177
|
const authority = lifecycle.capture();
|
|
1178
|
+
withdrawNavigation();
|
|
1169
1179
|
refreshPromise = void 0;
|
|
1170
1180
|
resetPromise = void 0;
|
|
1171
1181
|
await operation(authority, async () => {
|
|
@@ -1182,6 +1192,56 @@ var createOidcVaultDpopSession = (options) => {
|
|
|
1182
1192
|
await invalidateCookie(authority, key.jkt);
|
|
1183
1193
|
});
|
|
1184
1194
|
},
|
|
1195
|
+
logoutAndNavigate() {
|
|
1196
|
+
const authority = lifecycle.capture();
|
|
1197
|
+
if (navigationPromise) return navigationPromise;
|
|
1198
|
+
const intent = navigationIntent;
|
|
1199
|
+
const navigation = lifecycle.capture(() => navigationIntent === intent);
|
|
1200
|
+
refreshPromise = void 0;
|
|
1201
|
+
resetPromise = void 0;
|
|
1202
|
+
const pending = operation(authority, async () => {
|
|
1203
|
+
navigation.assertCurrent();
|
|
1204
|
+
const key = await authenticatedKey(authority);
|
|
1205
|
+
authority.assertCurrent();
|
|
1206
|
+
if (channel) {
|
|
1207
|
+
const version = await readCookieSessionVersion(scope.id);
|
|
1208
|
+
authority.assertCurrent();
|
|
1209
|
+
if (!version?.active || version.jkt !== key.jkt) throw loginRequired();
|
|
1210
|
+
cookieBindings.set(authority, { ...version });
|
|
1211
|
+
}
|
|
1212
|
+
const handle = sessionTransport === "body" ? readStorage(storageKey) : void 0;
|
|
1213
|
+
if (sessionTransport === "body" && (!handle?.sessionId || handle.jkt !== key.jkt)) throw loginRequired();
|
|
1214
|
+
const { value } = await post(
|
|
1215
|
+
authority,
|
|
1216
|
+
"logout/navigation",
|
|
1217
|
+
handle ? { sessionId: handle.sessionId } : {},
|
|
1218
|
+
key
|
|
1219
|
+
);
|
|
1220
|
+
authority.assertCurrent();
|
|
1221
|
+
const invalidResponse = () => new OidcVaultDpopClientError(
|
|
1222
|
+
"INVALID_LOGOUT_NAVIGATION_RESPONSE",
|
|
1223
|
+
"The logout navigation response is invalid."
|
|
1224
|
+
);
|
|
1225
|
+
if (typeof value !== "object" || value === null || !("loggedOut" in value) || value.loggedOut !== true)
|
|
1226
|
+
throw invalidResponse();
|
|
1227
|
+
clearLocal();
|
|
1228
|
+
await invalidateCookie(authority, key.jkt);
|
|
1229
|
+
authority.assertCurrent();
|
|
1230
|
+
navigation.assertCurrent();
|
|
1231
|
+
const bare = `${scope.backendOrigin}${scope.basePath === "/" ? "" : scope.basePath}/logout/navigation`;
|
|
1232
|
+
const url = "navigationUrl" in value ? value.navigationUrl : void 0;
|
|
1233
|
+
if (typeof url !== "string" || url !== bare && (url.length !== bare.length + "?ticket=".length + 47 || !url.startsWith(`${bare}?ticket=`) || !/^lnt_[A-Za-z0-9_-]{42}[AEIMQUYcgkosw048]$/.test(url.slice(`${bare}?ticket=`.length))))
|
|
1234
|
+
throw invalidResponse();
|
|
1235
|
+
navigation.assertCurrent();
|
|
1236
|
+
navigate(url);
|
|
1237
|
+
});
|
|
1238
|
+
navigationPromise = pending;
|
|
1239
|
+
void pending.finally(() => {
|
|
1240
|
+
if (navigationPromise === pending) navigationPromise = void 0;
|
|
1241
|
+
}).catch(() => {
|
|
1242
|
+
});
|
|
1243
|
+
return pending;
|
|
1244
|
+
},
|
|
1185
1245
|
getAccessToken: () => token,
|
|
1186
1246
|
async getKey() {
|
|
1187
1247
|
const authority = stateAuthority();
|
|
@@ -1236,6 +1296,7 @@ var createOidcVaultDpopSession = (options) => {
|
|
|
1236
1296
|
},
|
|
1237
1297
|
dispose() {
|
|
1238
1298
|
lifecycle.dispose();
|
|
1299
|
+
withdrawNavigation();
|
|
1239
1300
|
publication++;
|
|
1240
1301
|
refreshPromise = void 0;
|
|
1241
1302
|
resetPromise = void 0;
|
package/index.mjs
CHANGED
|
@@ -658,11 +658,17 @@ var createOidcVaultDpopSession = (options) => {
|
|
|
658
658
|
const cookieBindings = /* @__PURE__ */ new WeakMap();
|
|
659
659
|
let refreshPromise;
|
|
660
660
|
let resetPromise;
|
|
661
|
+
let navigationPromise;
|
|
662
|
+
let navigationIntent = 0;
|
|
661
663
|
const lifecycle = createSessionLifecycle();
|
|
662
664
|
let publication = 0;
|
|
663
665
|
let loginIntent = 0;
|
|
664
666
|
let acceptPeerTokens = true;
|
|
665
667
|
const peerWaiters = /* @__PURE__ */ new Set();
|
|
668
|
+
const withdrawNavigation = () => {
|
|
669
|
+
navigationIntent++;
|
|
670
|
+
navigationPromise = void 0;
|
|
671
|
+
};
|
|
666
672
|
const readStorage = (key) => {
|
|
667
673
|
try {
|
|
668
674
|
const value = sessionStorage.getItem(key);
|
|
@@ -700,6 +706,7 @@ var createOidcVaultDpopSession = (options) => {
|
|
|
700
706
|
};
|
|
701
707
|
const invalidateLocal = (clearPendingLogin = true) => {
|
|
702
708
|
const authority = lifecycle.invalidate();
|
|
709
|
+
withdrawNavigation();
|
|
703
710
|
refreshPromise = void 0;
|
|
704
711
|
resetPromise = void 0;
|
|
705
712
|
clearLocal(clearPendingLogin);
|
|
@@ -779,7 +786,7 @@ var createOidcVaultDpopSession = (options) => {
|
|
|
779
786
|
let nonceRetried = false;
|
|
780
787
|
for (; ; ) {
|
|
781
788
|
authority.assertCurrent();
|
|
782
|
-
const recognition = route === "logout" ? {} : await fingerprint?.headers() ?? {};
|
|
789
|
+
const recognition = route === "logout" || route === "logout/navigation" ? {} : await fingerprint?.headers() ?? {};
|
|
783
790
|
const currentKey = await existingKey(authority, jkt);
|
|
784
791
|
authority.assertCurrent();
|
|
785
792
|
const proof = await createDpopProof(currentKey, {
|
|
@@ -1023,6 +1030,7 @@ var createOidcVaultDpopSession = (options) => {
|
|
|
1023
1030
|
sessionTransport,
|
|
1024
1031
|
async login(returnTo) {
|
|
1025
1032
|
const authority = lifecycle.capture();
|
|
1033
|
+
withdrawNavigation();
|
|
1026
1034
|
loginIntent++;
|
|
1027
1035
|
refreshPromise = void 0;
|
|
1028
1036
|
resetPromise = void 0;
|
|
@@ -1068,6 +1076,7 @@ var createOidcVaultDpopSession = (options) => {
|
|
|
1068
1076
|
},
|
|
1069
1077
|
async exchange(code) {
|
|
1070
1078
|
const authority = lifecycle.capture();
|
|
1079
|
+
withdrawNavigation();
|
|
1071
1080
|
refreshPromise = void 0;
|
|
1072
1081
|
resetPromise = void 0;
|
|
1073
1082
|
return operation(authority, async () => {
|
|
@@ -1134,6 +1143,7 @@ var createOidcVaultDpopSession = (options) => {
|
|
|
1134
1143
|
},
|
|
1135
1144
|
async logout() {
|
|
1136
1145
|
const authority = lifecycle.capture();
|
|
1146
|
+
withdrawNavigation();
|
|
1137
1147
|
refreshPromise = void 0;
|
|
1138
1148
|
resetPromise = void 0;
|
|
1139
1149
|
await operation(authority, async () => {
|
|
@@ -1150,6 +1160,56 @@ var createOidcVaultDpopSession = (options) => {
|
|
|
1150
1160
|
await invalidateCookie(authority, key.jkt);
|
|
1151
1161
|
});
|
|
1152
1162
|
},
|
|
1163
|
+
logoutAndNavigate() {
|
|
1164
|
+
const authority = lifecycle.capture();
|
|
1165
|
+
if (navigationPromise) return navigationPromise;
|
|
1166
|
+
const intent = navigationIntent;
|
|
1167
|
+
const navigation = lifecycle.capture(() => navigationIntent === intent);
|
|
1168
|
+
refreshPromise = void 0;
|
|
1169
|
+
resetPromise = void 0;
|
|
1170
|
+
const pending = operation(authority, async () => {
|
|
1171
|
+
navigation.assertCurrent();
|
|
1172
|
+
const key = await authenticatedKey(authority);
|
|
1173
|
+
authority.assertCurrent();
|
|
1174
|
+
if (channel) {
|
|
1175
|
+
const version = await readCookieSessionVersion(scope.id);
|
|
1176
|
+
authority.assertCurrent();
|
|
1177
|
+
if (!version?.active || version.jkt !== key.jkt) throw loginRequired();
|
|
1178
|
+
cookieBindings.set(authority, { ...version });
|
|
1179
|
+
}
|
|
1180
|
+
const handle = sessionTransport === "body" ? readStorage(storageKey) : void 0;
|
|
1181
|
+
if (sessionTransport === "body" && (!handle?.sessionId || handle.jkt !== key.jkt)) throw loginRequired();
|
|
1182
|
+
const { value } = await post(
|
|
1183
|
+
authority,
|
|
1184
|
+
"logout/navigation",
|
|
1185
|
+
handle ? { sessionId: handle.sessionId } : {},
|
|
1186
|
+
key
|
|
1187
|
+
);
|
|
1188
|
+
authority.assertCurrent();
|
|
1189
|
+
const invalidResponse = () => new OidcVaultDpopClientError(
|
|
1190
|
+
"INVALID_LOGOUT_NAVIGATION_RESPONSE",
|
|
1191
|
+
"The logout navigation response is invalid."
|
|
1192
|
+
);
|
|
1193
|
+
if (typeof value !== "object" || value === null || !("loggedOut" in value) || value.loggedOut !== true)
|
|
1194
|
+
throw invalidResponse();
|
|
1195
|
+
clearLocal();
|
|
1196
|
+
await invalidateCookie(authority, key.jkt);
|
|
1197
|
+
authority.assertCurrent();
|
|
1198
|
+
navigation.assertCurrent();
|
|
1199
|
+
const bare = `${scope.backendOrigin}${scope.basePath === "/" ? "" : scope.basePath}/logout/navigation`;
|
|
1200
|
+
const url = "navigationUrl" in value ? value.navigationUrl : void 0;
|
|
1201
|
+
if (typeof url !== "string" || url !== bare && (url.length !== bare.length + "?ticket=".length + 47 || !url.startsWith(`${bare}?ticket=`) || !/^lnt_[A-Za-z0-9_-]{42}[AEIMQUYcgkosw048]$/.test(url.slice(`${bare}?ticket=`.length))))
|
|
1202
|
+
throw invalidResponse();
|
|
1203
|
+
navigation.assertCurrent();
|
|
1204
|
+
navigate(url);
|
|
1205
|
+
});
|
|
1206
|
+
navigationPromise = pending;
|
|
1207
|
+
void pending.finally(() => {
|
|
1208
|
+
if (navigationPromise === pending) navigationPromise = void 0;
|
|
1209
|
+
}).catch(() => {
|
|
1210
|
+
});
|
|
1211
|
+
return pending;
|
|
1212
|
+
},
|
|
1153
1213
|
getAccessToken: () => token,
|
|
1154
1214
|
async getKey() {
|
|
1155
1215
|
const authority = stateAuthority();
|
|
@@ -1204,6 +1264,7 @@ var createOidcVaultDpopSession = (options) => {
|
|
|
1204
1264
|
},
|
|
1205
1265
|
dispose() {
|
|
1206
1266
|
lifecycle.dispose();
|
|
1267
|
+
withdrawNavigation();
|
|
1207
1268
|
publication++;
|
|
1208
1269
|
refreshPromise = void 0;
|
|
1209
1270
|
resetPromise = void 0;
|
package/llms.txt
CHANGED
|
@@ -14,6 +14,7 @@ createDeviceFingerprint, fingerprintJsSignalSource, OidcVaultDpopClientError.
|
|
|
14
14
|
Full root type list and member contracts: README API reference + index.d.mts.
|
|
15
15
|
Both declaration conditions are DOM-only (types:[], skipLibCheck:false).
|
|
16
16
|
Wire DTOs come from express-oidc-vault/wire at build time and are bundled here.
|
|
17
|
+
Root DTOs: OidcVaultLogoutNavigationInput, OidcVaultLogoutNavigationResult (DOM-only).
|
|
17
18
|
|
|
18
19
|
Session: createOidcVaultDpopSession({backendOrigin,basePath,sessionTransport})
|
|
19
20
|
defaults to /auth/oidc + body. login -> exchange(code) -> API/refresh -> logout.
|
|
@@ -36,6 +37,19 @@ server revocation; reset leaves old sessions/JWTs until revocation/expiry.
|
|
|
36
37
|
DPOP_OPERATION_SUPERSEDED / DPOP_SESSION_DISPOSED are non-terminal; stale work
|
|
37
38
|
cannot publish/navigate/clear newer auth. See README's cleanup/recovery tables.
|
|
38
39
|
|
|
40
|
+
Provider logout: server logoutNavigation opt-in + session.logoutAndNavigate():Promise<void>.
|
|
41
|
+
Existing key/handle or active matching cookie metadata; absent/expired JWT works,
|
|
42
|
+
no refresh/fingerprint/Authorization/ath. Same HTTP bounds/nonce cache, one fresh-proof retry.
|
|
43
|
+
Exact pinned backend bare or canonical ticket URL only; no API token in GET/IdP URL in JSON.
|
|
44
|
+
Confirmed loggedOut:true clears old state even on INVALID_LOGOUT_NAVIGATION_RESPONSE.
|
|
45
|
+
Unconfirmed/lost responses retain uncertainty, no navigation or automatic mutation retry.
|
|
46
|
+
Clicks share one Promise/adapter call; FIFO preserves rotation. Login/exchange/local logout/
|
|
47
|
+
clear/reset/dispose withdraw old navigation intent immediately, including queued login.
|
|
48
|
+
Synchronous navigate adapter handles login AND logout inside authority; do not defer handoff.
|
|
49
|
+
Single-use ticket (default60s, 1..120s) -> provider or fixed fallback; replay falls back.
|
|
50
|
+
Hint URL retained server-side until consume/expiry/cleanup; navigation URLs remain log/history
|
|
51
|
+
exposures. After IdP handoff its availability/return is not guaranteed. logout() stays local.
|
|
52
|
+
|
|
39
53
|
HTTP: requestTimeoutMs=15000 ms per vault headers/body/JSON attempt;
|
|
40
54
|
maxResponseBytes=1048576 streamed bytes. Bounds exclude queue/storage/key/hooks/
|
|
41
55
|
other locks. Timeout/size errors retain auth. API/proof signal stops only this
|
package/package.json
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
"name": "@web-ts-toolkit/oidc-vault-dpop-client",
|
|
3
3
|
"description": "DPoP browser client for @web-ts-toolkit/express-oidc-vault with body+cookie transports",
|
|
4
4
|
"homepage": "https://web-ts-toolkit.pages.dev/docs/packages/oidc-vault-dpop-client",
|
|
5
|
-
"version": "0.
|
|
5
|
+
"version": "0.55.0",
|
|
6
6
|
"sideEffects": false,
|
|
7
7
|
"keywords": [
|
|
8
8
|
"oidc",
|