@devcoffee/nuxt-core 1.3.0 → 1.4.1

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/CHANGELOG.md CHANGED
@@ -1,5 +1,39 @@
1
1
  # Changelog
2
2
 
3
+ ## v1.4.1
4
+
5
+ [compare changes](https://github.com/coolkg1412/devcoffee-nuxt-core/compare/v1.4.0...v1.4.1)
6
+
7
+ ### 🩹 Fixes
8
+
9
+ - Normalize expiresIn to seconds across all session consumers ([2c197ab](https://github.com/coolkg1412/devcoffee-nuxt-core/commit/2c197ab))
10
+
11
+ ### ❤️ Contributors
12
+
13
+ - Hieu Nguyen <hieu.nguyen@devcoffee.tech>
14
+
15
+ ## v1.4.0
16
+
17
+ [compare changes](https://github.com/coolkg1412/devcoffee-nuxt-core/compare/v1.3.0...v1.4.0)
18
+
19
+ ### 🚀 Enhancements
20
+
21
+ - Implement writeSessionCookie function and integrate it into NuxtForwardRequestHandler and authts plugin ([e761f6c](https://github.com/coolkg1412/devcoffee-nuxt-core/commit/e761f6c))
22
+
23
+ ### 🩹 Fixes
24
+
25
+ - Drop return from proxyRequest in NuxtForwardRequestHandler to prevent ERR_HTTP_HEADERS_SENT ([fd3b91c](https://github.com/coolkg1412/devcoffee-nuxt-core/commit/fd3b91c))
26
+ - Prevent sending headers after response has been finalized in beforeResponse hook ([444a012](https://github.com/coolkg1412/devcoffee-nuxt-core/commit/444a012))
27
+ - Update version to 1.3.2 in package.json and package-lock.json ([e6ab449](https://github.com/coolkg1412/devcoffee-nuxt-core/commit/e6ab449))
28
+
29
+ ### 💅 Refactors
30
+
31
+ - Remove redundant signSessionId checks from authts.sec03.test.ts ([18686f4](https://github.com/coolkg1412/devcoffee-nuxt-core/commit/18686f4))
32
+
33
+ ### ❤️ Contributors
34
+
35
+ - Hieu Nguyen <hieu.nguyen@devcoffee.tech>
36
+
3
37
  ## v1.3.0
4
38
 
5
39
  [compare changes](https://github.com/coolkg1412/devcoffee-nuxt-core/compare/v1.2.5...v1.3.0)
package/dist/module.d.mts CHANGED
@@ -3,249 +3,255 @@ import { CookieSerializeOptions } from '../dist/runtime/server/adapters/http.js'
3
3
  import { OidcUserInfo } from '../dist/runtime/server/adapters/oidc.js';
4
4
  import { LogLevel as LogLevel$1, ConsolaOptions, ConsolaInstance } from 'consola';
5
5
 
6
- interface AuthorizedUser {
7
- id: string
8
- sub: string
9
- email: string
10
- firstName: string
11
- lastName: string
12
- locale: string
13
- language: string
14
- timezone: string
15
- }
16
-
17
- type AuthStatus = 'unauthenticated' | 'authenticated'
18
-
19
- type AuthData = {
20
- status: AuthStatus
21
- tokenSet?: {
22
- accessToken: string
23
- tokenType: string
24
- expiresAt: number
25
- idToken: string
26
- refreshToken: string
27
- scopes: string[]
28
- }
29
- }
30
-
31
- type SessionContext<TData = Record<string, unknown>> = {
32
- id: string
33
- auth: AuthData
34
- user: AuthorizedUser
35
- data: TData
36
- issuedAt: number
37
- expiresAt: number
38
- }
39
-
40
- type NuxtAuthOptions = {
41
- /**
42
- * Default Devcoffee Nuxt AuthTS option callbacks for session and user mapping.
43
- * These can be overridden by passing custom callbacks to `NuxtAuthtsHandler()`.
44
- * @since 1.0.0
45
- */
46
- session: (
47
- session: Omit<SessionContext, 'auth'>,
48
- auth: SessionContext['auth']
49
- ) => Awaitable<Omit<SessionContext, 'auth' | 'issuedAt' | 'expiresAt'> & { isAuthenticated: boolean }>
50
-
51
- /**
52
- * Maps the OpenID Connect user info response to your local user schema.
53
- *
54
- * @param user - The raw user info response from the OpenID provider.
55
- * @param opts - The token response containing access and ID tokens.
56
- * @returns A normalized user object.
57
- * @since 1.0.0
58
- */
59
- userInfo: (
60
- user: AuthorizedUser,
61
- opts: {
62
- openidUser?: OidcUserInfo
63
- tokenSet: Exclude<SessionContext['auth']['tokenSet'], undefined>
64
- }
65
- ) => Awaitable<AuthorizedUser>
66
- }
67
-
68
- type NuxtSessionContext<TData = Record<string, unknown>> = {
69
- readonly id: string
70
- readonly isAuthenticated: boolean
71
- readonly user: AuthorizedUser
72
- readonly data: TData
73
- }
74
-
75
- type NuxtSessionUpdateContext<TData = Record<string, unknown>> = DeepPartial<{
76
- user: AuthorizedUser
77
- data: TData
78
- }>
79
-
80
- /**
81
- * ⚙️ OpenID Connect configuration options.
82
- *
83
- * Defines parameters used for client registration, discovery,
84
- * and token exchange with the OpenID Provider.
85
- *
86
- * @since 1.0.0
87
- */
88
- type OpenIdOptions = {
89
- /** Client ID registered with the OpenID Provider. */
90
- clientId: string
91
-
92
- /** Client secret for token exchange (if applicable). */
93
- clientSecret: string
94
-
95
- /** Whether to use Proof Key for Code Exchange (PKCE) flow. */
96
- usePkce: boolean
97
-
98
- /** Code challenge method, e.g. `S256`. */
99
- codeChallengeMethod: string
100
-
101
- /** Scopes requested from the OpenID Provider. */
102
- scopes: string[]
103
-
104
- /** Redirect URI used after authorization. */
105
- redirectUri: string
106
-
107
- /** The `.well-known/openid-configuration` discovery URL. */
108
- wellKnownUrl: string
109
-
110
- /** Whether to automatically fetch user info after login. */
111
- autoFetchUser: boolean
112
-
113
- /**
114
- * TTL in seconds for caching autoFetchUser results per session.
115
- * A GET_SESSION within the TTL window does not trigger an OIDC provider call.
116
- * Only applies when autoFetchUser is true.
117
- * @default 300
118
- * @since 1.0.0
119
- */
120
- autoFetchUserTtl: number
121
-
122
- /** Whether to fetch user info immediately after token grant. */
123
- fetchUserOnLogin: boolean
124
-
125
- /**
126
- * Window in milliseconds before token expiry at which a refresh is triggered.
127
- * Tokens with expiresAt more than this many milliseconds in the future are
128
- * considered fresh and the refresh path is skipped entirely.
129
- * @default 60000
130
- * @since 1.0.0
131
- */
132
- tokenRefreshBufferMs: number
133
-
134
- /**
135
- * When `true`, the token refresh mutex uses an atomic Redis NX write
136
- * (`SET key value NX EX ttl`) via the IORedis native client accessor for
137
- * true distributed exclusion across multiple server instances. When `false`
138
- * (default), the existing optimistic lock (`hasItem` + `setItem`) is used.
139
- *
140
- * @remarks Only effective when the Nitro `cache` storage mount is backed by
141
- * a Redis driver. Setting `true` with a non-Redis backend falls through to
142
- * the optimistic path silently.
143
- *
144
- * @default false
145
- * @since 1.0.0
146
- */
147
- distributedLock?: boolean
148
-
149
- /** Cache configuration for storing OpenID metadata. */
150
- cache: {
151
- /** Cache key prefix for the metadata store. */
152
- prefix: string
153
- /** Expiry time in seconds for cached metadata. */
154
- expires: number
155
- }
156
- }
157
-
158
- /**
159
- * 🍪 Session management configuration.
160
- *
161
- * Defines session storage, expiration, and cookie behavior
162
- * for authentication session tracking.
163
- *
164
- * @since 1.0.0
165
- */
166
- type SessionsOptions = {
167
- /** Cookie name mappings used by the session handler. */
168
- names: {
169
- /** PKCE verifier cookie name. */
170
- pkce: string
171
- /** State cookie name. */
172
- state: string
173
- /** Session ID cookie name. */
174
- sessionId: string
175
- /** Redirect URL cookie name. */
176
- redirectUrl: string
177
- }
178
-
179
- storage: {
180
- name: string
181
-
182
- /** Key prefix for session data in the storage backend. */
183
- prefix: string
184
- }
185
-
186
- /** Session lifetime in seconds. */
187
- expiresIn: number
188
-
189
- /** Options for how session cookies are serialized. */
190
- cookieOpts: CookieSerializeOptions
191
-
192
- /**
193
- * Secret used to HMAC-sign session ID cookies and derive the AES-256-GCM key
194
- * for tokenSet encryption. When empty, signing and encryption are skipped.
195
- * @since 1.0.0
196
- */
197
- secret?: string
198
- }
199
-
200
- /**
201
- * 🔐 Authentication behavior configuration.
202
- *
203
- * Defines default redirect paths and URIs used for login/logout flow.
204
- *
205
- * @since 1.0.0
206
- */
207
- type AuthOptions = {
208
- /** Path to the login page. */
209
- loginUri: string
210
-
211
- /** Default redirect URI after successful login. */
212
- defaultLoginRedirectUri: string
213
-
214
- /** Default redirect URI after logout. */
215
- defaultLogoutRedirectUri: string
216
-
217
- /** Default anonymous user object. */
218
- anonymousUser: AuthorizedUser
219
-
220
- ignoreRegexPatterns: RegExp[]
221
-
222
- ignoreRegexPatternsDev: RegExp[]
223
- }
224
-
225
- /**
226
- * 🧩 Main configuration interface for the Nuxt AuthTS module.
227
- *
228
- * Combines OpenID Connect, session, and general authentication options.
229
- *
230
- * @since 1.0.0
231
- */
232
- type AuthtsModuleOptions = {
233
- /** Enable or disable the module */
234
- enabled: boolean
235
- openid: OpenIdOptions
236
- sessions: SessionsOptions
237
- auth: AuthOptions
238
- }
239
-
240
- interface AuthtsMiddlewareMeta {
241
- /** Require authentication to access */
242
- required?: boolean
243
-
244
- /** Only accessible if unauthenticated (login/register pages) */
245
- unauthenticatedOnly?: boolean
246
-
247
- /** Restrict access to these roles */
248
- roles?: string[]
6
+ interface AuthorizedUser {
7
+ id: string
8
+ sub: string
9
+ email: string
10
+ firstName: string
11
+ lastName: string
12
+ locale: string
13
+ language: string
14
+ timezone: string
15
+ }
16
+
17
+ type AuthStatus = 'unauthenticated' | 'authenticated'
18
+
19
+ type AuthData = {
20
+ status: AuthStatus
21
+ tokenSet?: {
22
+ accessToken: string
23
+ tokenType: string
24
+ expiresAt: number
25
+ idToken: string
26
+ refreshToken: string
27
+ scopes: string[]
28
+ }
29
+ }
30
+
31
+ type SessionContext<TData = Record<string, unknown>> = {
32
+ id: string
33
+ auth: AuthData
34
+ user: AuthorizedUser
35
+ data: TData
36
+ issuedAt: number
37
+ expiresAt: number
38
+ }
39
+
40
+ type NuxtAuthOptions = {
41
+ /**
42
+ * Default Devcoffee Nuxt AuthTS option callbacks for session and user mapping.
43
+ * These can be overridden by passing custom callbacks to `NuxtAuthtsHandler()`.
44
+ * @since 1.0.0
45
+ */
46
+ session: (
47
+ session: Omit<SessionContext, 'auth'>,
48
+ auth: SessionContext['auth']
49
+ ) => Awaitable<Omit<SessionContext, 'auth' | 'issuedAt' | 'expiresAt'> & { isAuthenticated: boolean }>
50
+
51
+ /**
52
+ * Maps the OpenID Connect user info response to your local user schema.
53
+ *
54
+ * @param user - The raw user info response from the OpenID provider.
55
+ * @param opts - The token response containing access and ID tokens.
56
+ * @returns A normalized user object.
57
+ * @since 1.0.0
58
+ */
59
+ userInfo: (
60
+ user: AuthorizedUser,
61
+ opts: {
62
+ openidUser?: OidcUserInfo
63
+ tokenSet: Exclude<SessionContext['auth']['tokenSet'], undefined>
64
+ }
65
+ ) => Awaitable<AuthorizedUser>
66
+ }
67
+
68
+ type NuxtSessionContext<TData = Record<string, unknown>> = {
69
+ readonly id: string
70
+ readonly isAuthenticated: boolean
71
+ readonly user: AuthorizedUser
72
+ readonly data: TData
73
+ }
74
+
75
+ type NuxtSessionUpdateContext<TData = Record<string, unknown>> = DeepPartial<{
76
+ user: AuthorizedUser
77
+ data: TData
78
+ }>
79
+
80
+ /**
81
+ * OpenID Connect configuration options.
82
+ *
83
+ * Defines parameters used for client registration, discovery,
84
+ * and token exchange with the OpenID Provider.
85
+ *
86
+ * @since 1.0.0
87
+ */
88
+ type OpenIdOptions = {
89
+ /** Client ID registered with the OpenID Provider. */
90
+ clientId: string
91
+
92
+ /** Client secret for token exchange (if applicable). */
93
+ clientSecret: string
94
+
95
+ /** Whether to use Proof Key for Code Exchange (PKCE) flow. */
96
+ usePkce: boolean
97
+
98
+ /** Code challenge method, e.g. `S256`. */
99
+ codeChallengeMethod: string
100
+
101
+ /** Scopes requested from the OpenID Provider. */
102
+ scopes: string[]
103
+
104
+ /** Redirect URI used after authorization. */
105
+ redirectUri: string
106
+
107
+ /** The `.well-known/openid-configuration` discovery URL. */
108
+ wellKnownUrl: string
109
+
110
+ /** Whether to automatically fetch user info after login. */
111
+ autoFetchUser: boolean
112
+
113
+ /**
114
+ * TTL in seconds for caching autoFetchUser results per session.
115
+ * A GET_SESSION within the TTL window does not trigger an OIDC provider call.
116
+ * Only applies when autoFetchUser is true.
117
+ * @default 300
118
+ * @since 1.0.0
119
+ */
120
+ autoFetchUserTtl: number
121
+
122
+ /** Whether to fetch user info immediately after token grant. */
123
+ fetchUserOnLogin: boolean
124
+
125
+ /**
126
+ * Window in milliseconds before token expiry at which a refresh is triggered.
127
+ * Tokens with expiresAt more than this many milliseconds in the future are
128
+ * considered fresh and the refresh path is skipped entirely.
129
+ * @default 60000
130
+ * @since 1.0.0
131
+ */
132
+ tokenRefreshBufferMs: number
133
+
134
+ /**
135
+ * When `true`, the token refresh mutex uses an atomic Redis NX write
136
+ * (`SET key value NX EX ttl`) via the IORedis native client accessor for
137
+ * true distributed exclusion across multiple server instances. When `false`
138
+ * (default), the existing optimistic lock (`hasItem` + `setItem`) is used.
139
+ *
140
+ * @remarks Only effective when the Nitro `cache` storage mount is backed by
141
+ * a Redis driver. Setting `true` with a non-Redis backend falls through to
142
+ * the optimistic path silently.
143
+ *
144
+ * @default false
145
+ * @since 1.0.0
146
+ */
147
+ distributedLock?: boolean
148
+
149
+ /** Cache configuration for storing OpenID metadata. */
150
+ cache: {
151
+ /** Cache key prefix for the metadata store. */
152
+ prefix: string
153
+ /** Expiry time in seconds for cached metadata. */
154
+ expires: number
155
+ }
156
+ }
157
+
158
+ /**
159
+ * Session management configuration.
160
+ *
161
+ * Defines session storage, expiration, and cookie behavior
162
+ * for authentication session tracking.
163
+ *
164
+ * @since 1.0.0
165
+ */
166
+ type SessionsOptions = {
167
+ /** Cookie name mappings used by the session handler. */
168
+ names: {
169
+ /** PKCE verifier cookie name. */
170
+ pkce: string
171
+ /** State cookie name. */
172
+ state: string
173
+ /** Session ID cookie name. */
174
+ sessionId: string
175
+ /** Redirect URL cookie name. */
176
+ redirectUrl: string
177
+ }
178
+
179
+ storage: {
180
+ name: string
181
+
182
+ /** Key prefix for session data in the storage backend. */
183
+ prefix: string
184
+ }
185
+
186
+ /**
187
+ * Session lifetime in seconds.
188
+ * Converted to milliseconds internally for Date.now() expiry calculations.
189
+ * Passed directly as Redis TTL (seconds).
190
+ * @default 518400 (6 days)
191
+ * @since 1.0.0
192
+ */
193
+ expiresIn: number
194
+
195
+ /** Options for how session cookies are serialized. */
196
+ cookieOpts: CookieSerializeOptions
197
+
198
+ /**
199
+ * Secret used to HMAC-sign session ID cookies and derive the AES-256-GCM key
200
+ * for tokenSet encryption. When empty, signing and encryption are skipped.
201
+ * @since 1.0.0
202
+ */
203
+ secret?: string
204
+ }
205
+
206
+ /**
207
+ * Authentication behavior configuration.
208
+ *
209
+ * Defines default redirect paths and URIs used for login/logout flow.
210
+ *
211
+ * @since 1.0.0
212
+ */
213
+ type AuthOptions = {
214
+ /** Path to the login page. */
215
+ loginUri: string
216
+
217
+ /** Default redirect URI after successful login. */
218
+ defaultLoginRedirectUri: string
219
+
220
+ /** Default redirect URI after logout. */
221
+ defaultLogoutRedirectUri: string
222
+
223
+ /** Default anonymous user object. */
224
+ anonymousUser: AuthorizedUser
225
+
226
+ ignoreRegexPatterns: RegExp[]
227
+
228
+ ignoreRegexPatternsDev: RegExp[]
229
+ }
230
+
231
+ /**
232
+ * Main configuration interface for the Nuxt AuthTS module.
233
+ *
234
+ * Combines OpenID Connect, session, and general authentication options.
235
+ *
236
+ * @since 1.0.0
237
+ */
238
+ type AuthtsModuleOptions = {
239
+ /** Enable or disable the module */
240
+ enabled: boolean
241
+ openid: OpenIdOptions
242
+ sessions: SessionsOptions
243
+ auth: AuthOptions
244
+ }
245
+
246
+ interface AuthtsMiddlewareMeta {
247
+ /** Require authentication to access */
248
+ required?: boolean
249
+
250
+ /** Only accessible if unauthenticated (login/register pages) */
251
+ unauthenticatedOnly?: boolean
252
+
253
+ /** Restrict access to these roles */
254
+ roles?: string[]
249
255
  }
250
256
 
251
257
  type CoreLogLevel = LogLevel$1
package/dist/module.d.ts CHANGED
@@ -3,249 +3,255 @@ import { CookieSerializeOptions } from '../dist/runtime/server/adapters/http.js'
3
3
  import { OidcUserInfo } from '../dist/runtime/server/adapters/oidc.js';
4
4
  import { LogLevel as LogLevel$1, ConsolaOptions, ConsolaInstance } from 'consola';
5
5
 
6
- interface AuthorizedUser {
7
- id: string
8
- sub: string
9
- email: string
10
- firstName: string
11
- lastName: string
12
- locale: string
13
- language: string
14
- timezone: string
15
- }
16
-
17
- type AuthStatus = 'unauthenticated' | 'authenticated'
18
-
19
- type AuthData = {
20
- status: AuthStatus
21
- tokenSet?: {
22
- accessToken: string
23
- tokenType: string
24
- expiresAt: number
25
- idToken: string
26
- refreshToken: string
27
- scopes: string[]
28
- }
29
- }
30
-
31
- type SessionContext<TData = Record<string, unknown>> = {
32
- id: string
33
- auth: AuthData
34
- user: AuthorizedUser
35
- data: TData
36
- issuedAt: number
37
- expiresAt: number
38
- }
39
-
40
- type NuxtAuthOptions = {
41
- /**
42
- * Default Devcoffee Nuxt AuthTS option callbacks for session and user mapping.
43
- * These can be overridden by passing custom callbacks to `NuxtAuthtsHandler()`.
44
- * @since 1.0.0
45
- */
46
- session: (
47
- session: Omit<SessionContext, 'auth'>,
48
- auth: SessionContext['auth']
49
- ) => Awaitable<Omit<SessionContext, 'auth' | 'issuedAt' | 'expiresAt'> & { isAuthenticated: boolean }>
50
-
51
- /**
52
- * Maps the OpenID Connect user info response to your local user schema.
53
- *
54
- * @param user - The raw user info response from the OpenID provider.
55
- * @param opts - The token response containing access and ID tokens.
56
- * @returns A normalized user object.
57
- * @since 1.0.0
58
- */
59
- userInfo: (
60
- user: AuthorizedUser,
61
- opts: {
62
- openidUser?: OidcUserInfo
63
- tokenSet: Exclude<SessionContext['auth']['tokenSet'], undefined>
64
- }
65
- ) => Awaitable<AuthorizedUser>
66
- }
67
-
68
- type NuxtSessionContext<TData = Record<string, unknown>> = {
69
- readonly id: string
70
- readonly isAuthenticated: boolean
71
- readonly user: AuthorizedUser
72
- readonly data: TData
73
- }
74
-
75
- type NuxtSessionUpdateContext<TData = Record<string, unknown>> = DeepPartial<{
76
- user: AuthorizedUser
77
- data: TData
78
- }>
79
-
80
- /**
81
- * ⚙️ OpenID Connect configuration options.
82
- *
83
- * Defines parameters used for client registration, discovery,
84
- * and token exchange with the OpenID Provider.
85
- *
86
- * @since 1.0.0
87
- */
88
- type OpenIdOptions = {
89
- /** Client ID registered with the OpenID Provider. */
90
- clientId: string
91
-
92
- /** Client secret for token exchange (if applicable). */
93
- clientSecret: string
94
-
95
- /** Whether to use Proof Key for Code Exchange (PKCE) flow. */
96
- usePkce: boolean
97
-
98
- /** Code challenge method, e.g. `S256`. */
99
- codeChallengeMethod: string
100
-
101
- /** Scopes requested from the OpenID Provider. */
102
- scopes: string[]
103
-
104
- /** Redirect URI used after authorization. */
105
- redirectUri: string
106
-
107
- /** The `.well-known/openid-configuration` discovery URL. */
108
- wellKnownUrl: string
109
-
110
- /** Whether to automatically fetch user info after login. */
111
- autoFetchUser: boolean
112
-
113
- /**
114
- * TTL in seconds for caching autoFetchUser results per session.
115
- * A GET_SESSION within the TTL window does not trigger an OIDC provider call.
116
- * Only applies when autoFetchUser is true.
117
- * @default 300
118
- * @since 1.0.0
119
- */
120
- autoFetchUserTtl: number
121
-
122
- /** Whether to fetch user info immediately after token grant. */
123
- fetchUserOnLogin: boolean
124
-
125
- /**
126
- * Window in milliseconds before token expiry at which a refresh is triggered.
127
- * Tokens with expiresAt more than this many milliseconds in the future are
128
- * considered fresh and the refresh path is skipped entirely.
129
- * @default 60000
130
- * @since 1.0.0
131
- */
132
- tokenRefreshBufferMs: number
133
-
134
- /**
135
- * When `true`, the token refresh mutex uses an atomic Redis NX write
136
- * (`SET key value NX EX ttl`) via the IORedis native client accessor for
137
- * true distributed exclusion across multiple server instances. When `false`
138
- * (default), the existing optimistic lock (`hasItem` + `setItem`) is used.
139
- *
140
- * @remarks Only effective when the Nitro `cache` storage mount is backed by
141
- * a Redis driver. Setting `true` with a non-Redis backend falls through to
142
- * the optimistic path silently.
143
- *
144
- * @default false
145
- * @since 1.0.0
146
- */
147
- distributedLock?: boolean
148
-
149
- /** Cache configuration for storing OpenID metadata. */
150
- cache: {
151
- /** Cache key prefix for the metadata store. */
152
- prefix: string
153
- /** Expiry time in seconds for cached metadata. */
154
- expires: number
155
- }
156
- }
157
-
158
- /**
159
- * 🍪 Session management configuration.
160
- *
161
- * Defines session storage, expiration, and cookie behavior
162
- * for authentication session tracking.
163
- *
164
- * @since 1.0.0
165
- */
166
- type SessionsOptions = {
167
- /** Cookie name mappings used by the session handler. */
168
- names: {
169
- /** PKCE verifier cookie name. */
170
- pkce: string
171
- /** State cookie name. */
172
- state: string
173
- /** Session ID cookie name. */
174
- sessionId: string
175
- /** Redirect URL cookie name. */
176
- redirectUrl: string
177
- }
178
-
179
- storage: {
180
- name: string
181
-
182
- /** Key prefix for session data in the storage backend. */
183
- prefix: string
184
- }
185
-
186
- /** Session lifetime in seconds. */
187
- expiresIn: number
188
-
189
- /** Options for how session cookies are serialized. */
190
- cookieOpts: CookieSerializeOptions
191
-
192
- /**
193
- * Secret used to HMAC-sign session ID cookies and derive the AES-256-GCM key
194
- * for tokenSet encryption. When empty, signing and encryption are skipped.
195
- * @since 1.0.0
196
- */
197
- secret?: string
198
- }
199
-
200
- /**
201
- * 🔐 Authentication behavior configuration.
202
- *
203
- * Defines default redirect paths and URIs used for login/logout flow.
204
- *
205
- * @since 1.0.0
206
- */
207
- type AuthOptions = {
208
- /** Path to the login page. */
209
- loginUri: string
210
-
211
- /** Default redirect URI after successful login. */
212
- defaultLoginRedirectUri: string
213
-
214
- /** Default redirect URI after logout. */
215
- defaultLogoutRedirectUri: string
216
-
217
- /** Default anonymous user object. */
218
- anonymousUser: AuthorizedUser
219
-
220
- ignoreRegexPatterns: RegExp[]
221
-
222
- ignoreRegexPatternsDev: RegExp[]
223
- }
224
-
225
- /**
226
- * 🧩 Main configuration interface for the Nuxt AuthTS module.
227
- *
228
- * Combines OpenID Connect, session, and general authentication options.
229
- *
230
- * @since 1.0.0
231
- */
232
- type AuthtsModuleOptions = {
233
- /** Enable or disable the module */
234
- enabled: boolean
235
- openid: OpenIdOptions
236
- sessions: SessionsOptions
237
- auth: AuthOptions
238
- }
239
-
240
- interface AuthtsMiddlewareMeta {
241
- /** Require authentication to access */
242
- required?: boolean
243
-
244
- /** Only accessible if unauthenticated (login/register pages) */
245
- unauthenticatedOnly?: boolean
246
-
247
- /** Restrict access to these roles */
248
- roles?: string[]
6
+ interface AuthorizedUser {
7
+ id: string
8
+ sub: string
9
+ email: string
10
+ firstName: string
11
+ lastName: string
12
+ locale: string
13
+ language: string
14
+ timezone: string
15
+ }
16
+
17
+ type AuthStatus = 'unauthenticated' | 'authenticated'
18
+
19
+ type AuthData = {
20
+ status: AuthStatus
21
+ tokenSet?: {
22
+ accessToken: string
23
+ tokenType: string
24
+ expiresAt: number
25
+ idToken: string
26
+ refreshToken: string
27
+ scopes: string[]
28
+ }
29
+ }
30
+
31
+ type SessionContext<TData = Record<string, unknown>> = {
32
+ id: string
33
+ auth: AuthData
34
+ user: AuthorizedUser
35
+ data: TData
36
+ issuedAt: number
37
+ expiresAt: number
38
+ }
39
+
40
+ type NuxtAuthOptions = {
41
+ /**
42
+ * Default Devcoffee Nuxt AuthTS option callbacks for session and user mapping.
43
+ * These can be overridden by passing custom callbacks to `NuxtAuthtsHandler()`.
44
+ * @since 1.0.0
45
+ */
46
+ session: (
47
+ session: Omit<SessionContext, 'auth'>,
48
+ auth: SessionContext['auth']
49
+ ) => Awaitable<Omit<SessionContext, 'auth' | 'issuedAt' | 'expiresAt'> & { isAuthenticated: boolean }>
50
+
51
+ /**
52
+ * Maps the OpenID Connect user info response to your local user schema.
53
+ *
54
+ * @param user - The raw user info response from the OpenID provider.
55
+ * @param opts - The token response containing access and ID tokens.
56
+ * @returns A normalized user object.
57
+ * @since 1.0.0
58
+ */
59
+ userInfo: (
60
+ user: AuthorizedUser,
61
+ opts: {
62
+ openidUser?: OidcUserInfo
63
+ tokenSet: Exclude<SessionContext['auth']['tokenSet'], undefined>
64
+ }
65
+ ) => Awaitable<AuthorizedUser>
66
+ }
67
+
68
+ type NuxtSessionContext<TData = Record<string, unknown>> = {
69
+ readonly id: string
70
+ readonly isAuthenticated: boolean
71
+ readonly user: AuthorizedUser
72
+ readonly data: TData
73
+ }
74
+
75
+ type NuxtSessionUpdateContext<TData = Record<string, unknown>> = DeepPartial<{
76
+ user: AuthorizedUser
77
+ data: TData
78
+ }>
79
+
80
+ /**
81
+ * OpenID Connect configuration options.
82
+ *
83
+ * Defines parameters used for client registration, discovery,
84
+ * and token exchange with the OpenID Provider.
85
+ *
86
+ * @since 1.0.0
87
+ */
88
+ type OpenIdOptions = {
89
+ /** Client ID registered with the OpenID Provider. */
90
+ clientId: string
91
+
92
+ /** Client secret for token exchange (if applicable). */
93
+ clientSecret: string
94
+
95
+ /** Whether to use Proof Key for Code Exchange (PKCE) flow. */
96
+ usePkce: boolean
97
+
98
+ /** Code challenge method, e.g. `S256`. */
99
+ codeChallengeMethod: string
100
+
101
+ /** Scopes requested from the OpenID Provider. */
102
+ scopes: string[]
103
+
104
+ /** Redirect URI used after authorization. */
105
+ redirectUri: string
106
+
107
+ /** The `.well-known/openid-configuration` discovery URL. */
108
+ wellKnownUrl: string
109
+
110
+ /** Whether to automatically fetch user info after login. */
111
+ autoFetchUser: boolean
112
+
113
+ /**
114
+ * TTL in seconds for caching autoFetchUser results per session.
115
+ * A GET_SESSION within the TTL window does not trigger an OIDC provider call.
116
+ * Only applies when autoFetchUser is true.
117
+ * @default 300
118
+ * @since 1.0.0
119
+ */
120
+ autoFetchUserTtl: number
121
+
122
+ /** Whether to fetch user info immediately after token grant. */
123
+ fetchUserOnLogin: boolean
124
+
125
+ /**
126
+ * Window in milliseconds before token expiry at which a refresh is triggered.
127
+ * Tokens with expiresAt more than this many milliseconds in the future are
128
+ * considered fresh and the refresh path is skipped entirely.
129
+ * @default 60000
130
+ * @since 1.0.0
131
+ */
132
+ tokenRefreshBufferMs: number
133
+
134
+ /**
135
+ * When `true`, the token refresh mutex uses an atomic Redis NX write
136
+ * (`SET key value NX EX ttl`) via the IORedis native client accessor for
137
+ * true distributed exclusion across multiple server instances. When `false`
138
+ * (default), the existing optimistic lock (`hasItem` + `setItem`) is used.
139
+ *
140
+ * @remarks Only effective when the Nitro `cache` storage mount is backed by
141
+ * a Redis driver. Setting `true` with a non-Redis backend falls through to
142
+ * the optimistic path silently.
143
+ *
144
+ * @default false
145
+ * @since 1.0.0
146
+ */
147
+ distributedLock?: boolean
148
+
149
+ /** Cache configuration for storing OpenID metadata. */
150
+ cache: {
151
+ /** Cache key prefix for the metadata store. */
152
+ prefix: string
153
+ /** Expiry time in seconds for cached metadata. */
154
+ expires: number
155
+ }
156
+ }
157
+
158
+ /**
159
+ * Session management configuration.
160
+ *
161
+ * Defines session storage, expiration, and cookie behavior
162
+ * for authentication session tracking.
163
+ *
164
+ * @since 1.0.0
165
+ */
166
+ type SessionsOptions = {
167
+ /** Cookie name mappings used by the session handler. */
168
+ names: {
169
+ /** PKCE verifier cookie name. */
170
+ pkce: string
171
+ /** State cookie name. */
172
+ state: string
173
+ /** Session ID cookie name. */
174
+ sessionId: string
175
+ /** Redirect URL cookie name. */
176
+ redirectUrl: string
177
+ }
178
+
179
+ storage: {
180
+ name: string
181
+
182
+ /** Key prefix for session data in the storage backend. */
183
+ prefix: string
184
+ }
185
+
186
+ /**
187
+ * Session lifetime in seconds.
188
+ * Converted to milliseconds internally for Date.now() expiry calculations.
189
+ * Passed directly as Redis TTL (seconds).
190
+ * @default 518400 (6 days)
191
+ * @since 1.0.0
192
+ */
193
+ expiresIn: number
194
+
195
+ /** Options for how session cookies are serialized. */
196
+ cookieOpts: CookieSerializeOptions
197
+
198
+ /**
199
+ * Secret used to HMAC-sign session ID cookies and derive the AES-256-GCM key
200
+ * for tokenSet encryption. When empty, signing and encryption are skipped.
201
+ * @since 1.0.0
202
+ */
203
+ secret?: string
204
+ }
205
+
206
+ /**
207
+ * Authentication behavior configuration.
208
+ *
209
+ * Defines default redirect paths and URIs used for login/logout flow.
210
+ *
211
+ * @since 1.0.0
212
+ */
213
+ type AuthOptions = {
214
+ /** Path to the login page. */
215
+ loginUri: string
216
+
217
+ /** Default redirect URI after successful login. */
218
+ defaultLoginRedirectUri: string
219
+
220
+ /** Default redirect URI after logout. */
221
+ defaultLogoutRedirectUri: string
222
+
223
+ /** Default anonymous user object. */
224
+ anonymousUser: AuthorizedUser
225
+
226
+ ignoreRegexPatterns: RegExp[]
227
+
228
+ ignoreRegexPatternsDev: RegExp[]
229
+ }
230
+
231
+ /**
232
+ * Main configuration interface for the Nuxt AuthTS module.
233
+ *
234
+ * Combines OpenID Connect, session, and general authentication options.
235
+ *
236
+ * @since 1.0.0
237
+ */
238
+ type AuthtsModuleOptions = {
239
+ /** Enable or disable the module */
240
+ enabled: boolean
241
+ openid: OpenIdOptions
242
+ sessions: SessionsOptions
243
+ auth: AuthOptions
244
+ }
245
+
246
+ interface AuthtsMiddlewareMeta {
247
+ /** Require authentication to access */
248
+ required?: boolean
249
+
250
+ /** Only accessible if unauthenticated (login/register pages) */
251
+ unauthenticatedOnly?: boolean
252
+
253
+ /** Restrict access to these roles */
254
+ roles?: string[]
249
255
  }
250
256
 
251
257
  type CoreLogLevel = LogLevel$1
package/dist/module.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "nuxt-core",
3
- "version": "1.3.0",
3
+ "version": "1.4.1",
4
4
  "configKey": "nuxtCore",
5
5
  "compatibility": {
6
6
  "nuxt": "^4.0.0"
package/dist/module.mjs CHANGED
@@ -2,7 +2,7 @@ import { addCustomTab } from '@nuxt/devtools-kit';
2
2
  import { defineNuxtModule, useLogger, createResolver, addTemplate, addServerImports, addServerImportsDir, addServerPlugin, addImportsDir, addPlugin, addRouteMiddleware, addServerHandler } from '@nuxt/kit';
3
3
  import { deepMerge, pick } from '../dist/runtime/utils.js';
4
4
 
5
- const version = "1.3.0";
5
+ const version = "1.4.1";
6
6
 
7
7
  const defaultLocale = "vi-VN";
8
8
  const defaultLanguage = "vi";
@@ -50,7 +50,7 @@ const authtsDefaults = {
50
50
  },
51
51
  storage: { name: "sessions", prefix: "session" },
52
52
  expiresIn: 60 * 60 * 24 * 6,
53
- // 6 days
53
+ // 6 days in seconds
54
54
  cookieOpts: {
55
55
  path: "/",
56
56
  sameSite: "lax",
@@ -1,4 +1,5 @@
1
1
  import type { SessionContext } from '@devcoffee/nuxt-core';
2
+ import type { CookieSerializeOptions, H3Event } from '#devcoffee-core/server/adapters/http';
2
3
  import type { ClientMetadata, ServerMetadata, TokenEndpointResponse } from '#devcoffee-core/server/adapters/oidc';
3
4
  /** Options used for session creation and validation. */
4
5
  type SessionCreateOptions = {
@@ -19,6 +20,28 @@ type SessionCreateOptions = {
19
20
  * @since 1.0.0
20
21
  */
21
22
  export declare function isSameOrigin(redirectUrl: string, requestUrl: URL): boolean;
23
+ /**
24
+ * Write the session cookie to the response.
25
+ *
26
+ * Shared by the `beforeResponse` Nitro hook (buffered responses) and the
27
+ * `proxyRequest` `onResponse` callback (streaming proxy responses). Calling
28
+ * it in both places ensures the cookie is always refreshed regardless of
29
+ * whether `headersSent` is true by the time `beforeResponse` fires.
30
+ *
31
+ * @param event - The H3 event for the current request.
32
+ * @param session - The resolved session context to encode into the cookie.
33
+ * @param sessionsConfig - Session cookie options from `nuxtCore.authts.sessions`.
34
+ * @since 1.3.3
35
+ */
36
+ export declare function writeSessionCookie(event: H3Event, session: SessionContext, sessionsConfig: {
37
+ cookieOpts: Omit<CookieSerializeOptions, 'sameSite'> & {
38
+ sameSite?: string | boolean;
39
+ };
40
+ secret?: string;
41
+ names: {
42
+ sessionId: string;
43
+ };
44
+ }): void;
22
45
  /**
23
46
  * Retrieve an existing session from storage.
24
47
  *
@@ -48,7 +71,7 @@ export declare function validateSession(sessionCookieId: string | undefined, opt
48
71
  *
49
72
  * @param sessionId - The session ID.
50
73
  * @param input - Partial update fields for the session (excluding ID, issuedAt, expiresAt).
51
- * @param opts - Expiration configuration (in milliseconds).
74
+ * @param opts - Expiration configuration (expiresIn in seconds).
52
75
  * @returns The updated {@link SessionContext}.
53
76
  * @throws {Error} If session cannot be found in storage.
54
77
  * @since 1.0.0
@@ -1,4 +1,4 @@
1
- import { createError } from "#devcoffee-core/server/adapters/http";
1
+ import { createError, setCookie } from "#devcoffee-core/server/adapters/http";
2
2
  import {
3
3
  allowInsecureRequests,
4
4
  authorizationCodeGrant as authorizationCodeGrantOidc,
@@ -22,7 +22,14 @@ import { deepMerge, omit } from "#devcoffee-core/server/adapters/utils";
22
22
  import useServerLogger from "#devcoffee-core/server/composables/useServerLogger";
23
23
  import { useRuntimeConfig } from "#imports";
24
24
  import { useStorage } from "nitropack/runtime";
25
- import { decryptTokenSet, encryptTokenSet, generateSessionId, isValidSessionId, verifySessionId } from "./crypto.js";
25
+ import {
26
+ decryptTokenSet,
27
+ encryptTokenSet,
28
+ generateSessionId,
29
+ isValidSessionId,
30
+ signSessionId,
31
+ verifySessionId
32
+ } from "./crypto.js";
26
33
  import { tryAcquireLock } from "./mutex.js";
27
34
  function getAnonymousUser(extras) {
28
35
  const anonymous = useRuntimeConfig().nuxtCore.authts.auth.anonymousUser;
@@ -56,6 +63,18 @@ export function isSameOrigin(redirectUrl, requestUrl) {
56
63
  return false;
57
64
  }
58
65
  }
66
+ export function writeSessionCookie(event, session, sessionsConfig) {
67
+ const {
68
+ cookieOpts,
69
+ secret = "",
70
+ names: { sessionId: cookieName }
71
+ } = sessionsConfig;
72
+ const cookieValue = secret ? signSessionId(session.id, secret) : session.id;
73
+ setCookie(event, cookieName, cookieValue, {
74
+ ...cookieOpts,
75
+ expires: new Date(session.expiresAt)
76
+ });
77
+ }
59
78
  export async function getSession(sessionId, opts) {
60
79
  const sessingKey = getSessionStorageKey(opts.storagePrefix, sessionId);
61
80
  if (!await hasSessionData(opts.storageName, sessingKey)) return null;
@@ -87,7 +106,7 @@ function newSession(sessionId, expiresAt) {
87
106
  }
88
107
  export async function validateSession(sessionCookieId, opts) {
89
108
  const now = Date.now();
90
- const expiresAt = now + opts.expiresIn;
109
+ const expiresAt = now + opts.expiresIn * 1e3;
91
110
  let sessionId = void 0;
92
111
  let sessionKey = void 0;
93
112
  let deleteSessionKey = void 0;
@@ -114,7 +133,7 @@ export async function validateSession(sessionCookieId, opts) {
114
133
  if (deleteSessionKey && deleteSessionKey !== sessionKey) {
115
134
  await removeSessionData(opts.storageName, deleteSessionKey);
116
135
  }
117
- await setSessionData(opts.storageName, sessionKey, session, opts.expiresIn / 1e3 | 0);
136
+ await setSessionData(opts.storageName, sessionKey, session, opts.expiresIn);
118
137
  if (session.auth?.tokenSet && session.auth.tokenSet.encrypted === true) {
119
138
  if (opts.secret) {
120
139
  const decrypted = decryptTokenSet(session.auth.tokenSet, opts.secret);
@@ -141,7 +160,7 @@ export async function updateSession(sessionId, input, opts) {
141
160
  });
142
161
  }
143
162
  session = deepMerge({}, session, normalizedInput);
144
- session.expiresAt = now + opts.expiresIn;
163
+ session.expiresAt = now + opts.expiresIn * 1e3;
145
164
  const sessionToStore = { ...session };
146
165
  if (opts.secret && sessionToStore.auth?.tokenSet) {
147
166
  sessionToStore.auth = {
@@ -152,7 +171,7 @@ export async function updateSession(sessionId, input, opts) {
152
171
  )
153
172
  };
154
173
  }
155
- await setSessionData(opts.storageName, serverKey, sessionToStore, opts.expiresIn / 1e3 | 0);
174
+ await setSessionData(opts.storageName, serverKey, sessionToStore, opts.expiresIn);
156
175
  return session;
157
176
  }
158
177
  export async function renewSession(sessionId, opts) {
@@ -160,9 +179,9 @@ export async function renewSession(sessionId, opts) {
160
179
  if (await hasSessionData(opts.storageName, sessionKey)) {
161
180
  await removeSessionData(opts.storageName, sessionKey);
162
181
  }
163
- const session = newSession(generateSessionId(), Date.now() + opts.expiresIn);
182
+ const session = newSession(generateSessionId(), Date.now() + opts.expiresIn * 1e3);
164
183
  sessionKey = getSessionStorageKey(opts.storagePrefix, session.id);
165
- await setSessionData(opts.storageName, sessionKey, session, opts.expiresIn / 1e3 | 0);
184
+ await setSessionData(opts.storageName, sessionKey, session, opts.expiresIn);
166
185
  return session;
167
186
  }
168
187
  export async function deleteSession(sessionId, opts) {
@@ -7,7 +7,7 @@ import {
7
7
  } from "#devcoffee-core/server/adapters/http";
8
8
  import { deepMerge } from "#devcoffee-core/server/adapters/utils";
9
9
  import useServerLogger from "#devcoffee-core/server/composables/useServerLogger";
10
- import { getOpenIdConfiguration } from "./helpers.js";
10
+ import { getOpenIdConfiguration, writeSessionCookie } from "./helpers.js";
11
11
  const defaultOpts = {
12
12
  logLevel: 2,
13
13
  proxyPrefix: ""
@@ -51,6 +51,15 @@ export default function NuxtForwardRequestHandler(opts) {
51
51
  };
52
52
  logger.info(`from '${event.path}' to '${defaultForwardInit.forwardUrl}' - auth stt='${session?.auth?.status}'`);
53
53
  const { forwardUrl, ...proxyOption } = await onBeforeRequest?.(defaultForwardInit, params) || defaultForwardInit;
54
- return await proxyRequest(event, forwardUrl, proxyOption);
54
+ const { sessions } = useRuntimeConfig(event).nuxtCore.authts;
55
+ await proxyRequest(event, forwardUrl, {
56
+ ...proxyOption,
57
+ onResponse(proxyEvent, _upstreamResponse) {
58
+ const session2 = proxyEvent.context.session;
59
+ if (session2) {
60
+ writeSessionCookie(proxyEvent, session2, sessions);
61
+ }
62
+ }
63
+ });
55
64
  });
56
65
  }
@@ -1,7 +1,11 @@
1
- import { defineNitroPlugin, getCookie, setCookie, useRuntimeConfig } from "#devcoffee-core/server/adapters/http";
1
+ import { defineNitroPlugin, getCookie, useRuntimeConfig } from "#devcoffee-core/server/adapters/http";
2
2
  import useServerLogger from "#devcoffee-core/server/composables/useServerLogger";
3
- import { signSessionId } from "#devcoffee-core/server/core/crypto";
4
- import { refreshTokenIfNeeded, updateSession, validateSession } from "#devcoffee-core/server/core/helpers";
3
+ import {
4
+ refreshTokenIfNeeded,
5
+ updateSession,
6
+ validateSession,
7
+ writeSessionCookie
8
+ } from "#devcoffee-core/server/core/helpers";
5
9
  import { useNitroApp, useStorage } from "nitropack/runtime";
6
10
  export default defineNitroPlugin((nitroApp) => {
7
11
  nitroApp.hooks.hook("request", async (event) => {
@@ -65,18 +69,10 @@ export default defineNitroPlugin((nitroApp) => {
65
69
  event.context.session = session;
66
70
  });
67
71
  nitroApp.hooks.hook("beforeResponse", async (event) => {
68
- const {
69
- cookieOpts,
70
- secret = "",
71
- names: { sessionId: cookieName }
72
- } = useRuntimeConfig(event).nuxtCore.authts.sessions;
72
+ if (event.node.res.headersSent) return;
73
73
  const session = event.context.session;
74
74
  if (session) {
75
- const cookieValue = secret ? signSessionId(session.id, secret) : session.id;
76
- setCookie(event, cookieName, cookieValue, {
77
- ...cookieOpts,
78
- expires: new Date(session.expiresAt)
79
- });
75
+ writeSessionCookie(event, session, useRuntimeConfig(event).nuxtCore.authts.sessions);
80
76
  }
81
77
  });
82
78
  });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@devcoffee/nuxt-core",
3
- "version": "1.3.0",
3
+ "version": "1.4.1",
4
4
  "publishConfig": {
5
5
  "access": "public"
6
6
  },
@@ -53,7 +53,7 @@
53
53
  "cleanup": "nuxi cleanup && nuxi cleanup playground",
54
54
  "dev:build": "nuxi build playground",
55
55
  "prepack": "nuxt-module-build build",
56
- "release": "npm run lint && npm run test:all && npm run prepack && changelogen --release && npm publish && git push --follow-tags",
56
+ "release": "npm run lint && npm run test:all && npm run prepack && npm publish && git push --follow-tags",
57
57
  "lint": "eslint",
58
58
  "lint:fix": "eslint --fix",
59
59
  "test": "cross-env NODE_OPTIONS=--no-deprecation vitest run test/unit",