@eventuras/fides-auth 0.14.0 → 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.
Files changed (99) hide show
  1. package/README.md +225 -33
  2. package/dist/activity-tracker.d.ts +1 -1
  3. package/dist/activity-tracker.d.ts.map +1 -1
  4. package/dist/activity-tracker.js +1 -2
  5. package/dist/cookies.d.ts +6 -0
  6. package/dist/cookies.d.ts.map +1 -1
  7. package/dist/cookies.js +8 -3
  8. package/dist/cookies.js.map +1 -1
  9. package/dist/heartbeat.d.ts +1 -1
  10. package/dist/heartbeat.js +1 -2
  11. package/dist/id-token.d.ts +31 -0
  12. package/dist/id-token.d.ts.map +1 -0
  13. package/dist/id-token.js +60 -0
  14. package/dist/id-token.js.map +1 -0
  15. package/dist/index.js +3 -3
  16. package/dist/logger.js +1 -2
  17. package/dist/oauth-browser.d.ts +13 -10
  18. package/dist/oauth-browser.d.ts.map +1 -1
  19. package/dist/oauth-browser.js +4 -2
  20. package/dist/oauth-browser.js.map +1 -1
  21. package/dist/oauth-config.d.ts +70 -0
  22. package/dist/oauth-config.d.ts.map +1 -0
  23. package/dist/oauth-logging.d.ts +6 -5
  24. package/dist/oauth-logging.d.ts.map +1 -1
  25. package/dist/oauth-logging.js +5 -5
  26. package/dist/oauth-logging.js.map +1 -1
  27. package/dist/oauth.d.ts +48 -46
  28. package/dist/oauth.d.ts.map +1 -1
  29. package/dist/oauth.js +58 -29
  30. package/dist/oauth.js.map +1 -1
  31. package/dist/oidc-discovery.d.ts +13 -4
  32. package/dist/oidc-discovery.d.ts.map +1 -1
  33. package/dist/oidc-discovery.js +82 -15
  34. package/dist/oidc-discovery.js.map +1 -1
  35. package/dist/pkce.d.ts +10 -0
  36. package/dist/pkce.d.ts.map +1 -0
  37. package/dist/pkce.js +23 -0
  38. package/dist/pkce.js.map +1 -0
  39. package/dist/providers/vipps/client.d.ts +4 -3
  40. package/dist/providers/vipps/client.d.ts.map +1 -1
  41. package/dist/providers/vipps/index.js +10 -10
  42. package/dist/providers/vipps/index.js.map +1 -1
  43. package/dist/providers/vipps/types.d.ts +10 -10
  44. package/dist/providers/vipps/types.d.ts.map +1 -1
  45. package/dist/rate-limit.d.ts.map +1 -1
  46. package/dist/rate-limit.js +1 -2
  47. package/dist/server/authorized-fetch.d.ts +78 -0
  48. package/dist/server/authorized-fetch.d.ts.map +1 -0
  49. package/dist/server/cookie-store.d.ts +1 -1
  50. package/dist/server/heartbeat-handler.d.ts +7 -5
  51. package/dist/server/heartbeat-handler.d.ts.map +1 -1
  52. package/dist/server/index.d.ts +4 -0
  53. package/dist/server/index.d.ts.map +1 -1
  54. package/dist/server/index.js +744 -94
  55. package/dist/server/index.js.map +1 -1
  56. package/dist/server/login-transaction.d.ts +55 -0
  57. package/dist/server/login-transaction.d.ts.map +1 -0
  58. package/dist/server/oidc-callback.d.ts +14 -5
  59. package/dist/server/oidc-callback.d.ts.map +1 -1
  60. package/dist/server/oidc-login.d.ts +15 -6
  61. package/dist/server/oidc-login.d.ts.map +1 -1
  62. package/dist/server/oidc-logout.d.ts +7 -5
  63. package/dist/server/oidc-logout.d.ts.map +1 -1
  64. package/dist/server/session-persistence.d.ts +102 -0
  65. package/dist/server/session-persistence.d.ts.map +1 -0
  66. package/dist/server/session-store.d.ts +39 -0
  67. package/dist/server/session-store.d.ts.map +1 -0
  68. package/dist/server/session.d.ts +38 -31
  69. package/dist/server/session.d.ts.map +1 -1
  70. package/dist/session-cookies.d.ts +2 -1
  71. package/dist/session-cookies.d.ts.map +1 -1
  72. package/dist/session-cookies.js +4 -3
  73. package/dist/session-cookies.js.map +1 -1
  74. package/dist/session-events.d.ts +16 -9
  75. package/dist/session-events.d.ts.map +1 -1
  76. package/dist/session-events.js +4 -3
  77. package/dist/session-events.js.map +1 -1
  78. package/dist/session-refresh.d.ts +2 -2
  79. package/dist/session-refresh.d.ts.map +1 -1
  80. package/dist/session-refresh.js +1 -2
  81. package/dist/session-refresh.js.map +1 -1
  82. package/dist/session-validation.d.ts.map +1 -1
  83. package/dist/session-validation.js +9 -6
  84. package/dist/session-validation.js.map +1 -1
  85. package/dist/silent-login.d.ts +14 -5
  86. package/dist/silent-login.d.ts.map +1 -1
  87. package/dist/silent-login.js +3 -3
  88. package/dist/silent-login.js.map +1 -1
  89. package/dist/types.d.ts +4 -1
  90. package/dist/types.d.ts.map +1 -1
  91. package/dist/utils.d.ts +1 -15
  92. package/dist/utils.d.ts.map +1 -1
  93. package/dist/utils.js +2 -24
  94. package/dist/utils.js.map +1 -1
  95. package/docs/discovery-cache.md +7 -3
  96. package/docs/rp-initiated-logout.md +6 -3
  97. package/docs/server-sessions.md +154 -0
  98. package/docs/session-events.md +17 -16
  99. package/package.json +18 -10
