@eventuras/fides-auth 0.14.1 → 0.15.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 +225 -33
- package/dist/activity-tracker.js +1 -2
- package/dist/cookies.d.ts +6 -0
- package/dist/cookies.d.ts.map +1 -1
- package/dist/cookies.js +8 -3
- package/dist/cookies.js.map +1 -1
- package/dist/heartbeat.js +1 -2
- package/dist/id-token.d.ts +31 -0
- package/dist/id-token.d.ts.map +1 -0
- package/dist/id-token.js +60 -0
- package/dist/id-token.js.map +1 -0
- package/dist/index.js +3 -3
- package/dist/logger.js +1 -2
- package/dist/oauth-browser.d.ts +13 -10
- package/dist/oauth-browser.d.ts.map +1 -1
- package/dist/oauth-browser.js +4 -2
- package/dist/oauth-browser.js.map +1 -1
- package/dist/oauth-config.d.ts +70 -0
- package/dist/oauth-config.d.ts.map +1 -0
- package/dist/oauth-logging.d.ts +4 -3
- package/dist/oauth-logging.d.ts.map +1 -1
- package/dist/oauth-logging.js +5 -5
- package/dist/oauth-logging.js.map +1 -1
- package/dist/oauth.d.ts +47 -45
- package/dist/oauth.d.ts.map +1 -1
- package/dist/oauth.js +58 -29
- package/dist/oauth.js.map +1 -1
- package/dist/oidc-discovery.d.ts +13 -4
- package/dist/oidc-discovery.d.ts.map +1 -1
- package/dist/oidc-discovery.js +82 -15
- package/dist/oidc-discovery.js.map +1 -1
- package/dist/pkce.d.ts +10 -0
- package/dist/pkce.d.ts.map +1 -0
- package/dist/pkce.js +23 -0
- package/dist/pkce.js.map +1 -0
- package/dist/providers/vipps/client.d.ts +3 -2
- package/dist/providers/vipps/client.d.ts.map +1 -1
- package/dist/providers/vipps/index.js +10 -10
- package/dist/providers/vipps/index.js.map +1 -1
- package/dist/providers/vipps/types.d.ts +2 -2
- package/dist/providers/vipps/types.d.ts.map +1 -1
- package/dist/rate-limit.js +1 -2
- package/dist/server/authorized-fetch.d.ts +78 -0
- package/dist/server/authorized-fetch.d.ts.map +1 -0
- package/dist/server/heartbeat-handler.d.ts +6 -4
- package/dist/server/heartbeat-handler.d.ts.map +1 -1
- package/dist/server/index.d.ts +4 -0
- package/dist/server/index.d.ts.map +1 -1
- package/dist/server/index.js +744 -94
- package/dist/server/index.js.map +1 -1
- package/dist/server/login-transaction.d.ts +55 -0
- package/dist/server/login-transaction.d.ts.map +1 -0
- package/dist/server/oidc-callback.d.ts +13 -4
- package/dist/server/oidc-callback.d.ts.map +1 -1
- package/dist/server/oidc-login.d.ts +14 -5
- package/dist/server/oidc-login.d.ts.map +1 -1
- package/dist/server/oidc-logout.d.ts +6 -4
- package/dist/server/oidc-logout.d.ts.map +1 -1
- package/dist/server/session-persistence.d.ts +102 -0
- package/dist/server/session-persistence.d.ts.map +1 -0
- package/dist/server/session-store.d.ts +39 -0
- package/dist/server/session-store.d.ts.map +1 -0
- package/dist/server/session.d.ts +35 -28
- package/dist/server/session.d.ts.map +1 -1
- package/dist/session-cookies.d.ts +1 -0
- package/dist/session-cookies.d.ts.map +1 -1
- package/dist/session-cookies.js +4 -3
- package/dist/session-cookies.js.map +1 -1
- package/dist/session-events.d.ts +10 -3
- package/dist/session-events.d.ts.map +1 -1
- package/dist/session-events.js +4 -3
- package/dist/session-events.js.map +1 -1
- package/dist/session-refresh.d.ts +1 -1
- package/dist/session-refresh.d.ts.map +1 -1
- package/dist/session-refresh.js +1 -2
- package/dist/session-refresh.js.map +1 -1
- package/dist/session-validation.d.ts.map +1 -1
- package/dist/session-validation.js +9 -6
- package/dist/session-validation.js.map +1 -1
- package/dist/silent-login.d.ts +1 -1
- package/dist/silent-login.d.ts.map +1 -1
- package/dist/silent-login.js +3 -3
- package/dist/silent-login.js.map +1 -1
- package/dist/types.d.ts +4 -1
- package/dist/types.d.ts.map +1 -1
- package/dist/utils.d.ts +0 -14
- package/dist/utils.d.ts.map +1 -1
- package/dist/utils.js +2 -24
- package/dist/utils.js.map +1 -1
- package/docs/discovery-cache.md +7 -3
- package/docs/rp-initiated-logout.md +6 -3
- package/docs/server-sessions.md +154 -0
- package/docs/session-events.md +17 -16
- package/package.json +14 -6
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
import type { SessionPersistenceMode, SessionRejectedReason } from '../session-events';
|
|
2
|
+
import type { Session } from '../types';
|
|
3
|
+
import type { CookieStore } from './cookie-store';
|
|
4
|
+
import type { SessionStore } from './session-store';
|
|
5
|
+
/** A session encryption key: 32 bytes for A256GCM, as hex or raw bytes. */
|
|
6
|
+
export type SessionSecret = string | Uint8Array;
|
|
7
|
+
/** A missing or malformed session secret: a configuration error, never a bad cookie. */
|
|
8
|
+
export declare class SessionSecretError extends TypeError {
|
|
9
|
+
name: string;
|
|
10
|
+
}
|
|
11
|
+
/** Throws unless `secret` is the 32 bytes A256GCM needs. Never echoes the secret. */
|
|
12
|
+
export declare function assertSessionSecret(secret: SessionSecret): void;
|
|
13
|
+
/** Where a handler finds the session: the cookies under `secret`, or `persistence`. One is required. */
|
|
14
|
+
export type SessionPersistenceConfig = {
|
|
15
|
+
/** Session encryption secret for the default cookie persistence. */
|
|
16
|
+
secret: SessionSecret;
|
|
17
|
+
/** Where the session lives; wins over `secret`. See {@link serverPersistence}. */
|
|
18
|
+
persistence?: SessionPersistence;
|
|
19
|
+
} | {
|
|
20
|
+
secret?: undefined;
|
|
21
|
+
persistence: SessionPersistence;
|
|
22
|
+
};
|
|
23
|
+
/** Outcome of reading the session: a session, or why there isn't one. */
|
|
24
|
+
export type ReadSessionResult = {
|
|
25
|
+
session: Session;
|
|
26
|
+
reason?: undefined;
|
|
27
|
+
} | {
|
|
28
|
+
session: null;
|
|
29
|
+
reason: SessionRejectedReason;
|
|
30
|
+
};
|
|
31
|
+
export interface WriteSessionOptions {
|
|
32
|
+
/**
|
|
33
|
+
* True when the write continues the session the request arrived with (a
|
|
34
|
+
* refresh). Otherwise the write starts a new session — under server
|
|
35
|
+
* persistence that mints a new handle, which is what defeats session
|
|
36
|
+
* fixation on login.
|
|
37
|
+
*/
|
|
38
|
+
continuation?: boolean;
|
|
39
|
+
}
|
|
40
|
+
export interface WriteSessionResult {
|
|
41
|
+
/** The value now in the main session cookie: the encrypted session, or the handle. */
|
|
42
|
+
cookieValue: string;
|
|
43
|
+
/** Bytes written across the session cookies. Absent when the session is not in cookies. */
|
|
44
|
+
cookieBytes?: number;
|
|
45
|
+
}
|
|
46
|
+
/**
|
|
47
|
+
* The persistence contract `session.ts` works against. Get one from
|
|
48
|
+
* {@link cookiePersistence} or {@link serverPersistence}; the methods are the
|
|
49
|
+
* library's own seam, not an extension point.
|
|
50
|
+
*/
|
|
51
|
+
export interface SessionPersistence {
|
|
52
|
+
readonly mode: SessionPersistenceMode;
|
|
53
|
+
write(cookies: CookieStore, session: Session, options?: WriteSessionOptions): Promise<WriteSessionResult>;
|
|
54
|
+
read(cookies: CookieStore): Promise<ReadSessionResult>;
|
|
55
|
+
/** The raw ID token whether or not the session is usable — logout needs it most when it isn't. */
|
|
56
|
+
readIdToken(cookies: CookieStore): Promise<string | undefined>;
|
|
57
|
+
clear(cookies: CookieStore): Promise<void>;
|
|
58
|
+
/**
|
|
59
|
+
* Runs `fn` with other refreshes of the same session held off. Concurrent
|
|
60
|
+
* callers in this process share one result; across instances the store's
|
|
61
|
+
* `withLock` serializes them. Present only where requests share state
|
|
62
|
+
* server-side — in cookie mode every request holds its own copy and there
|
|
63
|
+
* is nothing to coordinate.
|
|
64
|
+
*/
|
|
65
|
+
exclusive?<T>(cookies: CookieStore, fn: () => Promise<T>): Promise<T>;
|
|
66
|
+
}
|
|
67
|
+
/** Accepts the pre-0.15 bare secret wherever a persistence is expected. */
|
|
68
|
+
export declare function resolvePersistence(value: SessionSecret | SessionPersistence): SessionPersistence;
|
|
69
|
+
/** The configured `persistence`, else the bare `secret`. */
|
|
70
|
+
export declare function persistenceFrom(config: SessionPersistenceConfig): SessionSecret | SessionPersistence;
|
|
71
|
+
/** Deletes the `session`, `session_at` and `session_it` cookies. */
|
|
72
|
+
export declare function clearSessionCookies(cookies: CookieStore): Promise<void>;
|
|
73
|
+
/**
|
|
74
|
+
* The default: the session encrypted into the `session`, `session_at` and
|
|
75
|
+
* `session_it` cookies.
|
|
76
|
+
*/
|
|
77
|
+
export declare function cookiePersistence(secret: SessionSecret): SessionPersistence;
|
|
78
|
+
export interface ServerPersistenceOptions {
|
|
79
|
+
/** Where sessions live. See {@link SessionStore}. */
|
|
80
|
+
store: SessionStore;
|
|
81
|
+
/** Encrypts each record at rest, so a store dump alone yields no tokens. */
|
|
82
|
+
secret: SessionSecret;
|
|
83
|
+
/**
|
|
84
|
+
* Name of the cookie carrying the handle.
|
|
85
|
+
* @default 'session_ref'
|
|
86
|
+
*/
|
|
87
|
+
cookieName?: string;
|
|
88
|
+
/**
|
|
89
|
+
* Absolute session lifetime: the record and the handle cookie expire this
|
|
90
|
+
* long after login, refreshes notwithstanding.
|
|
91
|
+
* @default 30
|
|
92
|
+
*/
|
|
93
|
+
sessionDurationDays?: number;
|
|
94
|
+
}
|
|
95
|
+
/**
|
|
96
|
+
* The session in a {@link SessionStore}; the browser holds only a random
|
|
97
|
+
* 256-bit handle. The store is keyed by `SHA-256(handle)`, so neither a store
|
|
98
|
+
* dump nor a logged key can be replayed as a cookie, and the record is
|
|
99
|
+
* encrypted under `secret`, so a dump alone yields no tokens.
|
|
100
|
+
*/
|
|
101
|
+
export declare function serverPersistence(options: ServerPersistenceOptions): SessionPersistence;
|
|
102
|
+
//# sourceMappingURL=session-persistence.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"session-persistence.d.ts","sourceRoot":"","sources":["../../src/server/session-persistence.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
/** One persisted session, as the store sees it. */
|
|
2
|
+
export interface StoredSession {
|
|
3
|
+
/** The session, encrypted under the session secret. Opaque to the store. */
|
|
4
|
+
data: string;
|
|
5
|
+
/**
|
|
6
|
+
* Absolute session expiry, ISO 8601. Adapters map it to a TTL; the library
|
|
7
|
+
* also checks it on read, so a late TTL sweep never revives a session.
|
|
8
|
+
*/
|
|
9
|
+
expiresAt: string;
|
|
10
|
+
/** Correlation id of the session, so a record can be found from a log line. Not a secret. */
|
|
11
|
+
sid?: string;
|
|
12
|
+
}
|
|
13
|
+
/**
|
|
14
|
+
* Server-side session storage. Methods may be sync or async.
|
|
15
|
+
*
|
|
16
|
+
* Keys are hex SHA-256 digests of the handle, never the handle itself, so a
|
|
17
|
+
* store dump or a logged key cannot be replayed as a cookie.
|
|
18
|
+
*/
|
|
19
|
+
export interface SessionStore {
|
|
20
|
+
/** Returns the record, or null/undefined when there is none. May return an expired record. */
|
|
21
|
+
get(key: string): StoredSession | null | undefined | Promise<StoredSession | null | undefined>;
|
|
22
|
+
/** Creates or replaces the record. */
|
|
23
|
+
set(key: string, record: StoredSession): void | Promise<void>;
|
|
24
|
+
/** Removes the record. Deleting a missing key is not an error. */
|
|
25
|
+
delete(key: string): void | Promise<void>;
|
|
26
|
+
/**
|
|
27
|
+
* Optional: runs `fn` while holding a lock on `key`, so refreshes of one
|
|
28
|
+
* session are serialized across instances. Without it they are serialized
|
|
29
|
+
* within the process only. Implementations should expire the lock if the
|
|
30
|
+
* holder dies.
|
|
31
|
+
*/
|
|
32
|
+
withLock?<T>(key: string, fn: () => Promise<T>): Promise<T>;
|
|
33
|
+
}
|
|
34
|
+
/**
|
|
35
|
+
* In-memory store for tests and single-instance development. Sessions do not
|
|
36
|
+
* survive a restart, and expired records are dropped on read.
|
|
37
|
+
*/
|
|
38
|
+
export declare function createMemorySessionStore(): SessionStore;
|
|
39
|
+
//# sourceMappingURL=session-store.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"session-store.d.ts","sourceRoot":"","sources":["../../src/server/session-store.ts"],"names":[],"mappings":"AAMA,mDAAmD;AACnD,MAAM,WAAW,aAAa;IAC5B,4EAA4E;IAC5E,IAAI,EAAE,MAAM,CAAC;IACb;;;OAGG;IACH,SAAS,EAAE,MAAM,CAAC;IAClB,6FAA6F;IAC7F,GAAG,CAAC,EAAE,MAAM,CAAC;CACd;AAED;;;;;GAKG;AACH,MAAM,WAAW,YAAY;IAC3B,8FAA8F;IAC9F,GAAG,CAAC,GAAG,EAAE,MAAM,GAAG,aAAa,GAAG,IAAI,GAAG,SAAS,GAAG,OAAO,CAAC,aAAa,GAAG,IAAI,GAAG,SAAS,CAAC,CAAC;IAC/F,sCAAsC;IACtC,GAAG,CAAC,GAAG,EAAE,MAAM,EAAE,MAAM,EAAE,aAAa,GAAG,IAAI,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;IAC9D,kEAAkE;IAClE,MAAM,CAAC,GAAG,EAAE,MAAM,GAAG,IAAI,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;IAC1C;;;;;OAKG;IACH,QAAQ,CAAC,CAAC,CAAC,EAAE,GAAG,EAAE,MAAM,EAAE,EAAE,EAAE,MAAM,OAAO,CAAC,CAAC,CAAC,GAAG,OAAO,CAAC,CAAC,CAAC,CAAC;CAC7D;AAED;;;GAGG;AACH,wBAAgB,wBAAwB,IAAI,YAAY,CAiCvD"}
|
package/dist/server/session.d.ts
CHANGED
|
@@ -1,34 +1,33 @@
|
|
|
1
|
-
import type { OAuthConfig } from '../oauth';
|
|
1
|
+
import type { OAuthConfig } from '../oauth-config';
|
|
2
2
|
import { SESSION_EVENT, type RefreshFailureCause, type SessionClearedTrigger, type SessionRejectedReason } from '../session-events';
|
|
3
3
|
import type { CreateSessionOptions, Session } from '../types';
|
|
4
4
|
import type { CookieStore } from './cookie-store';
|
|
5
|
-
type
|
|
5
|
+
import { type ReadSessionResult, type SessionPersistence, type SessionSecret } from './session-persistence';
|
|
6
|
+
export type { ReadSessionResult } from './session-persistence';
|
|
6
7
|
/** How {@link persistSession} should report what it just wrote. */
|
|
7
8
|
export interface PersistSessionOptions {
|
|
8
9
|
/**
|
|
9
10
|
* Which lifecycle event this write represents. Omit to write silently — for
|
|
10
11
|
* callers that emit their own event, or that persist for reasons the session
|
|
11
|
-
* vocabulary doesn't cover.
|
|
12
|
+
* vocabulary doesn't cover. Under server persistence only
|
|
13
|
+
* {@link SESSION_EVENT.REFRESHED} keeps the handle; every other write starts
|
|
14
|
+
* a new session with a new one.
|
|
12
15
|
*/
|
|
13
16
|
event?: typeof SESSION_EVENT.CREATED | typeof SESSION_EVENT.REFRESHED;
|
|
14
17
|
/** Only meaningful for {@link SESSION_EVENT.REFRESHED}: did the provider issue a new refresh token? */
|
|
15
18
|
rotatedRefreshToken?: boolean;
|
|
16
19
|
}
|
|
17
20
|
/**
|
|
18
|
-
*
|
|
19
|
-
* "session_it" cookies, clearing the split-out cookies the session has no token for.
|
|
21
|
+
* Persists a session and raises its lifecycle event.
|
|
20
22
|
*
|
|
21
|
-
*
|
|
23
|
+
* With a secret, or {@link cookiePersistence}, the session is encrypted across
|
|
24
|
+
* the "session", "session_at" and "session_it" cookies. With
|
|
25
|
+
* {@link serverPersistence} it goes to the store and the browser gets a handle.
|
|
26
|
+
*
|
|
27
|
+
* @returns The value written to the main session cookie: the encrypted session,
|
|
28
|
+
* or the handle. Either is a bearer secret.
|
|
22
29
|
*/
|
|
23
|
-
export declare function persistSession(store: CookieStore, session: Session,
|
|
24
|
-
/** Outcome of reading the session from the cookie store. */
|
|
25
|
-
export type ReadSessionResult = {
|
|
26
|
-
session: Session;
|
|
27
|
-
reason?: undefined;
|
|
28
|
-
} | {
|
|
29
|
-
session: null;
|
|
30
|
-
reason: SessionRejectedReason;
|
|
31
|
-
};
|
|
30
|
+
export declare function persistSession(store: CookieStore, session: Session, persistence: SessionSecret | SessionPersistence, options?: PersistSessionOptions): Promise<string>;
|
|
32
31
|
/**
|
|
33
32
|
* Reads and reassembles the current session, reporting why when there isn't one.
|
|
34
33
|
*
|
|
@@ -37,15 +36,14 @@ export type ReadSessionResult = {
|
|
|
37
36
|
* endpoint logs. Does not log on its own — the caller owns the request context
|
|
38
37
|
* and decides whether a missing session is worth a line.
|
|
39
38
|
*/
|
|
40
|
-
export declare function tryReadSession(store: CookieStore,
|
|
39
|
+
export declare function tryReadSession(store: CookieStore, persistence: SessionSecret | SessionPersistence): Promise<ReadSessionResult>;
|
|
41
40
|
/**
|
|
42
|
-
* Reads and reassembles the current session
|
|
43
|
-
*
|
|
44
|
-
* the returned session may carry an expired access token.
|
|
41
|
+
* Reads and reassembles the current session, or null when there is none. The
|
|
42
|
+
* returned session may carry an expired access token.
|
|
45
43
|
*
|
|
46
44
|
* Prefer {@link tryReadSession} when you want to log *why* there is no session.
|
|
47
45
|
*/
|
|
48
|
-
export declare function readSession(store: CookieStore,
|
|
46
|
+
export declare function readSession(store: CookieStore, persistence: SessionSecret | SessionPersistence): Promise<Session | null>;
|
|
49
47
|
/** Outcome of a refresh attempt. */
|
|
50
48
|
export type RefreshSessionResult = {
|
|
51
49
|
ok: true;
|
|
@@ -74,8 +72,12 @@ export type RefreshSessionResult = {
|
|
|
74
72
|
* provider" into the same `null`, and a caller that logs the user out on both
|
|
75
73
|
* ends sessions that were never actually invalid. Only `invalid_grant` is
|
|
76
74
|
* terminal — see {@link classifyRefreshFailure}.
|
|
75
|
+
*
|
|
76
|
+
* Under server persistence, refreshes of one session are serialized: concurrent
|
|
77
|
+
* callers get the first refresh's result rather than each spending the refresh
|
|
78
|
+
* token, which with rotation would leave all but one holding a dead one.
|
|
77
79
|
*/
|
|
78
|
-
export declare function tryRefreshSessionInStore(store: CookieStore, config: OAuthConfig,
|
|
80
|
+
export declare function tryRefreshSessionInStore(store: CookieStore, config: OAuthConfig, persistence: SessionSecret | SessionPersistence, options?: CreateSessionOptions): Promise<RefreshSessionResult>;
|
|
79
81
|
/**
|
|
80
82
|
* Refreshes the stored session using its refresh token and persists the result.
|
|
81
83
|
* Returns the updated session, or null when there is nothing to refresh or the
|
|
@@ -85,20 +87,25 @@ export declare function tryRefreshSessionInStore(store: CookieStore, config: OAu
|
|
|
85
87
|
* refresh token apart from an unreachable provider, and treating the two alike
|
|
86
88
|
* logs out users whose sessions are still valid.
|
|
87
89
|
*/
|
|
88
|
-
export declare function refreshSessionInStore(store: CookieStore, config: OAuthConfig,
|
|
90
|
+
export declare function refreshSessionInStore(store: CookieStore, config: OAuthConfig, persistence: SessionSecret | SessionPersistence, options?: CreateSessionOptions): Promise<Session | null>;
|
|
89
91
|
/**
|
|
90
|
-
* Reads the raw ID token independently of
|
|
91
|
-
*
|
|
92
|
+
* Reads the raw ID token independently of session validity — logout needs the
|
|
93
|
+
* hint even when {@link readSession} returns null.
|
|
92
94
|
*/
|
|
93
|
-
export declare function readIdToken(store: CookieStore,
|
|
95
|
+
export declare function readIdToken(store: CookieStore, persistence: SessionSecret | SessionPersistence): Promise<string | undefined>;
|
|
94
96
|
/** Context for the {@link SESSION_EVENT.CLEARED} event raised by {@link clearSession}. */
|
|
95
97
|
export interface ClearSessionOptions {
|
|
96
|
-
/** Why the
|
|
98
|
+
/** Why the session is going away. Omit to clear without raising an event. */
|
|
97
99
|
trigger?: SessionClearedTrigger;
|
|
98
100
|
/** Correlation id of the session being cleared, when the caller has it. */
|
|
99
101
|
sid?: string;
|
|
102
|
+
/**
|
|
103
|
+
* Where the session lives. Defaults to the cookies; under server
|
|
104
|
+
* persistence this is what deletes the record, which is the revocation
|
|
105
|
+
* cookie persistence cannot do.
|
|
106
|
+
*/
|
|
107
|
+
persistence?: SessionSecret | SessionPersistence;
|
|
100
108
|
}
|
|
101
|
-
/**
|
|
109
|
+
/** Ends the session: deletes the cookies and, under server persistence, the record. */
|
|
102
110
|
export declare function clearSession(store: CookieStore, options?: ClearSessionOptions): Promise<void>;
|
|
103
|
-
export {};
|
|
104
111
|
//# sourceMappingURL=session.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"session.d.ts","sourceRoot":"","sources":["../../src/server/session.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"session.d.ts","sourceRoot":"","sources":["../../src/server/session.ts"],"names":[],"mappings":"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"}
|
|
@@ -27,6 +27,7 @@ export interface RawSessionCookies {
|
|
|
27
27
|
*
|
|
28
28
|
* @param session - The session to encode.
|
|
29
29
|
* @param secret - The encryption key as a hex string or Uint8Array (32 bytes for A256GCM).
|
|
30
|
+
* @throws TypeError when the session has neither `tokens` nor `user`, so it could not be read back.
|
|
30
31
|
*/
|
|
31
32
|
export declare function encodeSessionCookies(session: Session, secret: string | Uint8Array): Promise<EncodedSessionCookies>;
|
|
32
33
|
/** Outcome of decoding the cookie values: a session, or why there isn't one. */
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"session-cookies.d.ts","sourceRoot":"","sources":["../src/session-cookies.ts"],"names":[],"mappings":"AAcA,OAAO,KAAK,EAAE,qBAAqB,EAAE,MAAM,kBAAkB,CAAC;AAC9D,OAAO,EAAE,OAAO,EAAE,MAAM,SAAS,CAAC;AAIlC,uDAAuD;AACvD,MAAM,WAAW,qBAAqB;IACpC,sFAAsF;IACtF,OAAO,EAAE,MAAM,CAAC;IAChB;;;OAGG;IACH,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,mFAAmF;IACnF,OAAO,CAAC,EAAE,MAAM,CAAC;CAClB;AAED,8DAA8D;AAC9D,MAAM,WAAW,iBAAiB;IAChC,wDAAwD;IACxD,OAAO,EAAE,MAAM,GAAG,IAAI,CAAC;IACvB,wDAAwD;IACxD,WAAW,EAAE,MAAM,GAAG,IAAI,CAAC;IAC3B,oDAAoD;IACpD,OAAO,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;CACzB;AAED
|
|
1
|
+
{"version":3,"file":"session-cookies.d.ts","sourceRoot":"","sources":["../src/session-cookies.ts"],"names":[],"mappings":"AAcA,OAAO,KAAK,EAAE,qBAAqB,EAAE,MAAM,kBAAkB,CAAC;AAC9D,OAAO,EAAE,OAAO,EAAE,MAAM,SAAS,CAAC;AAIlC,uDAAuD;AACvD,MAAM,WAAW,qBAAqB;IACpC,sFAAsF;IACtF,OAAO,EAAE,MAAM,CAAC;IAChB;;;OAGG;IACH,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,mFAAmF;IACnF,OAAO,CAAC,EAAE,MAAM,CAAC;CAClB;AAED,8DAA8D;AAC9D,MAAM,WAAW,iBAAiB;IAChC,wDAAwD;IACxD,OAAO,EAAE,MAAM,GAAG,IAAI,CAAC;IACvB,wDAAwD;IACxD,WAAW,EAAE,MAAM,GAAG,IAAI,CAAC;IAC3B,oDAAoD;IACpD,OAAO,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;CACzB;AAED;;;;;;;GAOG;AACH,wBAAsB,oBAAoB,CACxC,OAAO,EAAE,OAAO,EAChB,MAAM,EAAE,MAAM,GAAG,UAAU,GAC1B,OAAO,CAAC,qBAAqB,CAAC,CAsBhC;AAED,gFAAgF;AAChF,MAAM,MAAM,qBAAqB,GAC7B;IAAE,OAAO,EAAE,OAAO,CAAC;IAAC,MAAM,CAAC,EAAE,SAAS,CAAA;CAAE,GACxC;IAAE,OAAO,EAAE,IAAI,CAAC;IAAC,MAAM,EAAE,qBAAqB,CAAA;CAAE,CAAC;AAErD;;;;;;;;;;;GAWG;AACH,wBAAsB,uBAAuB,CAC3C,GAAG,EAAE,iBAAiB,EACtB,MAAM,EAAE,MAAM,GAAG,UAAU,GAC1B,OAAO,CAAC,qBAAqB,CAAC,CAyChC;AAED;;;;;;;;;;;;;;GAcG;AACH,wBAAsB,oBAAoB,CACxC,GAAG,EAAE,iBAAiB,EACtB,MAAM,EAAE,MAAM,GAAG,UAAU,GAC1B,OAAO,CAAC,OAAO,GAAG,IAAI,CAAC,CAEzB;AAED;;;;;;;GAOG;AACH,wBAAsB,mBAAmB,CACvC,GAAG,EAAE,MAAM,EACX,MAAM,EAAE,MAAM,GAAG,UAAU,GAC1B,OAAO,CAAC,MAAM,GAAG,SAAS,CAAC,CAQ7B"}
|
package/dist/session-cookies.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { createLogger } from "./logger.js";
|
|
2
2
|
import { createEncryptedJWT, decryptJWT } from "./utils.js";
|
|
3
|
-
import { validateSessionJwt } from "./session-validation.js";
|
|
4
|
-
|
|
3
|
+
import { isSession, validateSessionJwt } from "./session-validation.js";
|
|
4
|
+
// src/session-cookies.ts
|
|
5
5
|
var logger = createLogger({ namespace: "fides-auth:session-cookies" });
|
|
6
6
|
/**
|
|
7
7
|
* Encrypts a session into the three cookie values, splitting the raw access and ID
|
|
@@ -9,8 +9,10 @@ var logger = createLogger({ namespace: "fides-auth:session-cookies" });
|
|
|
9
9
|
*
|
|
10
10
|
* @param session - The session to encode.
|
|
11
11
|
* @param secret - The encryption key as a hex string or Uint8Array (32 bytes for A256GCM).
|
|
12
|
+
* @throws TypeError when the session has neither `tokens` nor `user`, so it could not be read back.
|
|
12
13
|
*/
|
|
13
14
|
async function encodeSessionCookies(session, secret) {
|
|
15
|
+
if (!isSession(session)) throw new TypeError("A session needs a tokens or user object");
|
|
14
16
|
const { tokens, ...rest } = session;
|
|
15
17
|
const accessToken = tokens?.accessToken;
|
|
16
18
|
const idToken = tokens?.idToken;
|
|
@@ -104,7 +106,6 @@ async function decodeIdTokenCookie(raw, secret) {
|
|
|
104
106
|
return;
|
|
105
107
|
}
|
|
106
108
|
}
|
|
107
|
-
//#endregion
|
|
108
109
|
export { decodeIdTokenCookie, decodeSessionCookies, encodeSessionCookies, tryDecodeSessionCookies };
|
|
109
110
|
|
|
110
111
|
//# sourceMappingURL=session-cookies.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"session-cookies.js","names":[],"sources":["../src/session-cookies.ts"],"sourcesContent":["// session-cookies.ts\n//\n// Framework-agnostic encoding/decoding of a session into cookie *values*. This\n// module performs no I/O — it never reads or writes cookies. Framework adapters\n// (Next.js, React Router, …) own the actual cookie store and call these helpers\n// to split a session across two cookie values and to reassemble it again.\n//\n// The split: the large raw JWTs — the access token and the ID token (kept for\n// `id_token_hint` on logout) — each get their own encrypted value, everything else\n// goes in the main one, so no cookie has to carry two JWTs against the ~4KB limit.\n\nimport { createEncryptedJWT, decryptJWT } from './utils';\nimport { validateSessionJwt } from './session-validation';\nimport { createLogger } from './logger';\nimport type { SessionRejectedReason } from './session-events';\nimport { Session } from './types';\n\nconst logger = createLogger({ namespace: 'fides-auth:session-cookies' });\n\n/** Encrypted cookie values produced from a session. */\nexport interface EncodedSessionCookies {\n /** Value for the main session cookie (everything except the raw access/ID tokens). */\n session: string;\n /**\n * Value for the access-token cookie, or `undefined` when the session carries\n * no access token. Adapters should delete the access-token cookie in that case.\n */\n accessToken?: string;\n /** Value for the ID-token cookie, or `undefined` when the session carries none. */\n idToken?: string;\n}\n\n/** Raw cookie values read from the store, before decoding. */\nexport interface RawSessionCookies {\n /** The main session cookie value, or null if absent. */\n session: string | null;\n /** The access-token cookie value, or null if absent. */\n accessToken: string | null;\n /** The ID-token cookie value, or null if absent. */\n idToken?: string | null;\n}\n\n/**\n * Encrypts a session into the three cookie values, splitting the raw access and ID\n * tokens out.\n *\n * @param session - The session to encode.\n * @param secret - The encryption key as a hex string or Uint8Array (32 bytes for A256GCM).\n */\nexport async function encodeSessionCookies(\n session: Session,\n secret: string | Uint8Array,\n): Promise<EncodedSessionCookies> {\n const { tokens, ...rest } = session;\n const accessToken = tokens?.accessToken;\n const idToken = tokens?.idToken;\n\n // Main value: the whole session minus the raw access and ID tokens (undefined\n // keys are dropped during JWT serialization).\n const coreSession: Session = {\n ...rest,\n tokens: tokens ? { ...tokens, accessToken: undefined, idToken: undefined } : undefined,\n };\n\n const main = await createEncryptedJWT(coreSession, secret);\n\n return {\n session: main,\n accessToken: accessToken ? await createEncryptedJWT({ accessToken }, secret) : undefined,\n idToken: idToken ? await createEncryptedJWT({ idToken }, secret) : undefined,\n };\n}\n\n/** Outcome of decoding the cookie values: a session, or why there isn't one. */\nexport type DecodedSessionCookies =\n | { session: Session; reason?: undefined }\n | { session: null; reason: SessionRejectedReason };\n\n/**\n * Decodes the cookie values back into a session, reporting why when it can't.\n *\n * Same rules as {@link decodeSessionCookies}, but the failure carries a\n * {@link SessionRejectedReason} from the package-wide vocabulary so a proxy or a\n * status route logs the same word for the same situation as the heartbeat\n * endpoint does. Deliberately does not log: the caller knows the request\n * context, and logging here too would double every rejection.\n *\n * @param raw - The raw cookie values read from the store.\n * @param secret - The decryption key as a hex string or Uint8Array (32 bytes for A256GCM).\n */\nexport async function tryDecodeSessionCookies(\n raw: RawSessionCookies,\n secret: string | Uint8Array,\n): Promise<DecodedSessionCookies> {\n if (!raw.session) {\n return { session: null, reason: 'no_session_cookie' };\n }\n\n const { status, session } = await validateSessionJwt(raw.session, secret);\n\n // EXPIRED only fires for legacy single-cookie sessions — dropped, not migrated,\n // and worth separating from corruption because it costs exactly one re-login.\n // validateSessionJwt already logs why it rejected the value; this layer only\n // translates that into the shared vocabulary.\n if (status !== 'VALID' || !session) {\n return {\n session: null,\n reason: status === 'EXPIRED' ? 'stale_legacy_session' : 'unreadable_session',\n };\n }\n\n if (raw.accessToken) {\n try {\n const payload = await decryptJWT(raw.accessToken, secret);\n const accessToken = typeof payload.accessToken === 'string' ? payload.accessToken : undefined;\n\n if (accessToken) {\n // Attached as stored, expired or not — freshness is the caller's concern.\n session.tokens = { ...session.tokens, accessToken };\n }\n } catch (error) {\n // A corrupt/forged access-token value shouldn't take down the session.\n logger.warn({ error }, 'Failed to decode access-token cookie');\n }\n }\n\n if (raw.idToken) {\n const idToken = await decodeIdTokenCookie(raw.idToken, secret);\n if (idToken) {\n session.tokens = { ...session.tokens, idToken };\n }\n }\n\n return { session };\n}\n\n/**\n * Decodes the cookie values back into a session, reattaching the access and ID tokens.\n *\n * Returns the session whenever the main value decrypts and validates — an\n * expired access token does not hide it; token freshness is the caller's\n * concern. Returns null for a missing/invalid main value, or a stale legacy\n * single-cookie session (one re-login). A corrupt access-token value is\n * tolerated: the session is returned without it.\n *\n * Prefer {@link tryDecodeSessionCookies} when you want to log *why* there is no\n * session.\n *\n * @param raw - The raw cookie values read from the store.\n * @param secret - The decryption key as a hex string or Uint8Array (32 bytes for A256GCM).\n */\nexport async function decodeSessionCookies(\n raw: RawSessionCookies,\n secret: string | Uint8Array,\n): Promise<Session | null> {\n return (await tryDecodeSessionCookies(raw, secret)).session;\n}\n\n/**\n * Decrypts the ID-token cookie value on its own, independent of session validity.\n * Logout needs the hint precisely when the session has gone stale, so this must not\n * go through {@link decodeSessionCookies}.\n *\n * @param raw - The ID-token cookie value.\n * @param secret - The decryption key as a hex string or Uint8Array (32 bytes for A256GCM).\n */\nexport async function decodeIdTokenCookie(\n raw: string,\n secret: string | Uint8Array,\n): Promise<string | undefined> {\n try {\n const payload = await decryptJWT(raw, secret);\n return typeof payload.idToken === 'string' ? payload.idToken : undefined;\n } catch (error) {\n logger.warn({ error }, 'Failed to decode ID-token cookie');\n return undefined;\n }\n}\n"],"mappings":";;;;AAiBA,IAAM,SAAS,aAAa,EAAE,WAAW,6BAA6B,CAAC
|
|
1
|
+
{"version":3,"file":"session-cookies.js","names":[],"sources":["../src/session-cookies.ts"],"sourcesContent":["// session-cookies.ts\n//\n// Framework-agnostic encoding/decoding of a session into cookie *values*. This\n// module performs no I/O — it never reads or writes cookies. Framework adapters\n// (Next.js, React Router, …) own the actual cookie store and call these helpers\n// to split a session across two cookie values and to reassemble it again.\n//\n// The split: the large raw JWTs — the access token and the ID token (kept for\n// `id_token_hint` on logout) — each get their own encrypted value, everything else\n// goes in the main one, so no cookie has to carry two JWTs against the ~4KB limit.\n\nimport { createEncryptedJWT, decryptJWT } from './utils';\nimport { isSession, validateSessionJwt } from './session-validation';\nimport { createLogger } from './logger';\nimport type { SessionRejectedReason } from './session-events';\nimport { Session } from './types';\n\nconst logger = createLogger({ namespace: 'fides-auth:session-cookies' });\n\n/** Encrypted cookie values produced from a session. */\nexport interface EncodedSessionCookies {\n /** Value for the main session cookie (everything except the raw access/ID tokens). */\n session: string;\n /**\n * Value for the access-token cookie, or `undefined` when the session carries\n * no access token. Adapters should delete the access-token cookie in that case.\n */\n accessToken?: string;\n /** Value for the ID-token cookie, or `undefined` when the session carries none. */\n idToken?: string;\n}\n\n/** Raw cookie values read from the store, before decoding. */\nexport interface RawSessionCookies {\n /** The main session cookie value, or null if absent. */\n session: string | null;\n /** The access-token cookie value, or null if absent. */\n accessToken: string | null;\n /** The ID-token cookie value, or null if absent. */\n idToken?: string | null;\n}\n\n/**\n * Encrypts a session into the three cookie values, splitting the raw access and ID\n * tokens out.\n *\n * @param session - The session to encode.\n * @param secret - The encryption key as a hex string or Uint8Array (32 bytes for A256GCM).\n * @throws TypeError when the session has neither `tokens` nor `user`, so it could not be read back.\n */\nexport async function encodeSessionCookies(\n session: Session,\n secret: string | Uint8Array,\n): Promise<EncodedSessionCookies> {\n if (!isSession(session)) {\n throw new TypeError('A session needs a tokens or user object');\n }\n const { tokens, ...rest } = session;\n const accessToken = tokens?.accessToken;\n const idToken = tokens?.idToken;\n\n // Main value: the whole session minus the raw access and ID tokens (undefined\n // keys are dropped during JWT serialization).\n const coreSession: Session = {\n ...rest,\n tokens: tokens ? { ...tokens, accessToken: undefined, idToken: undefined } : undefined,\n };\n\n const main = await createEncryptedJWT(coreSession, secret);\n\n return {\n session: main,\n accessToken: accessToken ? await createEncryptedJWT({ accessToken }, secret) : undefined,\n idToken: idToken ? await createEncryptedJWT({ idToken }, secret) : undefined,\n };\n}\n\n/** Outcome of decoding the cookie values: a session, or why there isn't one. */\nexport type DecodedSessionCookies =\n | { session: Session; reason?: undefined }\n | { session: null; reason: SessionRejectedReason };\n\n/**\n * Decodes the cookie values back into a session, reporting why when it can't.\n *\n * Same rules as {@link decodeSessionCookies}, but the failure carries a\n * {@link SessionRejectedReason} from the package-wide vocabulary so a proxy or a\n * status route logs the same word for the same situation as the heartbeat\n * endpoint does. Deliberately does not log: the caller knows the request\n * context, and logging here too would double every rejection.\n *\n * @param raw - The raw cookie values read from the store.\n * @param secret - The decryption key as a hex string or Uint8Array (32 bytes for A256GCM).\n */\nexport async function tryDecodeSessionCookies(\n raw: RawSessionCookies,\n secret: string | Uint8Array,\n): Promise<DecodedSessionCookies> {\n if (!raw.session) {\n return { session: null, reason: 'no_session_cookie' };\n }\n\n const { status, session } = await validateSessionJwt(raw.session, secret);\n\n // EXPIRED only fires for legacy single-cookie sessions — dropped, not migrated,\n // and worth separating from corruption because it costs exactly one re-login.\n // validateSessionJwt already logs why it rejected the value; this layer only\n // translates that into the shared vocabulary.\n if (status !== 'VALID' || !session) {\n return {\n session: null,\n reason: status === 'EXPIRED' ? 'stale_legacy_session' : 'unreadable_session',\n };\n }\n\n if (raw.accessToken) {\n try {\n const payload = await decryptJWT(raw.accessToken, secret);\n const accessToken = typeof payload.accessToken === 'string' ? payload.accessToken : undefined;\n\n if (accessToken) {\n // Attached as stored, expired or not — freshness is the caller's concern.\n session.tokens = { ...session.tokens, accessToken };\n }\n } catch (error) {\n // A corrupt/forged access-token value shouldn't take down the session.\n logger.warn({ error }, 'Failed to decode access-token cookie');\n }\n }\n\n if (raw.idToken) {\n const idToken = await decodeIdTokenCookie(raw.idToken, secret);\n if (idToken) {\n session.tokens = { ...session.tokens, idToken };\n }\n }\n\n return { session };\n}\n\n/**\n * Decodes the cookie values back into a session, reattaching the access and ID tokens.\n *\n * Returns the session whenever the main value decrypts and validates — an\n * expired access token does not hide it; token freshness is the caller's\n * concern. Returns null for a missing/invalid main value, or a stale legacy\n * single-cookie session (one re-login). A corrupt access-token value is\n * tolerated: the session is returned without it.\n *\n * Prefer {@link tryDecodeSessionCookies} when you want to log *why* there is no\n * session.\n *\n * @param raw - The raw cookie values read from the store.\n * @param secret - The decryption key as a hex string or Uint8Array (32 bytes for A256GCM).\n */\nexport async function decodeSessionCookies(\n raw: RawSessionCookies,\n secret: string | Uint8Array,\n): Promise<Session | null> {\n return (await tryDecodeSessionCookies(raw, secret)).session;\n}\n\n/**\n * Decrypts the ID-token cookie value on its own, independent of session validity.\n * Logout needs the hint precisely when the session has gone stale, so this must not\n * go through {@link decodeSessionCookies}.\n *\n * @param raw - The ID-token cookie value.\n * @param secret - The decryption key as a hex string or Uint8Array (32 bytes for A256GCM).\n */\nexport async function decodeIdTokenCookie(\n raw: string,\n secret: string | Uint8Array,\n): Promise<string | undefined> {\n try {\n const payload = await decryptJWT(raw, secret);\n return typeof payload.idToken === 'string' ? payload.idToken : undefined;\n } catch (error) {\n logger.warn({ error }, 'Failed to decode ID-token cookie');\n return undefined;\n }\n}\n"],"mappings":";;;;AAiBA,IAAM,SAAS,aAAa,EAAE,WAAW,6BAA6B,CAAC;;;;;;;;;AAiCvE,eAAsB,qBACpB,SACA,QACgC;CAChC,IAAI,CAAC,UAAU,OAAO,GACpB,MAAM,IAAI,UAAU,yCAAyC;CAE/D,MAAM,EAAE,QAAQ,GAAG,SAAS;CAC5B,MAAM,cAAc,QAAQ;CAC5B,MAAM,UAAU,QAAQ;CAIxB,MAAM,cAAuB;EAC3B,GAAG;EACH,QAAQ,SAAS;GAAE,GAAG;GAAQ,aAAa,KAAA;GAAW,SAAS,KAAA;EAAU,IAAI,KAAA;CAC/E;CAIA,OAAO;EACL,SAAS,MAHQ,mBAAmB,aAAa,MAAM;EAIvD,aAAa,cAAc,MAAM,mBAAmB,EAAE,YAAY,GAAG,MAAM,IAAI,KAAA;EAC/E,SAAS,UAAU,MAAM,mBAAmB,EAAE,QAAQ,GAAG,MAAM,IAAI,KAAA;CACrE;AACF;;;;;;;;;;;;;AAmBA,eAAsB,wBACpB,KACA,QACgC;CAChC,IAAI,CAAC,IAAI,SACP,OAAO;EAAE,SAAS;EAAM,QAAQ;CAAoB;CAGtD,MAAM,EAAE,QAAQ,YAAY,MAAM,mBAAmB,IAAI,SAAS,MAAM;CAMxE,IAAI,WAAW,WAAW,CAAC,SACzB,OAAO;EACL,SAAS;EACT,QAAQ,WAAW,YAAY,yBAAyB;CAC1D;CAGF,IAAI,IAAI,aACN,IAAI;EACF,MAAM,UAAU,MAAM,WAAW,IAAI,aAAa,MAAM;EACxD,MAAM,cAAc,OAAO,QAAQ,gBAAgB,WAAW,QAAQ,cAAc,KAAA;EAEpF,IAAI,aAEF,QAAQ,SAAS;GAAE,GAAG,QAAQ;GAAQ;EAAY;CAEtD,SAAS,OAAO;EAEd,OAAO,KAAK,EAAE,MAAM,GAAG,sCAAsC;CAC/D;CAGF,IAAI,IAAI,SAAS;EACf,MAAM,UAAU,MAAM,oBAAoB,IAAI,SAAS,MAAM;EAC7D,IAAI,SACF,QAAQ,SAAS;GAAE,GAAG,QAAQ;GAAQ;EAAQ;CAElD;CAEA,OAAO,EAAE,QAAQ;AACnB;;;;;;;;;;;;;;;;AAiBA,eAAsB,qBACpB,KACA,QACyB;CACzB,QAAQ,MAAM,wBAAwB,KAAK,MAAM,EAAA,CAAG;AACtD;;;;;;;;;AAUA,eAAsB,oBACpB,KACA,QAC6B;CAC7B,IAAI;EACF,MAAM,UAAU,MAAM,WAAW,KAAK,MAAM;EAC5C,OAAO,OAAO,QAAQ,YAAY,WAAW,QAAQ,UAAU,KAAA;CACjE,SAAS,OAAO;EACd,OAAO,KAAK,EAAE,MAAM,GAAG,kCAAkC;EACzD;CACF;AACF"}
|
package/dist/session-events.d.ts
CHANGED
|
@@ -32,7 +32,9 @@ export type SessionRejectedReason =
|
|
|
32
32
|
/** The session decoded but carries no refresh token — usually `offline_access` was never granted. */
|
|
33
33
|
| 'no_refresh_token'
|
|
34
34
|
/** The refresh token was rejected by the provider. See {@link RefreshFailureCause}. */
|
|
35
|
-
| 'refresh_failed'
|
|
35
|
+
| 'refresh_failed'
|
|
36
|
+
/** A handle was sent but the server store has no live record for it: revoked, expired or evicted. Server persistence only. */
|
|
37
|
+
| 'session_not_found';
|
|
36
38
|
/**
|
|
37
39
|
* Why a refresh attempt failed.
|
|
38
40
|
*
|
|
@@ -47,6 +49,8 @@ export type RefreshFailureCause =
|
|
|
47
49
|
| 'transport'
|
|
48
50
|
/** The provider was reached but answered with an error or something unparseable. Retryable. */
|
|
49
51
|
| 'idp_error';
|
|
52
|
+
/** Where a session lives between requests. See `server/session-persistence.ts`. */
|
|
53
|
+
export type SessionPersistenceMode = 'cookie' | 'server';
|
|
50
54
|
/** What caused the session cookies to be deleted. */
|
|
51
55
|
export type SessionClearedTrigger =
|
|
52
56
|
/** The user (or the app) logged out deliberately. */
|
|
@@ -63,8 +67,10 @@ export type SessionEvent = {
|
|
|
63
67
|
scopes?: string[];
|
|
64
68
|
/** Seconds until the access token expires. */
|
|
65
69
|
expiresIn?: number;
|
|
66
|
-
/** Total bytes written across the session cookies. Near ~4096 means a session about to break. */
|
|
70
|
+
/** Total bytes written across the session cookies. Near ~4096 means a session about to break. Absent under server persistence. */
|
|
67
71
|
cookieBytes?: number;
|
|
72
|
+
/** Where the session was written. Absent on events from before this field existed. */
|
|
73
|
+
persistence?: SessionPersistenceMode;
|
|
68
74
|
} | {
|
|
69
75
|
event: typeof SESSION_EVENT.REFRESHED;
|
|
70
76
|
sid?: string;
|
|
@@ -72,6 +78,7 @@ export type SessionEvent = {
|
|
|
72
78
|
/** True when the provider issued a new refresh token. Rotation turns a lost cookie write into a dead session. */
|
|
73
79
|
rotatedRefreshToken: boolean;
|
|
74
80
|
cookieBytes?: number;
|
|
81
|
+
persistence?: SessionPersistenceMode;
|
|
75
82
|
} | {
|
|
76
83
|
event: typeof SESSION_EVENT.REJECTED;
|
|
77
84
|
sid?: string;
|
|
@@ -104,7 +111,7 @@ export declare function sessionEventLevel(payload: SessionEvent): Level;
|
|
|
104
111
|
*/
|
|
105
112
|
export declare function logSessionEvent(logger: FidesLogger, payload: SessionEvent): void;
|
|
106
113
|
/** The reasons, as a runtime list — for validating values that arrive over the wire. */
|
|
107
|
-
export declare const SESSION_REJECTED_REASONS: readonly ["no_session_cookie", "unreadable_session", "stale_legacy_session", "no_refresh_token", "refresh_failed"];
|
|
114
|
+
export declare const SESSION_REJECTED_REASONS: readonly ["no_session_cookie", "unreadable_session", "stale_legacy_session", "no_refresh_token", "refresh_failed", "session_not_found"];
|
|
108
115
|
/** Narrows an untrusted value (a response body, a query param) to a known reason. */
|
|
109
116
|
export declare function isSessionRejectedReason(value: unknown): value is SessionRejectedReason;
|
|
110
117
|
/**
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"session-events.d.ts","sourceRoot":"","sources":["../src/session-events.ts"],"names":[],"mappings":"AAeA,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,UAAU,CAAC;AAE5C;;;GAGG;AACH,eAAO,MAAM,aAAa;IACxB,wFAAwF;aACxF,OAAO,EAAE,iBAAiB;IAC1B,iEAAiE;aACjE,SAAS,EAAE,mBAAmB;IAC9B,oGAAoG;aACpG,QAAQ,EAAE,kBAAkB;IAC5B,uEAAuE;aACvE,cAAc,EAAE,wBAAwB;IACxC,6EAA6E;aAC7E,OAAO,EAAE,iBAAiB;CAClB,CAAC;AAEX,MAAM,MAAM,gBAAgB,GAAG,CAAC,OAAO,aAAa,CAAC,CAAC,MAAM,OAAO,aAAa,CAAC,CAAC;AAElF;;;;;GAKG;AACH,MAAM,MAAM,qBAAqB;AAC/B,+DAA+D;AAC7D,mBAAmB;AACrB,mGAAmG;GACjG,oBAAoB;AACtB,2FAA2F;GACzF,sBAAsB;AACxB,qGAAqG;GACnG,kBAAkB;AACpB,uFAAuF;GACrF,gBAAgB,CAAC;
|
|
1
|
+
{"version":3,"file":"session-events.d.ts","sourceRoot":"","sources":["../src/session-events.ts"],"names":[],"mappings":"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"}
|
package/dist/session-events.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
|
|
1
|
+
// src/session-events.ts
|
|
2
2
|
/**
|
|
3
3
|
* Event names. Use these constants rather than string literals so a rename is a
|
|
4
4
|
* compile error at every call site instead of a silently broken dashboard.
|
|
@@ -26,6 +26,7 @@ var SESSION_EVENT = {
|
|
|
26
26
|
var REJECTED_LEVEL = {
|
|
27
27
|
no_session_cookie: "debug",
|
|
28
28
|
stale_legacy_session: "info",
|
|
29
|
+
session_not_found: "info",
|
|
29
30
|
refresh_failed: "info",
|
|
30
31
|
unreadable_session: "warn",
|
|
31
32
|
no_refresh_token: "warn"
|
|
@@ -70,7 +71,8 @@ var SESSION_REJECTED_REASONS = [
|
|
|
70
71
|
"unreadable_session",
|
|
71
72
|
"stale_legacy_session",
|
|
72
73
|
"no_refresh_token",
|
|
73
|
-
"refresh_failed"
|
|
74
|
+
"refresh_failed",
|
|
75
|
+
"session_not_found"
|
|
74
76
|
];
|
|
75
77
|
/** Narrows an untrusted value (a response body, a query param) to a known reason. */
|
|
76
78
|
function isSessionRejectedReason(value) {
|
|
@@ -90,7 +92,6 @@ function newSessionId() {
|
|
|
90
92
|
crypto.getRandomValues(bytes);
|
|
91
93
|
return Array.from(bytes, (b) => b.toString(16).padStart(2, "0")).join("");
|
|
92
94
|
}
|
|
93
|
-
//#endregion
|
|
94
95
|
export { SESSION_EVENT, SESSION_REJECTED_REASONS, isSessionRejectedReason, logSessionEvent, newSessionId, sessionEventLevel };
|
|
95
96
|
|
|
96
97
|
//# sourceMappingURL=session-events.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"session-events.js","names":[],"sources":["../src/session-events.ts"],"sourcesContent":["// session-events.ts\n//\n// The log vocabulary fides-auth emits for the session lifecycle. Every point\n// where the library creates a session, renews one, or decides one cannot\n// proceed emits exactly one event from this module — so a production operator\n// can answer \"which path ended this session?\" from logs alone, without reading\n// library source.\n//\n// Naming: `<area>.<outcome>`, dotted and past tense. Dotted so a dashboard can\n// match a whole area with a prefix (`event=~\"session\\..*\"`), past tense because\n// every event records something that already happened.\n//\n// The fields are deliberately low-cardinality except `sid` — see docs/session-events.md\n// for the dashboard/alerting contract.\n\nimport type { FidesLogger } from './logger';\n\n/**\n * Event names. Use these constants rather than string literals so a rename is a\n * compile error at every call site instead of a silently broken dashboard.\n */\nexport const SESSION_EVENT = {\n /** A session was created from a token response (login, or client-credentials grant). */\n CREATED: 'session.created',\n /** An existing session was renewed with a fresh access token. */\n REFRESHED: 'session.refreshed',\n /** A request could not proceed with the session it had. Carries a {@link SessionRejectedReason}. */\n REJECTED: 'session.rejected',\n /** A refresh attempt failed. Carries a {@link RefreshFailureCause}. */\n REFRESH_FAILED: 'session.refresh_failed',\n /** Session cookies were deleted. Carries a {@link SessionClearedTrigger}. */\n CLEARED: 'session.cleared',\n} as const;\n\nexport type SessionEventName = (typeof SESSION_EVENT)[keyof typeof SESSION_EVENT];\n\n/**\n * Why a session could not be used for a request.\n *\n * One vocabulary for the whole package, so a proxy, a status route and the\n * heartbeat endpoint all report the same word for the same situation.\n */\nexport type SessionRejectedReason =\n /** No session cookie was sent. The ordinary anonymous case. */\n | 'no_session_cookie'\n /** A session cookie was sent but did not decrypt or validate — corruption, or a rotated secret. */\n | 'unreadable_session'\n /** A pre-split single-cookie session. Dropped rather than migrated; costs one re-login. */\n | 'stale_legacy_session'\n /** The session decoded but carries no refresh token — usually `offline_access` was never granted. */\n | 'no_refresh_token'\n /** The refresh token was rejected by the provider. See {@link RefreshFailureCause}. */\n | 'refresh_failed';\n\n/**\n * Why a refresh attempt failed.\n *\n * The distinction that matters operationally: only `invalid_grant` means the\n * user must log in again. The other two are infrastructure faults where the\n * session is probably still fine, and logging the user out would be wrong.\n */\nexport type RefreshFailureCause =\n /** The provider rejected the refresh token: expired, revoked, or already used. Terminal. */\n | 'invalid_grant'\n /** The provider was never reached — DNS, TCP, TLS, timeout, abort. Retryable. */\n | 'transport'\n /** The provider was reached but answered with an error or something unparseable. Retryable. */\n | 'idp_error';\n\n/** What caused the session cookies to be deleted. */\nexport type SessionClearedTrigger =\n /** The user (or the app) logged out deliberately. */\n | 'logout'\n /** The session was unusable and cleaned up. */\n | 'rejected';\n\n/** Structured payload for one lifecycle event. */\nexport type SessionEvent =\n | {\n event: typeof SESSION_EVENT.CREATED;\n sid?: string;\n /** False here guarantees logouts at access-token TTL — the single most useful login-time signal. */\n hasRefreshToken: boolean;\n /** Scopes the provider actually granted, which is not necessarily what was requested. */\n scopes?: string[];\n /** Seconds until the access token expires. */\n expiresIn?: number;\n /** Total bytes written across the session cookies. Near ~4096 means a session about to break. */\n cookieBytes?: number;\n }\n | {\n event: typeof SESSION_EVENT.REFRESHED;\n sid?: string;\n expiresIn?: number;\n /** True when the provider issued a new refresh token. Rotation turns a lost cookie write into a dead session. */\n rotatedRefreshToken: boolean;\n cookieBytes?: number;\n }\n | {\n event: typeof SESSION_EVENT.REJECTED;\n sid?: string;\n reason: SessionRejectedReason;\n /** Which component made the call, e.g. 'heartbeat'. */\n source?: string;\n }\n | {\n event: typeof SESSION_EVENT.REFRESH_FAILED;\n sid?: string;\n cause: RefreshFailureCause;\n /** HTTP status from the provider, when it answered at all. */\n status?: number;\n /** Expiry of the access token we were trying to replace — how long the session had left. */\n accessTokenExpiresAt?: string;\n error?: unknown;\n }\n | {\n event: typeof SESSION_EVENT.CLEARED;\n sid?: string;\n trigger: SessionClearedTrigger;\n };\n\ntype Level = 'debug' | 'info' | 'warn' | 'error';\n\n/**\n * Level per reason rather than per event.\n *\n * A single level for the whole event would be wrong in both directions: an\n * anonymous request hitting a protected route is not a warning, and a session\n * cookie that won't decrypt is not routine. Getting this wrong is how a warn\n * stream becomes unreadable and operators conclude there is \"nothing in the logs\".\n */\nconst REJECTED_LEVEL: Record<SessionRejectedReason, Level> = {\n no_session_cookie: 'debug',\n stale_legacy_session: 'info',\n refresh_failed: 'info',\n unreadable_session: 'warn',\n no_refresh_token: 'warn',\n};\n\n/** Only `invalid_grant` is business as usual; the rest are someone's infrastructure failing. */\nconst REFRESH_FAILED_LEVEL: Record<RefreshFailureCause, Level> = {\n invalid_grant: 'info',\n transport: 'warn',\n idp_error: 'error',\n};\n\n/** Human-readable message per event; the machine-readable truth is in the fields. */\nconst MESSAGE: Record<SessionEventName, string> = {\n [SESSION_EVENT.CREATED]: 'Session created',\n [SESSION_EVENT.REFRESHED]: 'Session refreshed',\n [SESSION_EVENT.REJECTED]: 'Session rejected',\n [SESSION_EVENT.REFRESH_FAILED]: 'Session refresh failed',\n [SESSION_EVENT.CLEARED]: 'Session cleared',\n};\n\n/** Resolves the level for an event, applying the per-reason and per-cause policy. */\nexport function sessionEventLevel(payload: SessionEvent): Level {\n switch (payload.event) {\n case SESSION_EVENT.REJECTED:\n return REJECTED_LEVEL[payload.reason] ?? 'warn';\n case SESSION_EVENT.REFRESH_FAILED:\n return REFRESH_FAILED_LEVEL[payload.cause] ?? 'error';\n default:\n return 'info';\n }\n}\n\n/**\n * Emits one lifecycle event at the level the policy dictates.\n *\n * Call sites pass data, never a level or a message — that is what keeps the\n * vocabulary consistent across modules and makes a dashboard built on `event`\n * and `reason` keep working.\n */\nexport function logSessionEvent(logger: FidesLogger, payload: SessionEvent): void {\n const level = sessionEventLevel(payload);\n // Undefined fields are dropped so absent data doesn't render as `\"sid\":null`\n // and pollute label sets in the log backend.\n const fields = Object.fromEntries(\n Object.entries(payload).filter(([, value]) => value !== undefined),\n );\n logger[level](fields, MESSAGE[payload.event]);\n}\n\n\n/** The reasons, as a runtime list — for validating values that arrive over the wire. */\nexport const SESSION_REJECTED_REASONS = [\n 'no_session_cookie',\n 'unreadable_session',\n 'stale_legacy_session',\n 'no_refresh_token',\n 'refresh_failed',\n] as const satisfies readonly SessionRejectedReason[];\n\n/** Narrows an untrusted value (a response body, a query param) to a known reason. */\nexport function isSessionRejectedReason(value: unknown): value is SessionRejectedReason {\n return (\n typeof value === 'string' &&\n (SESSION_REJECTED_REASONS as readonly string[]).includes(value)\n );\n}\n\n/**\n * Browser-side counterpart of {@link SessionEvent}.\n *\n * Same names and same `reason` vocabulary as the server events, so a consumer\n * that beacons these to its own endpoint gets one queryable stream across both\n * halves of a session. The shapes differ where the browser genuinely knows\n * less: it never learns *why* a refresh failed server-side, only the status it\n * got back, so there is no `cause` here to invent.\n */\nexport type SessionClientEvent =\n | {\n event: typeof SESSION_EVENT.REFRESHED;\n source: 'heartbeat';\n /** Epoch ms of the new access-token expiry, or null when the server didn't say. */\n expiresAt: number | null;\n }\n | {\n event: typeof SESSION_EVENT.REJECTED;\n source: 'heartbeat' | 'session-monitor';\n /** The server's own reason when it sent one; absent when it didn't. */\n reason?: SessionRejectedReason;\n }\n | {\n event: typeof SESSION_EVENT.REFRESH_FAILED;\n source: 'heartbeat';\n /** HTTP status, when there was a response at all. Absent means the request never landed. */\n status?: number;\n /** 1 for the first consecutive failure, 2 for the next, and so on. */\n attempt: number;\n error?: string;\n };\n\n/**\n * Mints a session correlation id: 16 hex characters (64 bits).\n *\n * Deliberately random rather than derived from the refresh token. A hash of the\n * refresh token would change every time the provider rotates it — which is on\n * every refresh for a default Keycloak — and correlating a session's whole life\n * is the entire point. Rotation stays visible through\n * `rotatedRefreshToken` on {@link SESSION_EVENT.REFRESHED} instead.\n */\nexport function newSessionId(): string {\n const bytes = new Uint8Array(8);\n crypto.getRandomValues(bytes);\n return Array.from(bytes, (b) => b.toString(16).padStart(2, '0')).join('');\n}\n"],"mappings":";;;;;AAqBA,IAAa,gBAAgB;;CAE3B,SAAS;;CAET,WAAW;;CAEX,UAAU;;CAEV,gBAAgB;;CAEhB,SAAS;AACX;;;;;;;;;AAmGA,IAAM,iBAAuD;CAC3D,mBAAmB;CACnB,sBAAsB;CACtB,gBAAgB;CAChB,oBAAoB;CACpB,kBAAkB;AACpB;;AAGA,IAAM,uBAA2D;CAC/D,eAAe;CACf,WAAW;CACX,WAAW;AACb;;AAGA,IAAM,UAA4C;EAC/C,cAAc,UAAU;EACxB,cAAc,YAAY;EAC1B,cAAc,WAAW;EACzB,cAAc,iBAAiB;EAC/B,cAAc,UAAU;AAC3B;;AAGA,SAAgB,kBAAkB,SAA8B;CAC9D,QAAQ,QAAQ,OAAhB;EACE,KAAK,cAAc,UACjB,OAAO,eAAe,QAAQ,WAAW;EAC3C,KAAK,cAAc,gBACjB,OAAO,qBAAqB,QAAQ,UAAU;EAChD,SACE,OAAO;CACX;AACF;;;;;;;;AASA,SAAgB,gBAAgB,QAAqB,SAA6B;CAChF,MAAM,QAAQ,kBAAkB,OAAO;CAGvC,MAAM,SAAS,OAAO,YACpB,OAAO,QAAQ,OAAO,CAAC,CAAC,QAAQ,GAAG,WAAW,UAAU,KAAA,CAAS,CACnE;CACA,OAAO,MAAM,CAAC,QAAQ,QAAQ,QAAQ,MAAM;AAC9C;;AAIA,IAAa,2BAA2B;CACtC;CACA;CACA;CACA;CACA;AACF;;AAGA,SAAgB,wBAAwB,OAAgD;CACtF,OACE,OAAO,UAAU,YAChB,yBAA+C,SAAS,KAAK;AAElE;;;;;;;;;;AA2CA,SAAgB,eAAuB;CACrC,MAAM,wBAAQ,IAAI,WAAW,CAAC;CAC9B,OAAO,gBAAgB,KAAK;CAC5B,OAAO,MAAM,KAAK,QAAQ,MAAM,EAAE,SAAS,EAAE,CAAC,CAAC,SAAS,GAAG,GAAG,CAAC,CAAC,CAAC,KAAK,EAAE;AAC1E"}
|
|
1
|
+
{"version":3,"file":"session-events.js","names":[],"sources":["../src/session-events.ts"],"sourcesContent":["// session-events.ts\n//\n// The log vocabulary fides-auth emits for the session lifecycle. Every point\n// where the library creates a session, renews one, or decides one cannot\n// proceed emits exactly one event from this module — so a production operator\n// can answer \"which path ended this session?\" from logs alone, without reading\n// library source.\n//\n// Naming: `<area>.<outcome>`, dotted and past tense. Dotted so a dashboard can\n// match a whole area with a prefix (`event=~\"session\\..*\"`), past tense because\n// every event records something that already happened.\n//\n// The fields are deliberately low-cardinality except `sid` — see docs/session-events.md\n// for the dashboard/alerting contract.\n\nimport type { FidesLogger } from './logger';\n\n/**\n * Event names. Use these constants rather than string literals so a rename is a\n * compile error at every call site instead of a silently broken dashboard.\n */\nexport const SESSION_EVENT = {\n /** A session was created from a token response (login, or client-credentials grant). */\n CREATED: 'session.created',\n /** An existing session was renewed with a fresh access token. */\n REFRESHED: 'session.refreshed',\n /** A request could not proceed with the session it had. Carries a {@link SessionRejectedReason}. */\n REJECTED: 'session.rejected',\n /** A refresh attempt failed. Carries a {@link RefreshFailureCause}. */\n REFRESH_FAILED: 'session.refresh_failed',\n /** Session cookies were deleted. Carries a {@link SessionClearedTrigger}. */\n CLEARED: 'session.cleared',\n} as const;\n\nexport type SessionEventName = (typeof SESSION_EVENT)[keyof typeof SESSION_EVENT];\n\n/**\n * Why a session could not be used for a request.\n *\n * One vocabulary for the whole package, so a proxy, a status route and the\n * heartbeat endpoint all report the same word for the same situation.\n */\nexport type SessionRejectedReason =\n /** No session cookie was sent. The ordinary anonymous case. */\n | 'no_session_cookie'\n /** A session cookie was sent but did not decrypt or validate — corruption, or a rotated secret. */\n | 'unreadable_session'\n /** A pre-split single-cookie session. Dropped rather than migrated; costs one re-login. */\n | 'stale_legacy_session'\n /** The session decoded but carries no refresh token — usually `offline_access` was never granted. */\n | 'no_refresh_token'\n /** The refresh token was rejected by the provider. See {@link RefreshFailureCause}. */\n | 'refresh_failed'\n /** A handle was sent but the server store has no live record for it: revoked, expired or evicted. Server persistence only. */\n | 'session_not_found';\n\n/**\n * Why a refresh attempt failed.\n *\n * The distinction that matters operationally: only `invalid_grant` means the\n * user must log in again. The other two are infrastructure faults where the\n * session is probably still fine, and logging the user out would be wrong.\n */\nexport type RefreshFailureCause =\n /** The provider rejected the refresh token: expired, revoked, or already used. Terminal. */\n | 'invalid_grant'\n /** The provider was never reached — DNS, TCP, TLS, timeout, abort. Retryable. */\n | 'transport'\n /** The provider was reached but answered with an error or something unparseable. Retryable. */\n | 'idp_error';\n\n/** Where a session lives between requests. See `server/session-persistence.ts`. */\nexport type SessionPersistenceMode = 'cookie' | 'server';\n\n/** What caused the session cookies to be deleted. */\nexport type SessionClearedTrigger =\n /** The user (or the app) logged out deliberately. */\n | 'logout'\n /** The session was unusable and cleaned up. */\n | 'rejected';\n\n/** Structured payload for one lifecycle event. */\nexport type SessionEvent =\n | {\n event: typeof SESSION_EVENT.CREATED;\n sid?: string;\n /** False here guarantees logouts at access-token TTL — the single most useful login-time signal. */\n hasRefreshToken: boolean;\n /** Scopes the provider actually granted, which is not necessarily what was requested. */\n scopes?: string[];\n /** Seconds until the access token expires. */\n expiresIn?: number;\n /** Total bytes written across the session cookies. Near ~4096 means a session about to break. Absent under server persistence. */\n cookieBytes?: number;\n /** Where the session was written. Absent on events from before this field existed. */\n persistence?: SessionPersistenceMode;\n }\n | {\n event: typeof SESSION_EVENT.REFRESHED;\n sid?: string;\n expiresIn?: number;\n /** True when the provider issued a new refresh token. Rotation turns a lost cookie write into a dead session. */\n rotatedRefreshToken: boolean;\n cookieBytes?: number;\n persistence?: SessionPersistenceMode;\n }\n | {\n event: typeof SESSION_EVENT.REJECTED;\n sid?: string;\n reason: SessionRejectedReason;\n /** Which component made the call, e.g. 'heartbeat'. */\n source?: string;\n }\n | {\n event: typeof SESSION_EVENT.REFRESH_FAILED;\n sid?: string;\n cause: RefreshFailureCause;\n /** HTTP status from the provider, when it answered at all. */\n status?: number;\n /** Expiry of the access token we were trying to replace — how long the session had left. */\n accessTokenExpiresAt?: string;\n error?: unknown;\n }\n | {\n event: typeof SESSION_EVENT.CLEARED;\n sid?: string;\n trigger: SessionClearedTrigger;\n };\n\ntype Level = 'debug' | 'info' | 'warn' | 'error';\n\n/**\n * Level per reason rather than per event.\n *\n * A single level for the whole event would be wrong in both directions: an\n * anonymous request hitting a protected route is not a warning, and a session\n * cookie that won't decrypt is not routine. Getting this wrong is how a warn\n * stream becomes unreadable and operators conclude there is \"nothing in the logs\".\n */\nconst REJECTED_LEVEL: Record<SessionRejectedReason, Level> = {\n no_session_cookie: 'debug',\n stale_legacy_session: 'info',\n session_not_found: 'info',\n refresh_failed: 'info',\n unreadable_session: 'warn',\n no_refresh_token: 'warn',\n};\n\n/** Only `invalid_grant` is business as usual; the rest are someone's infrastructure failing. */\nconst REFRESH_FAILED_LEVEL: Record<RefreshFailureCause, Level> = {\n invalid_grant: 'info',\n transport: 'warn',\n idp_error: 'error',\n};\n\n/** Human-readable message per event; the machine-readable truth is in the fields. */\nconst MESSAGE: Record<SessionEventName, string> = {\n [SESSION_EVENT.CREATED]: 'Session created',\n [SESSION_EVENT.REFRESHED]: 'Session refreshed',\n [SESSION_EVENT.REJECTED]: 'Session rejected',\n [SESSION_EVENT.REFRESH_FAILED]: 'Session refresh failed',\n [SESSION_EVENT.CLEARED]: 'Session cleared',\n};\n\n/** Resolves the level for an event, applying the per-reason and per-cause policy. */\nexport function sessionEventLevel(payload: SessionEvent): Level {\n switch (payload.event) {\n case SESSION_EVENT.REJECTED:\n return REJECTED_LEVEL[payload.reason] ?? 'warn';\n case SESSION_EVENT.REFRESH_FAILED:\n return REFRESH_FAILED_LEVEL[payload.cause] ?? 'error';\n default:\n return 'info';\n }\n}\n\n/**\n * Emits one lifecycle event at the level the policy dictates.\n *\n * Call sites pass data, never a level or a message — that is what keeps the\n * vocabulary consistent across modules and makes a dashboard built on `event`\n * and `reason` keep working.\n */\nexport function logSessionEvent(logger: FidesLogger, payload: SessionEvent): void {\n const level = sessionEventLevel(payload);\n // Undefined fields are dropped so absent data doesn't render as `\"sid\":null`\n // and pollute label sets in the log backend.\n const fields = Object.fromEntries(\n Object.entries(payload).filter(([, value]) => value !== undefined),\n );\n logger[level](fields, MESSAGE[payload.event]);\n}\n\n\n/** The reasons, as a runtime list — for validating values that arrive over the wire. */\nexport const SESSION_REJECTED_REASONS = [\n 'no_session_cookie',\n 'unreadable_session',\n 'stale_legacy_session',\n 'no_refresh_token',\n 'refresh_failed',\n 'session_not_found',\n] as const satisfies readonly SessionRejectedReason[];\n\n/** Narrows an untrusted value (a response body, a query param) to a known reason. */\nexport function isSessionRejectedReason(value: unknown): value is SessionRejectedReason {\n return (\n typeof value === 'string' &&\n (SESSION_REJECTED_REASONS as readonly string[]).includes(value)\n );\n}\n\n/**\n * Browser-side counterpart of {@link SessionEvent}.\n *\n * Same names and same `reason` vocabulary as the server events, so a consumer\n * that beacons these to its own endpoint gets one queryable stream across both\n * halves of a session. The shapes differ where the browser genuinely knows\n * less: it never learns *why* a refresh failed server-side, only the status it\n * got back, so there is no `cause` here to invent.\n */\nexport type SessionClientEvent =\n | {\n event: typeof SESSION_EVENT.REFRESHED;\n source: 'heartbeat';\n /** Epoch ms of the new access-token expiry, or null when the server didn't say. */\n expiresAt: number | null;\n }\n | {\n event: typeof SESSION_EVENT.REJECTED;\n source: 'heartbeat' | 'session-monitor';\n /** The server's own reason when it sent one; absent when it didn't. */\n reason?: SessionRejectedReason;\n }\n | {\n event: typeof SESSION_EVENT.REFRESH_FAILED;\n source: 'heartbeat';\n /** HTTP status, when there was a response at all. Absent means the request never landed. */\n status?: number;\n /** 1 for the first consecutive failure, 2 for the next, and so on. */\n attempt: number;\n error?: string;\n };\n\n/**\n * Mints a session correlation id: 16 hex characters (64 bits).\n *\n * Deliberately random rather than derived from the refresh token. A hash of the\n * refresh token would change every time the provider rotates it — which is on\n * every refresh for a default Keycloak — and correlating a session's whole life\n * is the entire point. Rotation stays visible through\n * `rotatedRefreshToken` on {@link SESSION_EVENT.REFRESHED} instead.\n */\nexport function newSessionId(): string {\n const bytes = new Uint8Array(8);\n crypto.getRandomValues(bytes);\n return Array.from(bytes, (b) => b.toString(16).padStart(2, '0')).join('');\n}\n"],"mappings":";;;;;AAqBA,IAAa,gBAAgB;;CAE3B,SAAS;;CAET,WAAW;;CAEX,UAAU;;CAEV,gBAAgB;;CAEhB,SAAS;AACX;;;;;;;;;AA2GA,IAAM,iBAAuD;CAC3D,mBAAmB;CACnB,sBAAsB;CACtB,mBAAmB;CACnB,gBAAgB;CAChB,oBAAoB;CACpB,kBAAkB;AACpB;;AAGA,IAAM,uBAA2D;CAC/D,eAAe;CACf,WAAW;CACX,WAAW;AACb;;AAGA,IAAM,UAA4C;EAC/C,cAAc,UAAU;EACxB,cAAc,YAAY;EAC1B,cAAc,WAAW;EACzB,cAAc,iBAAiB;EAC/B,cAAc,UAAU;AAC3B;;AAGA,SAAgB,kBAAkB,SAA8B;CAC9D,QAAQ,QAAQ,OAAhB;EACE,KAAK,cAAc,UACjB,OAAO,eAAe,QAAQ,WAAW;EAC3C,KAAK,cAAc,gBACjB,OAAO,qBAAqB,QAAQ,UAAU;EAChD,SACE,OAAO;CACX;AACF;;;;;;;;AASA,SAAgB,gBAAgB,QAAqB,SAA6B;CAChF,MAAM,QAAQ,kBAAkB,OAAO;CAGvC,MAAM,SAAS,OAAO,YACpB,OAAO,QAAQ,OAAO,CAAC,CAAC,QAAQ,GAAG,WAAW,UAAU,KAAA,CAAS,CACnE;CACA,OAAO,MAAM,CAAC,QAAQ,QAAQ,QAAQ,MAAM;AAC9C;;AAIA,IAAa,2BAA2B;CACtC;CACA;CACA;CACA;CACA;CACA;AACF;;AAGA,SAAgB,wBAAwB,OAAgD;CACtF,OACE,OAAO,UAAU,YAChB,yBAA+C,SAAS,KAAK;AAElE;;;;;;;;;;AA2CA,SAAgB,eAAuB;CACrC,MAAM,wBAAQ,IAAI,WAAW,CAAC;CAC9B,OAAO,gBAAgB,KAAK;CAC5B,OAAO,MAAM,KAAK,QAAQ,MAAM,EAAE,SAAS,EAAE,CAAC,CAAC,SAAS,GAAG,GAAG,CAAC,CAAC,CAAC,KAAK,EAAE;AAC1E"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"session-refresh.d.ts","sourceRoot":"","sources":["../src/session-refresh.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"session-refresh.d.ts","sourceRoot":"","sources":["../src/session-refresh.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,gBAAgB,CAAC;AAElD,OAAO,EAAE,oBAAoB,EAAE,OAAO,EAAE,MAAM,SAAS,CAAC;AAIxD;;;;;;;;GAQG;AACH,eAAO,MAAM,cAAc,oBACR,OAAO,UAChB,WAAW,YACV,oBAAoB,KAC5B,OAAO,CAAC,OAAO,GAAG,IAAI,CAgDxB,CAAC"}
|
package/dist/session-refresh.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { createLogger } from "./logger.js";
|
|
2
2
|
import { getOAuthErrorLogContext } from "./oauth-logging.js";
|
|
3
3
|
import { refreshAccessToken } from "./oauth.js";
|
|
4
|
-
|
|
4
|
+
// src/session-refresh.ts
|
|
5
5
|
var logger = createLogger({ namespace: "fides-auth:session-refresh" });
|
|
6
6
|
/**
|
|
7
7
|
* Refreshes an existing session by exchanging its refresh token for new tokens.
|
|
@@ -43,7 +43,6 @@ var refreshSession = async (current_session, config, options = {}) => {
|
|
|
43
43
|
throw error;
|
|
44
44
|
}
|
|
45
45
|
};
|
|
46
|
-
//#endregion
|
|
47
46
|
export { refreshSession };
|
|
48
47
|
|
|
49
48
|
//# sourceMappingURL=session-refresh.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"session-refresh.js","names":[],"sources":["../src/session-refresh.ts"],"sourcesContent":["import { createLogger } from './logger';\nimport {
|
|
1
|
+
{"version":3,"file":"session-refresh.js","names":[],"sources":["../src/session-refresh.ts"],"sourcesContent":["import { createLogger } from './logger';\nimport { refreshAccessToken } from './oauth';\nimport type { OAuthConfig } from './oauth-config';\nimport { getOAuthErrorLogContext } from './oauth-logging';\nimport { CreateSessionOptions, Session } from \"./types\";\n\nconst logger = createLogger({ namespace: 'fides-auth:session-refresh' });\n\n/**\n * Refreshes an existing session by exchanging its refresh token for new tokens.\n *\n * @param current_session - The current session containing a refresh token\n * @param config - OAuth configuration for the identity provider\n * @param options - Optional session creation options\n * @returns The updated session with new tokens, or throws on failure\n * @throws Error if no refresh token is available or the token exchange fails\n */\nexport const refreshSession = async (\n current_session: Session,\n config: OAuthConfig,\n options: CreateSessionOptions = {}\n): Promise<Session | null> => {\n logger.debug('Starting session refresh');\n\n if (!current_session.tokens?.refreshToken) {\n logger.error('No refresh token available in session');\n throw new Error('No refresh token available');\n }\n\n try {\n const newtokens = await refreshAccessToken(config, current_session.tokens.refreshToken);\n\n const updatedSession = {\n ...current_session,\n tokens: {\n ...current_session.tokens,\n accessToken: newtokens.access_token,\n accessTokenExpiresAt: newtokens.expires_in\n ? new Date(Date.now() + newtokens.expires_in * 1000).toISOString()\n : undefined,\n refreshToken: newtokens.refresh_token ?? current_session.tokens?.refreshToken,\n // A refresh doesn't always return id_token, and a stale hint still works.\n idToken: newtokens.id_token ?? current_session.tokens?.idToken,\n },\n };\n\n logger.info({\n hasNewAccessToken: !!updatedSession.tokens.accessToken,\n hasNewRefreshToken: !!newtokens.refresh_token,\n expiresIn: newtokens.expires_in,\n }, 'Session refreshed successfully');\n\n return updatedSession;\n } catch (error) {\n // Check if this is an expected invalid_grant error\n const errorContext = getOAuthErrorLogContext(error);\n const isInvalidGrant =\n errorContext.code === 'OAUTH_RESPONSE_BODY_ERROR' && errorContext.error === 'invalid_grant';\n\n if (isInvalidGrant) {\n // Expected error during logout/session expiry - log at info level\n logger.info('Session refresh failed - refresh token expired or invalid');\n } else {\n // Unexpected error - log at error level\n logger.error({ error: errorContext }, 'Session refresh failed');\n }\n\n throw error;\n }\n};\n\n"],"mappings":";;;;AAMA,IAAM,SAAS,aAAa,EAAE,WAAW,6BAA6B,CAAC;;;;;;;;;;AAWvE,IAAa,iBAAiB,OAC5B,iBACA,QACA,UAAgC,CAAC,MACL;CAC5B,OAAO,MAAM,0BAA0B;CAEvC,IAAI,CAAC,gBAAgB,QAAQ,cAAc;EACzC,OAAO,MAAM,uCAAuC;EACpD,MAAM,IAAI,MAAM,4BAA4B;CAC9C;CAEA,IAAI;EACF,MAAM,YAAY,MAAM,mBAAmB,QAAQ,gBAAgB,OAAO,YAAY;EAEtF,MAAM,iBAAiB;GACrB,GAAG;GACH,QAAQ;IACN,GAAG,gBAAgB;IACnB,aAAa,UAAU;IACvB,sBAAsB,UAAU,aAC5B,IAAI,KAAK,KAAK,IAAI,IAAI,UAAU,aAAa,GAAI,CAAC,CAAC,YAAY,IAC/D,KAAA;IACJ,cAAc,UAAU,iBAAiB,gBAAgB,QAAQ;IAEjE,SAAS,UAAU,YAAY,gBAAgB,QAAQ;GACzD;EACF;EAEA,OAAO,KAAK;GACV,mBAAmB,CAAC,CAAC,eAAe,OAAO;GAC3C,oBAAoB,CAAC,CAAC,UAAU;GAChC,WAAW,UAAU;EACvB,GAAG,gCAAgC;EAEnC,OAAO;CACT,SAAS,OAAO;EAEd,MAAM,eAAe,wBAAwB,KAAK;EAIlD,IAFE,aAAa,SAAS,+BAA+B,aAAa,UAAU,iBAI5E,OAAO,KAAK,2DAA2D;OAGvE,OAAO,MAAM,EAAE,OAAO,aAAa,GAAG,wBAAwB;EAGhE,MAAM;CACR;AACF"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"session-validation.d.ts","sourceRoot":"","sources":["../src/session-validation.ts"],"names":[],"mappings":"AAMA,OAAO,EAAE,OAAO,EAAE,MAAM,SAAS,CAAC;AAIlC,qDAAqD;AACrD,MAAM,WAAW,uBAAuB;IACtC,qCAAqC;IACrC,OAAO,CAAC,EAAE,OAAO,CAAC;IAClB,+EAA+E;IAC/E,MAAM,EAAE,OAAO,GAAG,SAAS,GAAG,SAAS,CAAC;IACxC,+EAA+E;IAC/E,oBAAoB,CAAC,EAAE,MAAM,CAAC;IAC9B,sDAAsD;IACtD,MAAM,CAAC,EAAE,MAAM,CAAC;CACjB;AAED;;;;;;;GAOG;AACH,wBAAsB,kBAAkB,CACtC,YAAY,EAAE,MAAM,EACpB,MAAM,EAAE,MAAM,GAAG,UAAU,GAC1B,OAAO,CAAC,uBAAuB,CAAC,CAgDlC;AAyBD,6FAA6F;AAC7F,wBAAgB,SAAS,CAAC,MAAM,EAAE,OAAO,GAAG,MAAM,IAAI,OAAO,
|
|
1
|
+
{"version":3,"file":"session-validation.d.ts","sourceRoot":"","sources":["../src/session-validation.ts"],"names":[],"mappings":"AAMA,OAAO,EAAE,OAAO,EAAE,MAAM,SAAS,CAAC;AAIlC,qDAAqD;AACrD,MAAM,WAAW,uBAAuB;IACtC,qCAAqC;IACrC,OAAO,CAAC,EAAE,OAAO,CAAC;IAClB,+EAA+E;IAC/E,MAAM,EAAE,OAAO,GAAG,SAAS,GAAG,SAAS,CAAC;IACxC,+EAA+E;IAC/E,oBAAoB,CAAC,EAAE,MAAM,CAAC;IAC9B,sDAAsD;IACtD,MAAM,CAAC,EAAE,MAAM,CAAC;CACjB;AAED;;;;;;;GAOG;AACH,wBAAsB,kBAAkB,CACtC,YAAY,EAAE,MAAM,EACpB,MAAM,EAAE,MAAM,GAAG,UAAU,GAC1B,OAAO,CAAC,uBAAuB,CAAC,CAgDlC;AAyBD,6FAA6F;AAC7F,wBAAgB,SAAS,CAAC,MAAM,EAAE,OAAO,GAAG,MAAM,IAAI,OAAO,CAW5D"}
|