@devcoffee/nuxt-core 1.5.0 → 1.6.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -1,5 +1,36 @@
1
1
  # Changelog
2
2
 
3
+ ## v1.6.0
4
+
5
+ [compare changes](https://github.com/coolkg1412/devcoffee-nuxt-core/compare/v1.5.1...v1.6.0)
6
+
7
+ ### 🚀 Enhancements
8
+
9
+ - Add toFormattedSize function and update PluginProviders interface ([7e8d420](https://github.com/coolkg1412/devcoffee-nuxt-core/commit/7e8d420))
10
+
11
+ ### 🩹 Fixes
12
+
13
+ - Implement server bypass rules and enhance session handling in auth module ([c86c70a](https://github.com/coolkg1412/devcoffee-nuxt-core/commit/c86c70a))
14
+ - Update release script to include type checks before publishing ([2895197](https://github.com/coolkg1412/devcoffee-nuxt-core/commit/2895197))
15
+ - Enhance documentation and type definitions for server bypass rules and logging utilities ([429d4d7](https://github.com/coolkg1412/devcoffee-nuxt-core/commit/429d4d7))
16
+
17
+ ### ❤️ Contributors
18
+
19
+ - Hieu Nguyen <hieu.nguyen@devcoffee.tech>
20
+
21
+ ## v1.5.1
22
+
23
+ [compare changes](https://github.com/coolkg1412/devcoffee-nuxt-core/compare/v1.5.0...v1.5.1)
24
+
25
+ ### 🩹 Fixes
26
+
27
+ - Implement auth request bypass logic for improved session handling ([8b4299c](https://github.com/coolkg1412/devcoffee-nuxt-core/commit/8b4299c))
28
+ - Add ignore rule for .gitnexus/run.cjs in ESLint configuration ([5c563ff](https://github.com/coolkg1412/devcoffee-nuxt-core/commit/5c563ff))
29
+
30
+ ### ❤️ Contributors
31
+
32
+ - Hieu Nguyen <hieu.nguyen@devcoffee.tech>
33
+
3
34
  ## v1.5.0
4
35
 
5
36
  [compare changes](https://github.com/coolkg1412/devcoffee-nuxt-core/compare/v1.4.2...v1.5.0)
package/README.md CHANGED
@@ -144,8 +144,14 @@ All options are nested under `nuxtCore` in `nuxt.config.ts`.
144
144
  | `loginUri` | `string` | `'/login'` | Path middleware redirects unauthenticated users to |
145
145
  | `defaultLoginRedirectUri` | `string` | `'/'` | Default post-login redirect when no intended destination is recorded |
146
146
  | `defaultLogoutRedirectUri` | `string` | `'/login'` | Post-logout redirect |
147
- | `ignoreRegexPatterns` | `RegExp[]` | `[]` | Routes matching these patterns are excluded from middleware in all environments |
148
- | `ignoreRegexPatternsDev` | `RegExp[]` | `[]` | Routes excluded from middleware in development only |
147
+ | `ignoreRegexPatterns` | `RegExp[]` | `[]` | Routes matching these patterns are excluded from middleware in all environments |
148
+ | `ignoreRegexPatternsDev` | `RegExp[]` | `[]` | Routes excluded from middleware in development only |
149
+ | `appendIgnoreRegexPatterns` | `RegExp[]` | `[]` | Additional routes appended to `ignoreRegexPatterns`, useful for downstream modules |
150
+ | `appendIgnoreRegexPatternsDev` | `RegExp[]` | `[]` | Additional routes appended to `ignoreRegexPatternsDev`, useful for downstream modules |
151
+ | `serverBypassRules` | `{ pattern: string \| RegExp, mode: 'hard' \| 'soft' \| 'none' }[]` | built-ins | Server auth plugin bypass rules; last matching rule wins. Patterns are normalized to serializable regex source strings |
152
+ | `serverBypassRulesDev` | `{ pattern: string \| RegExp, mode: 'hard' \| 'soft' \| 'none' }[]` | `[]` | Development-only server auth plugin bypass rules |
153
+ | `appendServerBypassRules` | `{ pattern: string \| RegExp, mode: 'hard' \| 'soft' \| 'none' }[]` | `[]` | Additional server bypass rules appended after `serverBypassRules` |
154
+ | `appendServerBypassRulesDev` | `{ pattern: string \| RegExp, mode: 'hard' \| 'soft' \| 'none' }[]` | `[]` | Additional development-only server bypass rules |
149
155
 
150
156
  ### `logging` options
151
157
 
package/dist/module.d.mts CHANGED
@@ -16,6 +16,13 @@ interface AuthorizedUser {
16
16
 
17
17
  type AuthStatus = 'unauthenticated' | 'authenticated'
18
18
 
19
+ type AuthRequestBypassMode = 'hard' | 'soft' | 'none'
20
+
21
+ type AuthRequestBypassRule = {
22
+ pattern: string | RegExp
23
+ mode: AuthRequestBypassMode
24
+ }
25
+
19
26
  type AuthData = {
20
27
  status: AuthStatus
21
28
  tokenSet?: {
@@ -226,6 +233,42 @@ type AuthOptions = {
226
233
  ignoreRegexPatterns: RegExp[]
227
234
 
228
235
  ignoreRegexPatternsDev: RegExp[]
236
+
237
+ /**
238
+ * Server-side auth plugin bypass rules.
239
+ *
240
+ * - `hard`: skips session validation, refresh, userInfo, and cookie writes.
241
+ * - `soft`: validates session and writes cookies, but skips refresh and userInfo side effects.
242
+ * - `none`: applies full auth processing; useful to override an earlier broad rule.
243
+ *
244
+ * Rules are evaluated in order and the last matching rule wins.
245
+ */
246
+ serverBypassRules: AuthRequestBypassRule[]
247
+
248
+ /** Development-only server-side auth plugin bypass rules. */
249
+ serverBypassRulesDev: AuthRequestBypassRule[]
250
+
251
+ /**
252
+ * Additional ignore patterns appended after the normalized `ignoreRegexPatterns` list.
253
+ * Intended for downstream modules that need to contribute ignored routes without
254
+ * replacing the app-owned base list.
255
+ */
256
+ appendIgnoreRegexPatterns: RegExp[]
257
+
258
+ /**
259
+ * Additional development-only ignore patterns appended after the normalized
260
+ * `ignoreRegexPatternsDev` list.
261
+ */
262
+ appendIgnoreRegexPatternsDev: RegExp[]
263
+
264
+ /**
265
+ * Additional server-side bypass rules appended after `serverBypassRules`.
266
+ * Intended for downstream modules that need to contribute system routes.
267
+ */
268
+ appendServerBypassRules: AuthRequestBypassRule[]
269
+
270
+ /** Additional development-only server-side bypass rules appended after `serverBypassRulesDev`. */
271
+ appendServerBypassRulesDev: AuthRequestBypassRule[]
229
272
  }
230
273
 
231
274
  /**
@@ -304,7 +347,19 @@ type ModulePublicRuntimeConfig = Pick<ModuleOptions, 'defaultLocale' | 'defaultT
304
347
 
305
348
  type InputModuleOptions = DeepPartial<ModuleOptions>
306
349
 
350
+ /**
351
+ * The main entry point for the `@devcoffee/nuxt-core` module.
352
+ *
353
+ * This Nuxt module sets up the core infrastructure for applications, including:
354
+ * - Authentication and session management (authts)
355
+ * - Server and client logging functionality
356
+ * - Global utilities and formatters
357
+ * - Nitro plugins for request handling
358
+ * - Custom Nuxt DevTools integration for inspecting sessions
359
+ *
360
+ * @type {NuxtModule<InputModuleOptions>}
361
+ */
307
362
  declare const _module: NuxtModule<InputModuleOptions>;
308
363
 
309
364
  export { _module as default };
310
- export type { AuthData, AuthorizedUser, AuthtsMiddlewareMeta, AuthtsModuleOptions, CoreLogInstance, CoreLogLevel, InputModuleOptions, LoggingModuleOptions, LoggingOptions, ModuleOptions, ModulePublicRuntimeConfig, NuxtAuthOptions, NuxtCoreLogging, NuxtSessionContext, NuxtSessionUpdateContext, SessionContext };
365
+ export type { AuthData, AuthRequestBypassMode, AuthRequestBypassRule, AuthorizedUser, AuthtsMiddlewareMeta, AuthtsModuleOptions, CoreLogInstance, CoreLogLevel, InputModuleOptions, LoggingModuleOptions, LoggingOptions, ModuleOptions, ModulePublicRuntimeConfig, NuxtAuthOptions, NuxtCoreLogging, NuxtSessionContext, NuxtSessionUpdateContext, SessionContext };
package/dist/module.d.ts CHANGED
@@ -16,6 +16,13 @@ interface AuthorizedUser {
16
16
 
17
17
  type AuthStatus = 'unauthenticated' | 'authenticated'
18
18
 
19
+ type AuthRequestBypassMode = 'hard' | 'soft' | 'none'
20
+
21
+ type AuthRequestBypassRule = {
22
+ pattern: string | RegExp
23
+ mode: AuthRequestBypassMode
24
+ }
25
+
19
26
  type AuthData = {
20
27
  status: AuthStatus
21
28
  tokenSet?: {
@@ -226,6 +233,42 @@ type AuthOptions = {
226
233
  ignoreRegexPatterns: RegExp[]
227
234
 
228
235
  ignoreRegexPatternsDev: RegExp[]
236
+
237
+ /**
238
+ * Server-side auth plugin bypass rules.
239
+ *
240
+ * - `hard`: skips session validation, refresh, userInfo, and cookie writes.
241
+ * - `soft`: validates session and writes cookies, but skips refresh and userInfo side effects.
242
+ * - `none`: applies full auth processing; useful to override an earlier broad rule.
243
+ *
244
+ * Rules are evaluated in order and the last matching rule wins.
245
+ */
246
+ serverBypassRules: AuthRequestBypassRule[]
247
+
248
+ /** Development-only server-side auth plugin bypass rules. */
249
+ serverBypassRulesDev: AuthRequestBypassRule[]
250
+
251
+ /**
252
+ * Additional ignore patterns appended after the normalized `ignoreRegexPatterns` list.
253
+ * Intended for downstream modules that need to contribute ignored routes without
254
+ * replacing the app-owned base list.
255
+ */
256
+ appendIgnoreRegexPatterns: RegExp[]
257
+
258
+ /**
259
+ * Additional development-only ignore patterns appended after the normalized
260
+ * `ignoreRegexPatternsDev` list.
261
+ */
262
+ appendIgnoreRegexPatternsDev: RegExp[]
263
+
264
+ /**
265
+ * Additional server-side bypass rules appended after `serverBypassRules`.
266
+ * Intended for downstream modules that need to contribute system routes.
267
+ */
268
+ appendServerBypassRules: AuthRequestBypassRule[]
269
+
270
+ /** Additional development-only server-side bypass rules appended after `serverBypassRulesDev`. */
271
+ appendServerBypassRulesDev: AuthRequestBypassRule[]
229
272
  }
230
273
 
231
274
  /**
@@ -304,7 +347,19 @@ type ModulePublicRuntimeConfig = Pick<ModuleOptions, 'defaultLocale' | 'defaultT
304
347
 
305
348
  type InputModuleOptions = DeepPartial<ModuleOptions>
306
349
 
350
+ /**
351
+ * The main entry point for the `@devcoffee/nuxt-core` module.
352
+ *
353
+ * This Nuxt module sets up the core infrastructure for applications, including:
354
+ * - Authentication and session management (authts)
355
+ * - Server and client logging functionality
356
+ * - Global utilities and formatters
357
+ * - Nitro plugins for request handling
358
+ * - Custom Nuxt DevTools integration for inspecting sessions
359
+ *
360
+ * @type {NuxtModule<InputModuleOptions>}
361
+ */
307
362
  declare const _module: NuxtModule<InputModuleOptions>;
308
363
 
309
364
  export { _module as default };
310
- export type { AuthData, AuthorizedUser, AuthtsMiddlewareMeta, AuthtsModuleOptions, CoreLogInstance, CoreLogLevel, InputModuleOptions, LoggingModuleOptions, LoggingOptions, ModuleOptions, ModulePublicRuntimeConfig, NuxtAuthOptions, NuxtCoreLogging, NuxtSessionContext, NuxtSessionUpdateContext, SessionContext };
365
+ export type { AuthData, AuthRequestBypassMode, AuthRequestBypassRule, AuthorizedUser, AuthtsMiddlewareMeta, AuthtsModuleOptions, CoreLogInstance, CoreLogLevel, InputModuleOptions, LoggingModuleOptions, LoggingOptions, ModuleOptions, ModulePublicRuntimeConfig, NuxtAuthOptions, NuxtCoreLogging, NuxtSessionContext, NuxtSessionUpdateContext, SessionContext };
package/dist/module.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "nuxt-core",
3
- "version": "1.5.0",
3
+ "version": "1.6.0",
4
4
  "configKey": "nuxtCore",
5
5
  "compatibility": {
6
6
  "nuxt": "^4.0.0"
package/dist/module.mjs CHANGED
@@ -2,11 +2,17 @@ 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.5.0";
5
+ const version = "1.6.0";
6
6
 
7
7
  const defaultLocale = "vi-VN";
8
8
  const defaultLanguage = "vi";
9
9
  const defaultTimeZone = "Asia/Ho_Chi_Minh";
10
+ const builtInServerBypassRules = [
11
+ { pattern: "^/__devcoffee_core_session_devtools__$", mode: "hard" },
12
+ { pattern: "^/_i18n/", mode: "hard" },
13
+ { pattern: "^/_nuxt/", mode: "hard" },
14
+ { pattern: "^/__nuxt", mode: "soft" }
15
+ ];
10
16
  const loggingDefaults = {
11
17
  server: {
12
18
  tag: "server",
@@ -19,7 +25,8 @@ const loggingDefaults = {
19
25
  client: {
20
26
  tag: "app-client",
21
27
  level: 2
22
- }
28
+ },
29
+ loggers: {}
23
30
  };
24
31
  const authtsDefaults = {
25
32
  enabled: true,
@@ -74,11 +81,37 @@ const authtsDefaults = {
74
81
  timezone: defaultTimeZone
75
82
  },
76
83
  ignoreRegexPatterns: [],
77
- ignoreRegexPatternsDev: []
84
+ ignoreRegexPatternsDev: [],
85
+ serverBypassRules: [],
86
+ serverBypassRulesDev: [],
87
+ appendIgnoreRegexPatterns: [],
88
+ appendIgnoreRegexPatternsDev: [],
89
+ appendServerBypassRules: [],
90
+ appendServerBypassRulesDev: []
78
91
  }
79
92
  };
93
+ function collectAuthIgnorePatterns(inputOpts, key) {
94
+ return inputOpts.flatMap((opts) => (opts.authts?.auth?.[key] || []).filter(isRegExp));
95
+ }
96
+ function collectAuthServerBypassRules(inputOpts, key) {
97
+ return inputOpts.flatMap((opts) => (opts.authts?.auth?.[key] || []).filter(isAuthRequestBypassRule));
98
+ }
99
+ function isRegExp(value) {
100
+ return value instanceof RegExp;
101
+ }
102
+ function isAuthRequestBypassRule(value) {
103
+ if (!value || typeof value !== "object") return false;
104
+ const rule = value;
105
+ return (isRegExp(rule.pattern) || typeof rule.pattern === "string") && ["hard", "soft", "none"].includes(String(rule.mode));
106
+ }
107
+ function normalizeAuthRequestBypassRule(rule) {
108
+ return {
109
+ pattern: typeof rule.pattern === "string" ? rule.pattern : rule.pattern.source,
110
+ mode: rule.mode
111
+ };
112
+ }
80
113
  function normalizedModuleOptions(...inputOpts) {
81
- return deepMerge(
114
+ const mergedOptions = deepMerge(
82
115
  {
83
116
  defaultLocale,
84
117
  defaultLanguage,
@@ -88,6 +121,32 @@ function normalizedModuleOptions(...inputOpts) {
88
121
  },
89
122
  ...inputOpts
90
123
  );
124
+ const appendIgnoreRegexPatterns = collectAuthIgnorePatterns(inputOpts, "appendIgnoreRegexPatterns");
125
+ const appendIgnoreRegexPatternsDev = collectAuthIgnorePatterns(inputOpts, "appendIgnoreRegexPatternsDev");
126
+ const appendServerBypassRules = collectAuthServerBypassRules(inputOpts, "appendServerBypassRules");
127
+ const appendServerBypassRulesDev = collectAuthServerBypassRules(inputOpts, "appendServerBypassRulesDev");
128
+ mergedOptions.authts.auth.appendIgnoreRegexPatterns = appendIgnoreRegexPatterns;
129
+ mergedOptions.authts.auth.appendIgnoreRegexPatternsDev = appendIgnoreRegexPatternsDev;
130
+ mergedOptions.authts.auth.appendServerBypassRules = appendServerBypassRules.map(normalizeAuthRequestBypassRule);
131
+ mergedOptions.authts.auth.appendServerBypassRulesDev = appendServerBypassRulesDev.map(normalizeAuthRequestBypassRule);
132
+ mergedOptions.authts.auth.ignoreRegexPatterns = [
133
+ ...mergedOptions.authts.auth.ignoreRegexPatterns || [],
134
+ ...appendIgnoreRegexPatterns
135
+ ];
136
+ mergedOptions.authts.auth.ignoreRegexPatternsDev = [
137
+ ...mergedOptions.authts.auth.ignoreRegexPatternsDev || [],
138
+ ...appendIgnoreRegexPatternsDev
139
+ ];
140
+ mergedOptions.authts.auth.serverBypassRules = [
141
+ ...builtInServerBypassRules,
142
+ ...mergedOptions.authts.auth.serverBypassRules || [],
143
+ ...appendServerBypassRules
144
+ ].map(normalizeAuthRequestBypassRule);
145
+ mergedOptions.authts.auth.serverBypassRulesDev = [
146
+ ...mergedOptions.authts.auth.serverBypassRulesDev || [],
147
+ ...appendServerBypassRulesDev
148
+ ].map(normalizeAuthRequestBypassRule);
149
+ return mergedOptions;
91
150
  }
92
151
  function normalizePublicRuntimeConfig(inputOpts) {
93
152
  const { enabled } = inputOpts.authts;
@@ -1,4 +1,16 @@
1
1
  import type { CoreLogInstance, CoreLogLevel } from '#app';
2
+ /**
3
+ * 📝 Provides access to the core logging instance.
4
+ *
5
+ * Retrieves a Nuxt logger configured with the given options, falling back to
6
+ * default NuxtCore logging settings.
7
+ *
8
+ * @param {Object} [opts] - Optional logger configuration.
9
+ * @param {string} [opts.tag] - A specific tag to apply to the logger.
10
+ * @param {CoreLogLevel} [opts.level] - Override the default log level.
11
+ * @returns {CoreLogInstance} A configured logger instance.
12
+ * @since 1.0.0
13
+ */
2
14
  export default function useLogger(opts?: {
3
15
  tag?: string;
4
16
  level?: CoreLogLevel;
@@ -1,20 +1,37 @@
1
+ /**
2
+ * Options for padding a number or string.
3
+ */
1
4
  type PadOptions = {
5
+ /** A prefix to apply before the padded string. */
2
6
  prefix: string & {
3
7
  __uppercaseBrand?: never;
4
8
  };
9
+ /** The separator to insert between the prefix and the padded string. */
5
10
  separator: string;
11
+ /** The total target length to pad to. */
6
12
  length: number;
13
+ /** The character to use for padding. */
7
14
  padChar: string;
8
15
  };
16
+ /** Formats a number with specific padding and prefix options. */
9
17
  type ToPad = (num: number, opts?: Partial<PadOptions>) => string;
18
+ /** Formats a raw number string into a US phone number format `(XXX) XXX-XXXX`. */
10
19
  type AsPhoneText = (numStr: string) => string;
20
+ /** Converts a byte value into a formatted human-readable file size. */
21
+ type ToFormattedSize = (numStr?: Nullable<string> | Nullable<number>) => string;
22
+ /** Formats a Date object into a localized date string. */
11
23
  type AsDateString = (date: Date) => string;
24
+ /** Formats a Date object into a localized time string. */
12
25
  type AsTimeString = (date: Date, seconds?: boolean) => string;
26
+ /** Formats a Date object into a localized date and time string. */
13
27
  type AsDateTimeString = (date: Date, seconds?: boolean) => string;
28
+ /** Formats a Date object into a relative time string (e.g. "2 days ago"). */
14
29
  type AsRelativeTimeString = (date: Date) => string;
30
+ /** Defines the formatter functions provided to the Nuxt application context. */
15
31
  type PluginProviders = {
16
32
  toPad: ToPad;
17
33
  asPhoneText: AsPhoneText;
34
+ toFormattedSize: ToFormattedSize;
18
35
  asDateString: AsDateString;
19
36
  asTimeString: AsTimeString;
20
37
  asDateTimeString: AsDateTimeString;
@@ -25,5 +42,13 @@ declare module 'vue' {
25
42
  $formatters: PluginProviders;
26
43
  }
27
44
  }
45
+ /**
46
+ * 🧩 Nuxt plugin for global data formatting utilities.
47
+ *
48
+ * Provides helpers for formatting numbers, dates, times, filesizes, and phone numbers
49
+ * across the application, actively aware of the user's current locale and timezone.
50
+ *
51
+ * @since 1.0.0
52
+ */
28
53
  declare const _default: any;
29
54
  export default _default;
@@ -42,6 +42,15 @@ export default defineNuxtPlugin((_nuxtApp) => {
42
42
  const digits = numStr.replace(/\D/g, "");
43
43
  return digits.replace(/(\d{3})(\d{3})(\d{4})/, "($1) $2-$3");
44
44
  }
45
+ function toFormattedSize(numStr) {
46
+ if (!numStr) return "0B";
47
+ const bytes = typeof numStr == "number" ? numStr : parseInt(numStr);
48
+ const units = ["B", "KB", "MB", "GB", "TB"];
49
+ const index = Math.min(units.length - 1, Math.floor(Math.log(bytes) / Math.log(1024)));
50
+ const value = bytes / 1024 ** index;
51
+ const rounded = value >= 10 || index === 0 ? Math.round(value) : Number(value.toFixed(1));
52
+ return `${rounded}${units[index]}`;
53
+ }
45
54
  function asDateString(date) {
46
55
  logger.debug(`Format date with locale ='${currentLocale.value}'`);
47
56
  return new Intl.DateTimeFormat(currentLocale.value).format(date);
@@ -101,6 +110,7 @@ export default defineNuxtPlugin((_nuxtApp) => {
101
110
  }
102
111
  _nuxtApp.provide("formatters", {
103
112
  toPad,
113
+ toFormattedSize,
104
114
  asPhoneText,
105
115
  asDateString,
106
116
  asTimeString,
@@ -1,8 +1,14 @@
1
+ /**
2
+ * Represents the current active locale configuration.
3
+ */
1
4
  type LocaleState = {
2
5
  locale: string;
3
6
  language: string;
4
7
  timeZone: string;
5
8
  };
9
+ /**
10
+ * The provider shape for locale settings exposed to the application context.
11
+ */
6
12
  type LocaleProvider = LocaleState;
7
13
  /**
8
14
  * 🧩 Nuxt plugin for locale utilities.
@@ -1 +1,8 @@
1
+ /**
2
+ * Generates a random short hash without hyphens.
3
+ *
4
+ * @param {number} [length=8] - The desired length of the resulting hash.
5
+ * @returns {string} A random string of the specified length.
6
+ * @since 1.0.0
7
+ */
1
8
  export declare function randomShortHash(length?: number): string;
@@ -1,5 +1,17 @@
1
1
  import { type ConsolaInstance, type LogLevel } from 'consola';
2
2
  import type { H3Event } from 'h3';
3
+ /**
4
+ * Retrieves or initializes the server-side logger instance.
5
+ *
6
+ * Uses the Nuxt runtime configuration to set up a `consola` logger with the appropriate
7
+ * tags and log levels. If an H3 event is provided, it will be used to fetch the current event's runtime config.
8
+ *
9
+ * @param {Object} [options] - Optional logger configuration.
10
+ * @param {H3Event} [options.event] - The current H3 event, used to fetch runtime config context.
11
+ * @param {string} [options.tag] - A specific tag to apply to the logger.
12
+ * @param {LogLevel} [options.level] - Override the default log level.
13
+ * @returns {ConsolaInstance} A configured Consola logger instance.
14
+ */
3
15
  export default function useServerLogger(options?: Partial<{
4
16
  event?: H3Event;
5
17
  tag?: string;
@@ -77,6 +77,15 @@ export declare function validateSession(sessionCookieId: string | undefined, opt
77
77
  * @since 1.0.0
78
78
  */
79
79
  export declare function updateSession(sessionId: string, input: DeepPartial<Omit<SessionContext, 'id' | 'expiresAt' | 'issuedAt'>>, opts: SessionCreateOptions): Promise<SessionContext>;
80
+ /**
81
+ * Renews an existing session by destroying it and creating a fresh unauthenticated session
82
+ * with a new session ID and updated expiration time.
83
+ *
84
+ * @param sessionId - The ID of the session to renew.
85
+ * @param opts - Session creation options including storage name, prefix, and expiration time.
86
+ * @returns A promise that resolves to the newly created {@link SessionContext}.
87
+ * @since 1.0.0
88
+ */
80
89
  export declare function renewSession(sessionId: string, opts: SessionCreateOptions): Promise<SessionContext>;
81
90
  /**
82
91
  * Delete a session from storage by ID.
@@ -180,6 +189,14 @@ export declare function authorizationCodeGrant(authorizeParams: {
180
189
  redirectUri: string;
181
190
  usePkce: boolean;
182
191
  }): Promise<any>;
192
+ /**
193
+ * Constructs a normalized token set from an OpenID Connect token endpoint response.
194
+ * Ensures the token type is properly capitalized.
195
+ *
196
+ * @param input - The raw token endpoint response.
197
+ * @returns A normalized token set containing access, id, and refresh tokens along with expiry.
198
+ * @since 1.0.0
199
+ */
183
200
  export declare function constructTokenSet(input: TokenEndpointResponse): {
184
201
  tokenType: string;
185
202
  idToken: any;
@@ -188,6 +205,15 @@ export declare function constructTokenSet(input: TokenEndpointResponse): {
188
205
  scopes: any;
189
206
  expiresAt: number;
190
207
  };
208
+ /**
209
+ * Checks if the session's access token is expired or expiring soon, and refreshes it if necessary.
210
+ * Implements a distributed lock to prevent concurrent refresh attempts for the same session.
211
+ *
212
+ * @param session - The current session context.
213
+ * @param opts - Configuration options for the refresh token grant, caching, and locking.
214
+ * @returns A partial session update containing the new authentication status and user, or empty object if no refresh occurred.
215
+ * @since 1.0.0
216
+ */
191
217
  export declare function refreshTokenIfNeeded(session: SessionContext, opts: {
192
218
  wellKnownUrl: string;
193
219
  cache: {
@@ -208,7 +234,7 @@ export declare function refreshTokenIfNeeded(session: SessionContext, opts: {
208
234
  /**
209
235
  * Fetch user profile information from the OpenID Provider using the access token.
210
236
  *
211
- * @param tokenSet - The token response returned from the token endpoint.
237
+ * @param accessToken - The access token used to authenticate the request to the userinfo endpoint.
212
238
  * @param sub - The subject identifier of the authenticated user.
213
239
  * @param opts - OpenID discovery and client configuration options.
214
240
  * @returns Simplified user information (id, sub, email, first/last name).
@@ -217,6 +243,15 @@ export declare function refreshTokenIfNeeded(session: SessionContext, opts: {
217
243
  export declare function fetchUserInfo(accessToken: string, sub: string, opts: OpenIdDiscoveryOptions & {
218
244
  wellKnownUrl: string;
219
245
  }): Promise<any>;
246
+ /**
247
+ * Revoke an array of tokens (e.g., access and refresh tokens) at the OpenID Provider.
248
+ * Skips empty or undefined tokens.
249
+ *
250
+ * @param tokens - An array of token strings to revoke.
251
+ * @param opts - OpenID discovery and client configuration options.
252
+ * @returns A promise resolving to an array of settlement results for each revocation request.
253
+ * @since 1.0.0
254
+ */
220
255
  export declare function revokeTokens(tokens: string[], opts: OpenIdDiscoveryOptions & {
221
256
  wellKnownUrl: string;
222
257
  }): Promise<PromiseSettledResult<any>[]>;
@@ -1,2 +1,2 @@
1
- declare const _default: import("h3").EventHandler<import("h3").EventHandlerRequest, any>;
1
+ declare const _default: any;
2
2
  export default _default;
@@ -1,4 +1,281 @@
1
- import { defineEventHandler } from "h3";
2
- export default defineEventHandler((event) => {
3
- return event.context.session;
1
+ import { eventHandler, getCookie, getQuery, useRuntimeConfig } from "#devcoffee-core/server/adapters/http";
2
+ import { getSessionData } from "#devcoffee-core/server/adapters/storage";
3
+ import { isValidSessionId, verifySessionId } from "#devcoffee-core/server/core/crypto";
4
+ function getSessionStorageKey(storagePrefix, sessionId) {
5
+ return storagePrefix ? `${storagePrefix}:${sessionId}` : sessionId;
6
+ }
7
+ function isEncryptedTokenSet(tokenSet) {
8
+ return Boolean(tokenSet && "encrypted" in tokenSet && tokenSet.encrypted === true);
9
+ }
10
+ function sanitizeSession(session) {
11
+ const tokenSet = session.auth?.tokenSet;
12
+ const tokenSetEncrypted = isEncryptedTokenSet(tokenSet);
13
+ const tokenSetPresent = Boolean(tokenSet);
14
+ const tokenExpiresAt = tokenSet && !tokenSetEncrypted && "expiresAt" in tokenSet ? tokenSet.expiresAt : void 0;
15
+ return {
16
+ ...session,
17
+ auth: {
18
+ status: session.auth?.status || "unauthenticated",
19
+ tokenSet: {
20
+ encrypted: tokenSetEncrypted,
21
+ present: tokenSetPresent,
22
+ tokenType: tokenSet && !tokenSetEncrypted && "tokenType" in tokenSet ? tokenSet.tokenType : void 0,
23
+ scopes: tokenSet && !tokenSetEncrypted && "scopes" in tokenSet ? tokenSet.scopes : void 0,
24
+ expiresAt: tokenExpiresAt,
25
+ expiresInMs: typeof tokenExpiresAt === "number" ? tokenExpiresAt - Date.now() : void 0
26
+ }
27
+ }
28
+ };
29
+ }
30
+ async function readSessionSnapshot(event) {
31
+ const {
32
+ sessions: {
33
+ secret = "",
34
+ storage: { name: storageName, prefix: storagePrefix },
35
+ names: { sessionId: cookieName }
36
+ }
37
+ } = useRuntimeConfig(event).nuxtCore.authts;
38
+ const cookieValue = getCookie(event, cookieName);
39
+ const sessionId = verifySessionId(cookieValue, secret);
40
+ if (!cookieValue) {
41
+ return createSnapshot("missing-cookie", null, null, null);
42
+ }
43
+ if (!sessionId || !isValidSessionId(sessionId)) {
44
+ return createSnapshot("invalid-cookie", null, null, null);
45
+ }
46
+ const storageKey = getSessionStorageKey(storagePrefix, sessionId);
47
+ const session = await getSessionData(storageName, storageKey);
48
+ if (!session) {
49
+ return createSnapshot("missing-session", sessionId, storageKey, null);
50
+ }
51
+ const status = session.expiresAt <= Date.now() ? "expired" : "active";
52
+ return createSnapshot(status, sessionId, storageKey, sanitizeSession(session));
53
+ }
54
+ function createSnapshot(status, sessionId, storageKey, session) {
55
+ return {
56
+ meta: {
57
+ readOnly: true,
58
+ loadedAt: (/* @__PURE__ */ new Date()).toISOString(),
59
+ status,
60
+ sessionId,
61
+ storageKey
62
+ },
63
+ session
64
+ };
65
+ }
66
+ function renderSessionInspector() {
67
+ return `<!doctype html>
68
+ <html lang="en">
69
+ <head>
70
+ <meta charset="utf-8">
71
+ <meta name="viewport" content="width=device-width, initial-scale=1">
72
+ <title>Devcoffee Session</title>
73
+ <style>
74
+ :root {
75
+ color-scheme: light dark;
76
+ --bg: #f7f8fb;
77
+ --panel: #ffffff;
78
+ --text: #17202a;
79
+ --muted: #637083;
80
+ --border: #d9dee8;
81
+ --accent: #0f766e;
82
+ --accent-strong: #0d5f59;
83
+ --code: #101828;
84
+ --code-bg: #eef2f7;
85
+ font-family: Inter, ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
86
+ }
87
+
88
+ @media (prefers-color-scheme: dark) {
89
+ :root {
90
+ --bg: #111827;
91
+ --panel: #182233;
92
+ --text: #ecf1f8;
93
+ --muted: #aab6c7;
94
+ --border: #314057;
95
+ --accent: #2dd4bf;
96
+ --accent-strong: #5eead4;
97
+ --code: #e8eef8;
98
+ --code-bg: #0f172a;
99
+ }
100
+ }
101
+
102
+ * { box-sizing: border-box; }
103
+ body {
104
+ margin: 0;
105
+ min-height: 100vh;
106
+ background: var(--bg);
107
+ color: var(--text);
108
+ font-size: 14px;
109
+ }
110
+ main {
111
+ width: min(980px, calc(100vw - 32px));
112
+ margin: 0 auto;
113
+ padding: 24px 0;
114
+ }
115
+ header {
116
+ display: flex;
117
+ align-items: center;
118
+ justify-content: space-between;
119
+ gap: 16px;
120
+ margin-bottom: 16px;
121
+ }
122
+ h1 {
123
+ margin: 0;
124
+ font-size: 20px;
125
+ font-weight: 700;
126
+ letter-spacing: 0;
127
+ }
128
+ button {
129
+ border: 1px solid var(--accent);
130
+ border-radius: 6px;
131
+ background: var(--accent);
132
+ color: #ffffff;
133
+ cursor: pointer;
134
+ font: inherit;
135
+ font-weight: 700;
136
+ min-height: 36px;
137
+ padding: 0 14px;
138
+ }
139
+ button:hover { background: var(--accent-strong); }
140
+ button:disabled {
141
+ cursor: wait;
142
+ opacity: .7;
143
+ }
144
+ .panel {
145
+ background: var(--panel);
146
+ border: 1px solid var(--border);
147
+ border-radius: 8px;
148
+ overflow: hidden;
149
+ }
150
+ .summary {
151
+ display: grid;
152
+ grid-template-columns: repeat(4, minmax(0, 1fr));
153
+ gap: 0;
154
+ border-bottom: 1px solid var(--border);
155
+ }
156
+ .item {
157
+ min-width: 0;
158
+ padding: 14px 16px;
159
+ border-right: 1px solid var(--border);
160
+ }
161
+ .item:last-child { border-right: 0; }
162
+ .label {
163
+ color: var(--muted);
164
+ font-size: 12px;
165
+ font-weight: 700;
166
+ margin-bottom: 6px;
167
+ text-transform: uppercase;
168
+ }
169
+ .value {
170
+ overflow-wrap: anywhere;
171
+ font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
172
+ font-size: 13px;
173
+ }
174
+ pre {
175
+ margin: 0;
176
+ min-height: 320px;
177
+ overflow: auto;
178
+ padding: 16px;
179
+ background: var(--code-bg);
180
+ color: var(--code);
181
+ font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
182
+ font-size: 12px;
183
+ line-height: 1.55;
184
+ white-space: pre-wrap;
185
+ word-break: break-word;
186
+ }
187
+ .error {
188
+ color: #b42318;
189
+ padding: 16px;
190
+ }
191
+
192
+ @media (max-width: 720px) {
193
+ header { align-items: flex-start; flex-direction: column; }
194
+ button { width: 100%; }
195
+ .summary { grid-template-columns: 1fr; }
196
+ .item { border-bottom: 1px solid var(--border); border-right: 0; }
197
+ .item:last-child { border-bottom: 0; }
198
+ }
199
+ </style>
200
+ </head>
201
+ <body>
202
+ <main>
203
+ <header>
204
+ <h1>Devcoffee Session</h1>
205
+ <button id="refresh" type="button">Refresh</button>
206
+ </header>
207
+ <section class="panel">
208
+ <div class="summary">
209
+ <div class="item">
210
+ <div class="label">Status</div>
211
+ <div class="value" id="status">loading</div>
212
+ </div>
213
+ <div class="item">
214
+ <div class="label">Authenticated</div>
215
+ <div class="value" id="authenticated">unknown</div>
216
+ </div>
217
+ <div class="item">
218
+ <div class="label">User</div>
219
+ <div class="value" id="user">unknown</div>
220
+ </div>
221
+ <div class="item">
222
+ <div class="label">Loaded</div>
223
+ <div class="value" id="loaded">pending</div>
224
+ </div>
225
+ </div>
226
+ <pre id="payload">{}</pre>
227
+ <div class="error" id="error" hidden></div>
228
+ </section>
229
+ </main>
230
+ <script>
231
+ const refreshButton = document.getElementById('refresh')
232
+ const statusNode = document.getElementById('status')
233
+ const authenticatedNode = document.getElementById('authenticated')
234
+ const userNode = document.getElementById('user')
235
+ const loadedNode = document.getElementById('loaded')
236
+ const payloadNode = document.getElementById('payload')
237
+ const errorNode = document.getElementById('error')
238
+
239
+ async function loadSession() {
240
+ refreshButton.disabled = true
241
+ errorNode.hidden = true
242
+ statusNode.textContent = 'loading'
243
+
244
+ try {
245
+ const response = await fetch('?format=json&ts=' + Date.now(), {
246
+ credentials: 'same-origin',
247
+ headers: { Accept: 'application/json' },
248
+ })
249
+ if (!response.ok) throw new Error('Request failed: ' + response.status)
250
+
251
+ const snapshot = await response.json()
252
+ const session = snapshot.session
253
+ statusNode.textContent = snapshot.meta.status
254
+ authenticatedNode.textContent = String(session?.auth?.status === 'authenticated')
255
+ userNode.textContent = session?.user?.email || session?.user?.id || 'none'
256
+ loadedNode.textContent = new Date(snapshot.meta.loadedAt).toLocaleTimeString()
257
+ payloadNode.textContent = JSON.stringify(snapshot, null, 2)
258
+ } catch (error) {
259
+ errorNode.hidden = false
260
+ errorNode.textContent = error instanceof Error ? error.message : String(error)
261
+ statusNode.textContent = 'error'
262
+ } finally {
263
+ refreshButton.disabled = false
264
+ }
265
+ }
266
+
267
+ refreshButton.addEventListener('click', loadSession)
268
+ loadSession()
269
+ <\/script>
270
+ </body>
271
+ </html>`;
272
+ }
273
+ export default eventHandler(async (event) => {
274
+ const query = getQuery(event);
275
+ if (query.format === "json") {
276
+ event.node.res.setHeader("Content-Type", "application/json; charset=utf-8");
277
+ return await readSessionSnapshot(event);
278
+ }
279
+ event.node.res.setHeader("Content-Type", "text/html; charset=utf-8");
280
+ return renderSessionInspector();
4
281
  });
@@ -1,4 +1,4 @@
1
- import { defineNitroPlugin, getCookie, useRuntimeConfig } from "#devcoffee-core/server/adapters/http";
1
+ import { defineNitroPlugin, getCookie, getRequestHeaders, useRuntimeConfig } from "#devcoffee-core/server/adapters/http";
2
2
  import useServerLogger from "#devcoffee-core/server/composables/useServerLogger";
3
3
  import {
4
4
  refreshTokenIfNeeded,
@@ -7,10 +7,42 @@ import {
7
7
  writeSessionCookie
8
8
  } from "#devcoffee-core/server/core/helpers";
9
9
  import { useNitroApp, useStorage } from "nitropack/runtime";
10
+ const fallbackServerBypassRules = [
11
+ { pattern: "^/__devcoffee_core_session_devtools__$", mode: "hard" },
12
+ { pattern: "^/_i18n/", mode: "hard" },
13
+ { pattern: "^/_nuxt/", mode: "hard" },
14
+ { pattern: "^/__nuxt", mode: "soft" }
15
+ ];
16
+ function testBypassRule(rule, pathname) {
17
+ const pattern = typeof rule.pattern === "string" ? new RegExp(rule.pattern) : rule.pattern;
18
+ pattern.lastIndex = 0;
19
+ return pattern.test(pathname);
20
+ }
21
+ function getAuthRequestBypass(event, opts = {}) {
22
+ const path = event.path || "";
23
+ const pathname = path.split("?")[0] || "";
24
+ const i18nHeader = getRequestHeaders(event)["x-nuxt-i18n"];
25
+ if (i18nHeader?.toLowerCase() === "internal") {
26
+ return "hard";
27
+ }
28
+ const serverBypassRules = opts.serverBypassRules?.length ? opts.serverBypassRules : fallbackServerBypassRules;
29
+ const serverBypassRulesDev = opts.serverBypassRulesDev || [];
30
+ let mode = "none";
31
+ for (const rule of serverBypassRules) {
32
+ if (testBypassRule(rule, pathname)) mode = rule.mode;
33
+ }
34
+ if (import.meta.dev) {
35
+ for (const rule of serverBypassRulesDev) {
36
+ if (testBypassRule(rule, pathname)) mode = rule.mode;
37
+ }
38
+ }
39
+ return mode;
40
+ }
10
41
  export default defineNitroPlugin((nitroApp) => {
11
42
  nitroApp.hooks.hook("request", async (event) => {
12
43
  const {
13
44
  enabled: authtsEnabled,
45
+ auth: { serverBypassRules, serverBypassRulesDev } = {},
14
46
  openid: {
15
47
  wellKnownUrl,
16
48
  cache,
@@ -28,6 +60,8 @@ export default defineNitroPlugin((nitroApp) => {
28
60
  names: { sessionId: cookieName }
29
61
  }
30
62
  } = useRuntimeConfig(event).nuxtCore.authts;
63
+ const authRequestBypass = getAuthRequestBypass(event, { serverBypassRules, serverBypassRulesDev });
64
+ if (authRequestBypass === "hard") return;
31
65
  const sessionCookieId = getCookie(event, cookieName);
32
66
  const logger = useServerLogger({ event, tag: "plugin.auth" });
33
67
  let session = await validateSession(sessionCookieId, {
@@ -37,7 +71,8 @@ export default defineNitroPlugin((nitroApp) => {
37
71
  secret
38
72
  });
39
73
  const { status = "unauthenticated", tokenSet } = session.auth || {};
40
- if (authtsEnabled && status === "authenticated" && tokenSet) {
74
+ const shouldRunAuthSideEffects = authRequestBypass === "none";
75
+ if (shouldRunAuthSideEffects && authtsEnabled && status === "authenticated" && tokenSet) {
41
76
  const sessionUpdate = await refreshTokenIfNeeded(session, {
42
77
  wellKnownUrl,
43
78
  cache,
@@ -79,10 +114,12 @@ export default defineNitroPlugin((nitroApp) => {
79
114
  event.context.session = session;
80
115
  });
81
116
  nitroApp.hooks.hook("beforeResponse", async (event) => {
117
+ const { auth: { serverBypassRules, serverBypassRulesDev } = {}, sessions } = useRuntimeConfig(event).nuxtCore.authts;
118
+ if (getAuthRequestBypass(event, { serverBypassRules, serverBypassRulesDev }) === "hard") return;
82
119
  if (event.node.res.headersSent) return;
83
120
  const session = event.context.session;
84
121
  if (session) {
85
- writeSessionCookie(event, session, useRuntimeConfig(event).nuxtCore.authts.sessions);
122
+ writeSessionCookie(event, session, sessions);
86
123
  }
87
124
  });
88
125
  });
@@ -1,2 +1,11 @@
1
+ /**
2
+ * 📝 Nitro plugin for server-side logging integration.
3
+ *
4
+ * This plugin initializes the server logger and attaches it to the H3 event context (`event.context.logger`)
5
+ * for every incoming request. It also sets up a global error hook to automatically log uncaught
6
+ * Nitro server errors alongside their relevant request context (method and path).
7
+ *
8
+ * @since 1.0.0
9
+ */
1
10
  declare const _default: import("nitropack").NitroAppPlugin;
2
11
  export default _default;
package/dist/types.d.mts CHANGED
@@ -6,4 +6,4 @@ declare module '@nuxt/schema' {
6
6
 
7
7
  export { default } from './module.mjs'
8
8
 
9
- export { type AuthData, type AuthorizedUser, type AuthtsMiddlewareMeta, type AuthtsModuleOptions, type CoreLogInstance, type CoreLogLevel, type InputModuleOptions, type LoggingModuleOptions, type LoggingOptions, type ModuleOptions, type ModulePublicRuntimeConfig, type NuxtAuthOptions, type NuxtCoreLogging, type NuxtSessionContext, type NuxtSessionUpdateContext, type SessionContext } from './module.mjs'
9
+ export { type AuthData, type AuthRequestBypassMode, type AuthRequestBypassRule, type AuthorizedUser, type AuthtsMiddlewareMeta, type AuthtsModuleOptions, type CoreLogInstance, type CoreLogLevel, type InputModuleOptions, type LoggingModuleOptions, type LoggingOptions, type ModuleOptions, type ModulePublicRuntimeConfig, type NuxtAuthOptions, type NuxtCoreLogging, type NuxtSessionContext, type NuxtSessionUpdateContext, type SessionContext } from './module.mjs'
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@devcoffee/nuxt-core",
3
- "version": "1.5.0",
3
+ "version": "1.6.0",
4
4
  "publishConfig": {
5
5
  "access": "public"
6
6
  },
@@ -52,7 +52,7 @@
52
52
  "cleanup": "nuxi cleanup && nuxi cleanup playground",
53
53
  "dev:build": "nuxi build playground",
54
54
  "prepack": "nuxt-module-build build",
55
- "release": "npm run lint && npm run test:all && npm run prepack && changelogen --release && npm publish && git push --follow-tags",
55
+ "release": "npm run lint && npm run test:types && npm run test:all && npm run prepack && changelogen --release && npm publish && git push --follow-tags",
56
56
  "lint": "eslint",
57
57
  "lint:fix": "eslint --fix",
58
58
  "test": "cross-env NODE_OPTIONS=--no-deprecation vitest run test/unit",
@@ -60,7 +60,7 @@
60
60
  "test:e2e:ui": "cross-env NODE_OPTIONS=--no-deprecation PLAYWRIGHT_HEADLESS=false vitest run test/e2e",
61
61
  "test:all": "cross-env NODE_OPTIONS=--no-deprecation vitest run",
62
62
  "test:watch": "cross-env NODE_OPTIONS=--no-deprecation vitest watch test/unit",
63
- "test:types": "vue-tsc --noEmit",
63
+ "test:types": "vue-tsc --noEmit -p .nuxt/tsconfig.app.json",
64
64
  "typecheck": "vue-tsc --noEmit -p .nuxt/tsconfig.app.json"
65
65
  },
66
66
  "dependencies": {