@@ -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"}
@@ -1,34 +1,33 @@
1
- import { OAuthConfig } from '../oauth';
2
- import { SESSION_EVENT, RefreshFailureCause, SessionClearedTrigger, SessionRejectedReason } from '../session-events';
3
- import { CreateSessionOptions, Session } from '../types';
4
- import { CookieStore } from './cookie-store';
5
- type Secret = string | Uint8Array;
1
+ import type { OAuthConfig } from '../oauth-config';
2
+ import { SESSION_EVENT, type RefreshFailureCause, type SessionClearedTrigger, type SessionRejectedReason } from '../session-events';
3
+ import type { CreateSessionOptions, Session } from '../types';
4
+ import type { CookieStore } from './cookie-store';
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
- * Encrypts a session and writes it across the "session", "session_at" and
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
- * @returns The encrypted JWT stored in the main "session" cookie.
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, secret: Secret, options?: PersistSessionOptions): Promise<string>;
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, secret: Secret): Promise<ReadSessionResult>;
39
+ export declare function tryReadSession(store: CookieStore, persistence: SessionSecret | SessionPersistence): Promise<ReadSessionResult>;
41
40
  /**
42
- * Reads and reassembles the current session from the cookie store, or null when
43
- * there is none. Decode/validation rules live in `decodeSessionCookies`;
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, secret: Secret): Promise<Session | null>;
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, secret: Secret, options?: CreateSessionOptions): Promise<RefreshSessionResult>;
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, secret: Secret, options?: CreateSessionOptions): Promise<Session | null>;
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 the session cookies — logout needs
91
- * the hint even when {@link readSession} returns null.
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, secret: Secret): Promise<string | undefined>;
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 cookies are going away. Omit to clear without raising an event. */
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
- /** Deletes all session cookies. */
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":"AAqBA,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,UAAU,CAAC;AAG5C,OAAO,EACL,aAAa,EAEb,KAAK,mBAAmB,EACxB,KAAK,qBAAqB,EAC1B,KAAK,qBAAqB,EAC3B,MAAM,mBAAmB,CAAC;AAM3B,OAAO,KAAK,EAAE,oBAAoB,EAAE,OAAO,EAAE,MAAM,UAAU,CAAC;AAC9D,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,gBAAgB,CAAC;AAIlD,KAAK,MAAM,GAAG,MAAM,GAAG,UAAU,CAAC;AAqBlC,mEAAmE;AACnE,MAAM,WAAW,qBAAqB;IACpC;;;;OAIG;IACH,KAAK,CAAC,EAAE,OAAO,aAAa,CAAC,OAAO,GAAG,OAAO,aAAa,CAAC,SAAS,CAAC;IACtE,uGAAuG;IACvG,mBAAmB,CAAC,EAAE,OAAO,CAAC;CAC/B;AAED;;;;;GAKG;AACH,wBAAsB,cAAc,CAClC,KAAK,EAAE,WAAW,EAClB,OAAO,EAAE,OAAO,EAChB,MAAM,EAAE,MAAM,EACd,OAAO,GAAE,qBAA0B,GAClC,OAAO,CAAC,MAAM,CAAC,CA6CjB;AAED,4DAA4D;AAC5D,MAAM,MAAM,iBAAiB,GACzB;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;;;;;;;GAOG;AACH,wBAAsB,cAAc,CAAC,KAAK,EAAE,WAAW,EAAE,MAAM,EAAE,MAAM,GAAG,OAAO,CAAC,iBAAiB,CAAC,CAmBnG;AAED;;;;;;GAMG;AACH,wBAAsB,WAAW,CAAC,KAAK,EAAE,WAAW,EAAE,MAAM,EAAE,MAAM,GAAG,OAAO,CAAC,OAAO,GAAG,IAAI,CAAC,CAE7F;AAED,oCAAoC;AACpC,MAAM,MAAM,oBAAoB,GAC5B;IAAE,EAAE,EAAE,IAAI,CAAC;IAAC,OAAO,EAAE,OAAO,CAAC;IAAC,mBAAmB,EAAE,OAAO,CAAA;CAAE,GAC5D;IACA,EAAE,EAAE,KAAK,CAAC;IACV,MAAM,EAAE,qBAAqB,CAAC;IAC9B;;;OAGG;IACH,GAAG,CAAC,EAAE,MAAM,CAAC;IACb;;;;OAIG;IACH,KAAK,CAAC,EAAE,mBAAmB,CAAC;CAC7B,CAAC;AAEJ;;;;;;;;GAQG;AACH,wBAAsB,wBAAwB,CAC5C,KAAK,EAAE,WAAW,EAClB,MAAM,EAAE,WAAW,EACnB,MAAM,EAAE,MAAM,EACd,OAAO,GAAE,oBAAyB,GACjC,OAAO,CAAC,oBAAoB,CAAC,CA8C/B;AAED;;;;;;;;GAQG;AACH,wBAAsB,qBAAqB,CACzC,KAAK,EAAE,WAAW,EAClB,MAAM,EAAE,WAAW,EACnB,MAAM,EAAE,MAAM,EACd,OAAO,GAAE,oBAAyB,GACjC,OAAO,CAAC,OAAO,GAAG,IAAI,CAAC,CAGzB;AAED;;;GAGG;AACH,wBAAsB,WAAW,CAAC,KAAK,EAAE,WAAW,EAAE,MAAM,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,GAAG,SAAS,CAAC,CAQjG;AAED,0FAA0F;AAC1F,MAAM,WAAW,mBAAmB;IAClC,8EAA8E;IAC9E,OAAO,CAAC,EAAE,qBAAqB,CAAC;IAChC,2EAA2E;IAC3E,GAAG,CAAC,EAAE,MAAM,CAAC;CACd;AAED,mCAAmC;AACnC,wBAAsB,YAAY,CAChC,KAAK,EAAE,WAAW,EAClB,OAAO,GAAE,mBAAwB,GAChC,OAAO,CAAC,IAAI,CAAC,CAYf"}
1
+ {"version":3,"file":"session.d.ts","sourceRoot":"","sources":["../../src/server/session.ts"],"names":[],"mappings":"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"}
@@ -1,4 +1,4 @@
1
- import { SessionRejectedReason } from './session-events';
1
+ import type { SessionRejectedReason } from './session-events';
2
2
  import { Session } from './types';
3
3
  /** Encrypted cookie values produced from a session. */
4
4
  export interface EncodedSessionCookies {
@@ -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;;;;;;GAMG;AACH,wBAAsB,oBAAoB,CACxC,OAAO,EAAE,OAAO,EAChB,MAAM,EAAE,MAAM,GAAG,UAAU,GAC1B,OAAO,CAAC,qBAAqB,CAAC,CAmBhC;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"}
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"}
@@ -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
- //#region src/session-cookies.ts
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;;;;;;;;AAgCvE,eAAsB,qBACpB,SACA,QACgC;CAChC,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"}
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"}
@@ -1,19 +1,19 @@
1
- import { FidesLogger } from './logger';
1
+ import type { FidesLogger } from './logger';
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.
5
5
  */
6
6
  export declare const SESSION_EVENT: {
7
7
  /** A session was created from a token response (login, or client-credentials grant). */
8
- readonly CREATED: "session.created";
8
+ readonly CREATED: 'session.created';
9
9
  /** An existing session was renewed with a fresh access token. */
10
- readonly REFRESHED: "session.refreshed";
10
+ readonly REFRESHED: 'session.refreshed';
11
11
  /** A request could not proceed with the session it had. Carries a {@link SessionRejectedReason}. */
12
- readonly REJECTED: "session.rejected";
12
+ readonly REJECTED: 'session.rejected';
13
13
  /** A refresh attempt failed. Carries a {@link RefreshFailureCause}. */
14
- readonly REFRESH_FAILED: "session.refresh_failed";
14
+ readonly REFRESH_FAILED: 'session.refresh_failed';
15
15
  /** Session cookies were deleted. Carries a {@link SessionClearedTrigger}. */
16
- readonly CLEARED: "session.cleared";
16
+ readonly CLEARED: 'session.cleared';
17
17
  };
18
18
  export type SessionEventName = (typeof SESSION_EVENT)[keyof typeof SESSION_EVENT];
19
19
  /**
@@ -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":"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"}
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;AAClB,8HAA8H;GAC5H,mBAAmB,CAAC;AAExB;;;;;;GAMG;AACH,MAAM,MAAM,mBAAmB;AAC7B,4FAA4F;AAC1F,eAAe;AACjB,iFAAiF;GAC/E,WAAW;AACb,+FAA+F;GAC7F,WAAW,CAAC;AAEhB,mFAAmF;AACnF,MAAM,MAAM,sBAAsB,GAAG,QAAQ,GAAG,QAAQ,CAAC;AAEzD,qDAAqD;AACrD,MAAM,MAAM,qBAAqB;AAC/B,qDAAqD;AACnD,QAAQ;AACV,+CAA+C;GAC7C,UAAU,CAAC;AAEf,kDAAkD;AAClD,MAAM,MAAM,YAAY,GACpB;IACA,KAAK,EAAE,OAAO,aAAa,CAAC,OAAO,CAAC;IACpC,GAAG,CAAC,EAAE,MAAM,CAAC;IACb,oGAAoG;IACpG,eAAe,EAAE,OAAO,CAAC;IACzB,yFAAyF;IACzF,MAAM,CAAC,EAAE,MAAM,EAAE,CAAC;IAClB,8CAA8C;IAC9C,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,kIAAkI;IAClI,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,sFAAsF;IACtF,WAAW,CAAC,EAAE,sBAAsB,CAAC;CACtC,GACC;IACA,KAAK,EAAE,OAAO,aAAa,CAAC,SAAS,CAAC;IACtC,GAAG,CAAC,EAAE,MAAM,CAAC;IACb,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,iHAAiH;IACjH,mBAAmB,EAAE,OAAO,CAAC;IAC7B,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,WAAW,CAAC,EAAE,sBAAsB,CAAC;CACtC,GACC;IACA,KAAK,EAAE,OAAO,aAAa,CAAC,QAAQ,CAAC;IACrC,GAAG,CAAC,EAAE,MAAM,CAAC;IACb,MAAM,EAAE,qBAAqB,CAAC;IAC9B,uDAAuD;IACvD,MAAM,CAAC,EAAE,MAAM,CAAC;CACjB,GACC;IACA,KAAK,EAAE,OAAO,aAAa,CAAC,cAAc,CAAC;IAC3C,GAAG,CAAC,EAAE,MAAM,CAAC;IACb,KAAK,EAAE,mBAAmB,CAAC;IAC3B,8DAA8D;IAC9D,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,4FAA4F;IAC5F,oBAAoB,CAAC,EAAE,MAAM,CAAC;IAC9B,KAAK,CAAC,EAAE,OAAO,CAAC;CACjB,GACC;IACA,KAAK,EAAE,OAAO,aAAa,CAAC,OAAO,CAAC;IACpC,GAAG,CAAC,EAAE,MAAM,CAAC;IACb,OAAO,EAAE,qBAAqB,CAAC;CAChC,CAAC;AAEJ,KAAK,KAAK,GAAG,OAAO,GAAG,MAAM,GAAG,MAAM,GAAG,OAAO,CAAC;AAmCjD,qFAAqF;AACrF,wBAAgB,iBAAiB,CAAC,OAAO,EAAE,YAAY,GAAG,KAAK,CAS9D;AAED;;;;;;GAMG;AACH,wBAAgB,eAAe,CAAC,MAAM,EAAE,WAAW,EAAE,OAAO,EAAE,YAAY,GAAG,IAAI,CAQhF;AAGD,wFAAwF;AACxF,eAAO,MAAM,wBAAwB,yIAOgB,CAAC;AAEtD,qFAAqF;AACrF,wBAAgB,uBAAuB,CAAC,KAAK,EAAE,OAAO,GAAG,KAAK,IAAI,qBAAqB,CAKtF;AAED;;;;;;;;GAQG;AACH,MAAM,MAAM,kBAAkB,GAC1B;IACA,KAAK,EAAE,OAAO,aAAa,CAAC,SAAS,CAAC;IACtC,MAAM,EAAE,WAAW,CAAC;IACpB,mFAAmF;IACnF,SAAS,EAAE,MAAM,GAAG,IAAI,CAAC;CAC1B,GACC;IACA,KAAK,EAAE,OAAO,aAAa,CAAC,QAAQ,CAAC;IACrC,MAAM,EAAE,WAAW,GAAG,iBAAiB,CAAC;IACxC,uEAAuE;IACvE,MAAM,CAAC,EAAE,qBAAqB,CAAC;CAChC,GACC;IACA,KAAK,EAAE,OAAO,aAAa,CAAC,cAAc,CAAC;IAC3C,MAAM,EAAE,WAAW,CAAC;IACpB,4FAA4F;IAC5F,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,sEAAsE;IACtE,OAAO,EAAE,MAAM,CAAC;IAChB,KAAK,CAAC,EAAE,MAAM,CAAC;CAChB,CAAC;AAEJ;;;;;;;;GAQG;AACH,wBAAgB,YAAY,IAAI,MAAM,CAIrC"}
@@ -1,4 +1,4 @@
1
- //#region src/session-events.ts
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,5 +1,5 @@
1
- import { OAuthConfig } from './oauth';
2
- import { CreateSessionOptions, Session } from './types';
1
+ import type { OAuthConfig } from './oauth-config';
2
+ import { CreateSessionOptions, Session } from "./types";
3
3
  /**
4
4
  * Refreshes an existing session by exchanging its refresh token for new tokens.
5
5
  *
@@ -1 +1 @@
1
- {"version":3,"file":"session-refresh.d.ts","sourceRoot":"","sources":["../src/session-refresh.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,WAAW,EAAsB,MAAM,SAAS,CAAC;AAE1D,OAAO,EAAE,oBAAoB,EAAE,OAAO,EAAE,MAAM,SAAS,CAAC;AAIxD;;;;;;;;GAQG;AACH,eAAO,MAAM,cAAc,GACzB,iBAAiB,OAAO,EACxB,QAAQ,WAAW,EACnB,UAAS,oBAAyB,KACjC,OAAO,CAAC,OAAO,GAAG,IAAI,CAgDxB,CAAC"}
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"}
@@ -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
- //#region src/session-refresh.ts
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