@peterbud/nuxt-aegis 1.1.0-alpha.1 → 1.1.0-alpha.2

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/dist/module.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "nuxt-aegis",
3
3
  "configKey": "nuxtAegis",
4
- "version": "1.1.0-alpha.1",
4
+ "version": "1.1.0-alpha.2",
5
5
  "builder": {
6
6
  "@nuxt/module-builder": "1.0.2",
7
7
  "unbuild": "unknown"
package/dist/module.mjs CHANGED
@@ -316,7 +316,15 @@ const module$1 = defineNuxtModule({
316
316
  addTypeTemplate({
317
317
  filename: "types/nuxt-aegis-nitro.d.ts",
318
318
  getContents: () => {
319
- return `import type { NitroAegisAuth } from ${JSON.stringify(typesPath)}
319
+ return `import type {
320
+ NitroAegisAuth,
321
+ UserInfoHookPayload,
322
+ SuccessHookPayload,
323
+ ImpersonateCheckPayload,
324
+ ImpersonateFetchTargetPayload,
325
+ ImpersonateStartPayload,
326
+ ImpersonateEndPayload
327
+ } from ${JSON.stringify(typesPath)}
320
328
 
321
329
  type NuxtAegisRouteRules = {
322
330
  /**
@@ -335,6 +343,36 @@ declare module 'nitropack/types' {
335
343
  interface NitroRouteConfig {
336
344
  nuxtAegis?: NuxtAegisRouteRules
337
345
  }
346
+ interface NitroRuntimeHooks {
347
+ /**
348
+ * Hook called after fetching user info from the provider, before storing it.
349
+ * Use this to transform or validate the OAuth provider response.
350
+ */
351
+ 'nuxt-aegis:userInfo': (payload: UserInfoHookPayload) => Promise<void> | void
352
+ /**
353
+ * Hook called after successful authentication.
354
+ * Use this for logging, analytics, or database operations.
355
+ */
356
+ 'nuxt-aegis:success': (payload: SuccessHookPayload) => Promise<void> | void
357
+ /**
358
+ * Hook called to determine if a user is allowed to impersonate others.
359
+ * Return true to allow, false to deny.
360
+ */
361
+ 'nuxt-aegis:impersonate:check': (payload: ImpersonateCheckPayload) => Promise<boolean> | boolean
362
+ /**
363
+ * Hook called to fetch the target user's data for impersonation.
364
+ * Must return the target user's data or null if not found.
365
+ */
366
+ 'nuxt-aegis:impersonate:fetchTarget': (payload: ImpersonateFetchTargetPayload) => Promise<Record<string, unknown> | null> | Record<string, unknown> | null
367
+ /**
368
+ * Hook called after impersonation starts successfully (fire-and-forget for audit logging).
369
+ */
370
+ 'nuxt-aegis:impersonate:start': (payload: ImpersonateStartPayload) => Promise<void> | void
371
+ /**
372
+ * Hook called after impersonation ends successfully (fire-and-forget for audit logging).
373
+ */
374
+ 'nuxt-aegis:impersonate:end': (payload: ImpersonateEndPayload) => Promise<void> | void
375
+ }
338
376
  }
339
377
 
340
378
  declare module 'nitropack' {
@@ -348,6 +386,10 @@ declare module 'nitropack' {
348
386
 
349
387
  export {}`;
350
388
  }
389
+ }, {
390
+ nuxt: true,
391
+ nitro: true,
392
+ node: true
351
393
  });
352
394
  }
353
395
  });
@@ -1,10 +1,10 @@
1
1
  import type { ComputedRef } from 'vue';
2
- import type { TokenPayload } from '../../types/index.js';
2
+ import type { BaseTokenClaims } from '../../types/index.js';
3
3
  /**
4
4
  * Return type for the useAuth composable
5
- * @template T - Token payload type extending TokenPayload (defaults to TokenPayload)
5
+ * @template T - Token payload type extending BaseTokenClaims (defaults to BaseTokenClaims)
6
6
  */
7
- interface UseAuthReturn<T extends TokenPayload = TokenPayload> {
7
+ interface UseAuthReturn<T extends BaseTokenClaims = BaseTokenClaims> {
8
8
  /** Reactive property indicating whether a user is logged in */
9
9
  isLoggedIn: ComputedRef<boolean>;
10
10
  /** Reactive property indicating the authentication state is being initialized */
@@ -60,7 +60,7 @@ interface UseAuthReturn<T extends TokenPayload = TokenPayload> {
60
60
  * - logout() - End user session
61
61
  * - refresh() - Restore authentication state
62
62
  *
63
- * @template T - Custom token payload type extending TokenPayload
63
+ * @template T - Custom token payload type extending BaseTokenClaims
64
64
  * @returns {UseAuthReturn<T>} Authentication state and methods
65
65
  *
66
66
  * @example
@@ -71,15 +71,15 @@ interface UseAuthReturn<T extends TokenPayload = TokenPayload> {
71
71
  * // With custom claims
72
72
  * import type { CustomTokenClaims } from '#nuxt-aegis'
73
73
  *
74
- * type AppTokenPayload = CustomTokenClaims<{
74
+ * type AppTokenClaims = CustomTokenClaims<{
75
75
  * role: string
76
76
  * permissions: string[]
77
77
  * organizationId: string
78
78
  * }>
79
79
  *
80
- * const { user, login, logout } = useAuth<AppTokenPayload>()
80
+ * const { user, login, logout } = useAuth<AppTokenClaims>()
81
81
  * // user.value?.role is now type-safe
82
82
  * ```
83
83
  */
84
- export declare function useAuth<T extends TokenPayload = TokenPayload>(): UseAuthReturn<T>;
84
+ export declare function useAuth<T extends BaseTokenClaims = BaseTokenClaims>(): UseAuthReturn<T>;
85
85
  export {};
@@ -1,4 +1,4 @@
1
- import type { TokenPayload } from '../../types/index.js';
1
+ import type { BaseTokenClaims } from '../../types/index.js';
2
2
  /**
3
3
  * Filter out time-sensitive JWT metadata claims that cause hydration mismatches
4
4
  *
@@ -14,4 +14,4 @@ import type { TokenPayload } from '../../types/index.js';
14
14
  * @param user - Token payload with all claims
15
15
  * @returns Token payload with only stable user data
16
16
  */
17
- export declare function filterTimeSensitiveClaims(user: TokenPayload): TokenPayload;
17
+ export declare function filterTimeSensitiveClaims(user: BaseTokenClaims): BaseTokenClaims;
@@ -1,4 +1,4 @@
1
1
  export function filterTimeSensitiveClaims(user) {
2
- const { iat, exp, iss, ...stableUser } = user;
2
+ const { iat, exp, ...stableUser } = user;
3
3
  return stableUser;
4
4
  }
@@ -2,7 +2,6 @@ import { defineNitroPlugin } from "nitropack/runtime";
2
2
  import { getCookie } from "h3";
3
3
  import { hashRefreshToken, getRefreshTokenData } from "../utils/refreshToken.js";
4
4
  import { generateToken } from "../utils/jwt.js";
5
- import { processCustomClaims } from "../utils/customClaims.js";
6
5
  import { useRuntimeConfig } from "#imports";
7
6
  import { createLogger } from "../utils/logger.js";
8
7
  const logger = createLogger("SSR:Auth");
@@ -54,14 +53,7 @@ export default defineNitroPlugin((nitroApp) => {
54
53
  picture: providerUserInfo.picture,
55
54
  provider
56
55
  };
57
- let customClaims = {};
58
- const providerConfig = config.nuxtAegis?.providers?.[provider];
59
- if (providerConfig && "customClaims" in providerConfig && providerConfig.customClaims) {
60
- customClaims = await processCustomClaims(
61
- providerUserInfo,
62
- providerConfig.customClaims
63
- );
64
- }
56
+ const customClaims = storedRefreshToken.customClaims || {};
65
57
  const ssrTokenExpiry = tokenRefreshConfig?.ssrTokenExpiry || "5m";
66
58
  const ssrAccessToken = await generateToken(
67
59
  userPayload,
@@ -95,20 +95,28 @@ export function defineOAuthEventHandler(implementation, {
95
95
  if (_onUserInfo) {
96
96
  providerUserInfo = await _onUserInfo(providerUserInfo, tokens, event);
97
97
  } else {
98
- const handler = useAegisHandler();
99
- if (handler?.onUserInfo) {
98
+ const handler2 = useAegisHandler();
99
+ if (handler2?.onUserInfo) {
100
100
  const hookPayload = {
101
101
  providerUserInfo,
102
102
  tokens,
103
103
  provider: implementation.runtimeConfigKey,
104
104
  event
105
105
  };
106
- const transformedUser = await handler.onUserInfo(hookPayload);
106
+ const transformedUser = await handler2.onUserInfo(hookPayload);
107
107
  if (transformedUser) {
108
108
  providerUserInfo = transformedUser;
109
109
  }
110
110
  }
111
111
  }
112
+ const handler = useAegisHandler();
113
+ if (handler?.onUserPersist) {
114
+ const enrichedData = await handler.onUserPersist(providerUserInfo, {
115
+ provider: implementation.runtimeConfigKey,
116
+ event
117
+ });
118
+ providerUserInfo = { ...providerUserInfo, ...enrichedData };
119
+ }
112
120
  let resolvedCustomClaims;
113
121
  if (_customClaims) {
114
122
  if (typeof _customClaims === "function") {
@@ -116,6 +124,8 @@ export function defineOAuthEventHandler(implementation, {
116
124
  } else {
117
125
  resolvedCustomClaims = _customClaims;
118
126
  }
127
+ } else if (handler?.customClaims) {
128
+ resolvedCustomClaims = await handler.customClaims(providerUserInfo);
119
129
  }
120
130
  if (_onSuccess) {
121
131
  await _onSuccess({
@@ -2,5 +2,5 @@
2
2
  * GET /api/user/me
3
3
  * Returns the current authenticated user's information
4
4
  */
5
- declare const _default: import("h3").EventHandler<import("h3").EventHandlerRequest, Promise<import("../../types/index.js").TokenPayload>>;
5
+ declare const _default: import("h3").EventHandler<import("h3").EventHandlerRequest, Promise<import("../../types/index.js").BaseTokenClaims>>;
6
6
  export default _default;
@@ -93,10 +93,23 @@ export default defineEventHandler(async (event) => {
93
93
  });
94
94
  }
95
95
  const newHashedPassword = handler.password.hashPassword ? await handler.password.hashPassword(newPassword) : await hashPassword(newPassword);
96
- await handler.password.upsertUser({
97
- ...user,
98
- hashedPassword: newHashedPassword
99
- });
96
+ if (handler.onUserPersist) {
97
+ await handler.onUserPersist(
98
+ {
99
+ ...user,
100
+ hashedPassword: newHashedPassword
101
+ },
102
+ {
103
+ provider: "password",
104
+ event
105
+ }
106
+ );
107
+ } else {
108
+ throw createError({
109
+ statusCode: 500,
110
+ message: "onUserPersist handler is required for password authentication"
111
+ });
112
+ }
100
113
  const refreshCookieName = config.nuxtAegis?.tokenRefresh?.cookie?.cookieName || "nuxt-aegis-refresh";
101
114
  const currentRefreshToken = getCookie(event, refreshCookieName);
102
115
  let currentTokenHash;
@@ -50,19 +50,27 @@ export default defineEventHandler(async (event) => {
50
50
  });
51
51
  }
52
52
  try {
53
- await handler.password.upsertUser({
53
+ let userData = {
54
54
  email,
55
55
  hashedPassword
56
- });
57
- const user = await handler.password.findUser(email);
58
- if (!user) {
59
- throw new Error("User not found after creation");
56
+ };
57
+ if (handler.onUserPersist) {
58
+ const enrichedData = await handler.onUserPersist(userData, {
59
+ provider: "password",
60
+ event
61
+ });
62
+ userData = { ...userData, ...enrichedData };
63
+ } else {
64
+ throw createError({
65
+ statusCode: 500,
66
+ message: "onUserPersist handler is required for password authentication"
67
+ });
60
68
  }
61
69
  await retrieveAndDeleteMagicCode(code);
62
70
  const providerUserInfo = {
63
- sub: user.id || user.email,
71
+ sub: userData.id || userData.email,
64
72
  provider: "password",
65
- ...user
73
+ ...userData
66
74
  };
67
75
  const authCode = generateAuthCode();
68
76
  await storeAuthCode(
@@ -60,10 +60,23 @@ export default defineEventHandler(async (event) => {
60
60
  message: "User not found"
61
61
  });
62
62
  }
63
- await handler.password.upsertUser({
64
- ...user,
65
- hashedPassword
66
- });
63
+ if (handler.onUserPersist) {
64
+ await handler.onUserPersist(
65
+ {
66
+ ...user,
67
+ hashedPassword
68
+ },
69
+ {
70
+ provider: "password",
71
+ event
72
+ }
73
+ );
74
+ } else {
75
+ throw createError({
76
+ statusCode: 500,
77
+ message: "onUserPersist handler is required for password authentication"
78
+ });
79
+ }
67
80
  return { success: true };
68
81
  } catch (error) {
69
82
  logger.error("Password reset failed", error);
@@ -7,7 +7,6 @@ import {
7
7
  revokeRefreshToken
8
8
  } from "../utils/refreshToken.js";
9
9
  import { setRefreshTokenCookie } from "../utils/cookies.js";
10
- import { processCustomClaims } from "../utils/customClaims.js";
11
10
  import { useRuntimeConfig } from "#imports";
12
11
  export default defineEventHandler(async (event) => {
13
12
  const config = useRuntimeConfig(event);
@@ -56,14 +55,7 @@ export default defineEventHandler(async (event) => {
56
55
  }
57
56
  const providerUserInfo = storedRefreshToken.providerUserInfo;
58
57
  const provider = storedRefreshToken.provider;
59
- let customClaims = {};
60
- const providerConfig = config.nuxtAegis?.providers?.[provider];
61
- if (providerConfig && "customClaims" in providerConfig) {
62
- const customClaimsConfig = providerConfig.customClaims;
63
- if (customClaimsConfig) {
64
- customClaims = await processCustomClaims(providerUserInfo, customClaimsConfig);
65
- }
66
- }
58
+ const customClaims = storedRefreshToken.customClaims || {};
67
59
  const payload = {
68
60
  sub: String(providerUserInfo.sub || providerUserInfo.email || providerUserInfo.id || ""),
69
61
  email: providerUserInfo.email,
@@ -81,6 +73,8 @@ export default defineEventHandler(async (event) => {
81
73
  tokenRefreshConfig,
82
74
  hashedRefreshToken,
83
75
  // Pass previous token hash for rotation tracking
76
+ customClaims,
77
+ // Preserve custom claims in new refresh token
84
78
  event
85
79
  );
86
80
  if (newRefreshToken) {
@@ -1,5 +1,5 @@
1
1
  import type { H3Event } from 'h3';
2
- import type { TokenPayload } from '../../types/index.js';
2
+ import type { BaseTokenClaims } from '../../types/index.js';
3
3
  /**
4
4
  * Generate authentication tokens from user data with optional custom claims
5
5
  * This is the recommended way to generate tokens after successful OAuth authentication
@@ -42,19 +42,19 @@ export declare function generateAuthTokens(event: H3Event, providerUserInfo: Rec
42
42
  * @example
43
43
  * ```typescript
44
44
  * // With custom claims typing
45
- * interface MyTokenPayload extends TokenPayload {
45
+ * type AppTokenClaims = CustomTokenClaims<{
46
46
  * role: string
47
47
  * permissions: string[]
48
- * }
48
+ * }>
49
49
  *
50
50
  * export default defineEventHandler((event) => {
51
- * const authedEvent = requireAuth<MyTokenPayload>(event)
51
+ * const authedEvent = requireAuth<AppTokenClaims>(event)
52
52
  * // TypeScript knows about role and permissions
53
53
  * return { role: authedEvent.context.user.role }
54
54
  * })
55
55
  * ```
56
56
  */
57
- export declare function requireAuth<T extends TokenPayload = TokenPayload>(event: H3Event): H3Event & {
57
+ export declare function requireAuth<T extends BaseTokenClaims = BaseTokenClaims>(event: H3Event): H3Event & {
58
58
  context: {
59
59
  user: T;
60
60
  };
@@ -79,16 +79,16 @@ export declare function requireAuth<T extends TokenPayload = TokenPayload>(event
79
79
  * @example
80
80
  * ```typescript
81
81
  * // With custom claims typing
82
- * interface MyTokenPayload extends TokenPayload {
82
+ * export type AppTokenClaims = CustomTokenClaims<{
83
83
  * role: string
84
84
  * permissions: string[]
85
- * }
85
+ * }>
86
86
  *
87
87
  * export default defineEventHandler((event) => {
88
- * const user = getAuthUser<MyTokenPayload>(event)
88
+ * const user = getAuthUser<AppTokenClaims>(event)
89
89
  * // TypeScript knows about role and permissions
90
90
  * return { role: user.role, permissions: user.permissions }
91
91
  * })
92
92
  * ```
93
93
  */
94
- export declare function getAuthUser<T extends TokenPayload = TokenPayload>(event: H3Event): T | null;
94
+ export declare function getAuthUser<T extends BaseTokenClaims = BaseTokenClaims>(event: H3Event): T | null;
@@ -25,6 +25,8 @@ export async function generateAuthTokens(event, providerUserInfo, provider, cust
25
25
  tokenRefreshConfig,
26
26
  void 0,
27
27
  // No previous token hash for initial auth
28
+ customClaims,
29
+ // Store resolved custom claims for consistent refresh
28
30
  event
29
31
  );
30
32
  return {
@@ -1,14 +1,82 @@
1
1
  import type { H3Event } from 'h3';
2
2
  import type { UserInfoHookPayload } from '../../types/hooks.js';
3
- import type { TokenPayload } from '../../types/token.js';
3
+ import type { BaseTokenClaims } from '../../types/token.js';
4
4
  import type { PasswordUser } from '../../types/providers.js';
5
+ /**
6
+ * Context passed to onUserPersist handler
7
+ */
8
+ export interface UserPersistContext {
9
+ /** Provider name (e.g., 'google', 'github', 'password') */
10
+ provider: string;
11
+ /** H3 event for server context access */
12
+ event: H3Event;
13
+ }
5
14
  export interface AegisHandler {
6
15
  /**
7
16
  * Transform user data after fetching from OAuth provider.
8
- * Replaces `nuxt-aegis:userInfo` hook.
9
17
  * Return the modified user object to use it.
10
18
  */
11
19
  onUserInfo?: (payload: UserInfoHookPayload) => Promise<Record<string, unknown> | undefined> | Record<string, unknown> | undefined;
20
+ /**
21
+ * Persist user data to your database and return enriched user information.
22
+ *
23
+ * Called for:
24
+ * - OAuth authentication: After provider data transformation, before JWT generation
25
+ * - Password authentication: After registration, password change, or reset
26
+ *
27
+ * The returned object is merged into the user data and used for JWT claims.
28
+ *
29
+ * @param user - User data to persist (provider-specific fields vary)
30
+ * @param context - Context with provider name and H3 event
31
+ * @returns Enriched user object with database fields (e.g., userId, role, permissions)
32
+ *
33
+ * @example
34
+ * ```typescript
35
+ * onUserPersist: async (user, { provider, event }) => {
36
+ * // For password provider, user includes hashedPassword
37
+ * if (provider === 'password') {
38
+ * const dbUser = await db.users.upsert({
39
+ * where: { email: user.email },
40
+ * update: { hashedPassword: user.hashedPassword },
41
+ * create: { email: user.email, hashedPassword: user.hashedPassword },
42
+ * })
43
+ * return { userId: dbUser.id, role: dbUser.role }
44
+ * }
45
+ *
46
+ * // For OAuth providers, link or create user
47
+ * const dbUser = await db.users.upsert({
48
+ * where: { email: user.email },
49
+ * update: { lastLogin: new Date() },
50
+ * create: { email: user.email, name: user.name, picture: user.picture },
51
+ * })
52
+ * return { userId: dbUser.id, role: dbUser.role, permissions: dbUser.permissions }
53
+ * }
54
+ * ```
55
+ */
56
+ onUserPersist?: (user: Record<string, unknown>, context: UserPersistContext) => Promise<Record<string, unknown>> | Record<string, unknown>;
57
+ /**
58
+ * Generate custom claims for JWT tokens.
59
+ *
60
+ * Called after onUserPersist, receives the merged user data.
61
+ * This is the recommended location for adding database-driven claims.
62
+ *
63
+ * Provider-level customClaims (defined in route handlers) take precedence over this.
64
+ *
65
+ * @param user - Complete user object including data from onUserPersist
66
+ * @returns Custom claims to add to the JWT
67
+ *
68
+ * @example
69
+ * ```typescript
70
+ * customClaims: async (user) => {
71
+ * return {
72
+ * role: user.role,
73
+ * permissions: user.permissions,
74
+ * organizationId: user.organizationId,
75
+ * }
76
+ * }
77
+ * ```
78
+ */
79
+ customClaims?: (user: Record<string, unknown>) => Record<string, unknown> | Promise<Record<string, unknown>>;
12
80
  /**
13
81
  * Password authentication handler.
14
82
  * Required if password provider is enabled.
@@ -20,11 +88,6 @@ export interface AegisHandler {
20
88
  * Return null if user is not found.
21
89
  */
22
90
  findUser: (email: string) => Promise<PasswordUser | null> | PasswordUser | null;
23
- /**
24
- * Create or update a user.
25
- * Called after successful registration or password change.
26
- */
27
- upsertUser: (user: PasswordUser) => Promise<void> | void;
28
91
  /**
29
92
  * Send a verification code to the user.
30
93
  * Called during registration, login, and password reset.
@@ -63,7 +126,7 @@ export interface AegisHandler {
63
126
  * If not defined, defaults to allowing if fetchTarget returns a user.
64
127
  * You can throw an error here to provide a specific message.
65
128
  */
66
- canImpersonate?: (requester: TokenPayload, targetId: string, event: H3Event) => Promise<boolean> | boolean;
129
+ canImpersonate?: (requester: BaseTokenClaims, targetId: string, event: H3Event) => Promise<boolean> | boolean;
67
130
  };
68
131
  }
69
132
  /**
@@ -1,5 +1,5 @@
1
1
  import type { H3Event } from 'h3';
2
- import type { TokenPayload } from '../../types/index.js';
2
+ import type { BaseTokenClaims } from '../../types/index.js';
3
3
  /**
4
4
  * Check if the requester is allowed to impersonate other users
5
5
  * @param requester - The user requesting impersonation
@@ -7,7 +7,7 @@ import type { TokenPayload } from '../../types/index.js';
7
7
  * @param event - H3 event for context
8
8
  * @throws 403 error if impersonation is not allowed
9
9
  */
10
- export declare function checkImpersonationAllowed(requester: TokenPayload, targetUserId: string, event: H3Event): Promise<void>;
10
+ export declare function checkImpersonationAllowed(requester: BaseTokenClaims, targetUserId: string, event: H3Event): Promise<void>;
11
11
  /**
12
12
  * Fetch target user data for impersonation
13
13
  * @param requester - The user requesting impersonation
@@ -17,7 +17,7 @@ export declare function checkImpersonationAllowed(requester: TokenPayload, targe
17
17
  * @throws 404 error if user not found
18
18
  * @throws 500 error if hook is not implemented
19
19
  */
20
- export declare function fetchTargetUser(requester: TokenPayload, targetUserId: string, event: H3Event): Promise<Record<string, unknown>>;
20
+ export declare function fetchTargetUser(requester: BaseTokenClaims, targetUserId: string, event: H3Event): Promise<Record<string, unknown>>;
21
21
  /**
22
22
  * Generate an impersonated JWT token
23
23
  * @param requester - The user performing impersonation
@@ -26,7 +26,7 @@ export declare function fetchTargetUser(requester: TokenPayload, targetUserId: s
26
26
  * @param _event - H3 event for context
27
27
  * @returns JWT access token (no refresh token)
28
28
  */
29
- export declare function generateImpersonatedToken(requester: TokenPayload, targetUserData: Record<string, unknown>, reason: string | undefined, _event: H3Event): Promise<string>;
29
+ export declare function generateImpersonatedToken(requester: BaseTokenClaims, targetUserData: Record<string, unknown>, reason: string | undefined, _event: H3Event): Promise<string>;
30
30
  /**
31
31
  * Start impersonation session
32
32
  * @param requester - The user requesting impersonation (must be admin)
@@ -35,14 +35,14 @@ export declare function generateImpersonatedToken(requester: TokenPayload, targe
35
35
  * @param event - H3 event for context
36
36
  * @returns Access token for impersonated session (no refresh token)
37
37
  */
38
- export declare function startImpersonation(requester: TokenPayload, targetUserId: string, reason: string | undefined, event: H3Event): Promise<string>;
38
+ export declare function startImpersonation(requester: BaseTokenClaims, targetUserId: string, reason: string | undefined, event: H3Event): Promise<string>;
39
39
  /**
40
40
  * End impersonation and restore original user session
41
41
  * @param currentToken - Current JWT token (must contain impersonation context)
42
42
  * @param event - H3 event for context
43
43
  * @returns Object with new access token and refresh token ID
44
44
  */
45
- export declare function endImpersonation(currentToken: TokenPayload, event: H3Event): Promise<{
45
+ export declare function endImpersonation(currentToken: BaseTokenClaims, event: H3Event): Promise<{
46
46
  accessToken: string;
47
47
  refreshTokenId: string;
48
48
  }>;
@@ -1,6 +1,7 @@
1
1
  import { createError } from "h3";
2
2
  import { generateToken } from "./jwt.js";
3
- import { useRuntimeConfig, useNitroApp } from "#imports";
3
+ import { useRuntimeConfig } from "#imports";
4
+ import { useNitroApp } from "nitropack/runtime";
4
5
  import { createLogger } from "./logger.js";
5
6
  import { generateAndStoreRefreshToken } from "./refreshToken.js";
6
7
  import { useAegisHandler } from "./handler.js";
@@ -226,6 +227,8 @@ export async function endImpersonation(currentToken, event) {
226
227
  refreshTokenConfig,
227
228
  void 0,
228
229
  // No previous token
230
+ customClaims,
231
+ // Store custom claims for restored session
229
232
  event
230
233
  );
231
234
  if (!refreshTokenId) {
@@ -1,4 +1,4 @@
1
- import type { TokenConfig, TokenPayload } from '../../types/index.js';
1
+ import type { TokenConfig, BaseTokenClaims } from '../../types/index.js';
2
2
  /**
3
3
  * Generate a JWT token with the given payload and custom claims
4
4
  * @param payload - Base token payload containing user information
@@ -6,7 +6,7 @@ import type { TokenConfig, TokenPayload } from '../../types/index.js';
6
6
  * @param customClaims - Optional custom claims to add to the token
7
7
  * @returns Signed JWT token
8
8
  */
9
- export declare function generateToken(payload: TokenPayload, config: TokenConfig, customClaims?: Record<string, unknown>): Promise<string>;
9
+ export declare function generateToken(payload: BaseTokenClaims, config: TokenConfig, customClaims?: Record<string, unknown>): Promise<string>;
10
10
  /**
11
11
  * Update an existing JWT token with additional claims
12
12
  * @param token - Existing JWT token to update
@@ -21,4 +21,4 @@ export declare function updateTokenWithClaims(token: string, claims: Record<stri
21
21
  * @param secret - Secret key used to sign the token
22
22
  * @returns Decoded token payload or null if verification fails
23
23
  */
24
- export declare function verifyToken(token: string, secret: string, checkExpiration?: boolean): Promise<TokenPayload | null>;
24
+ export declare function verifyToken(token: string, secret: string, checkExpiration?: boolean): Promise<BaseTokenClaims | null>;
@@ -60,10 +60,11 @@ export declare function revokeRefreshToken(tokenHash: string, event?: H3Event):
60
60
  * @param provider - Provider name (e.g., 'google', 'github', 'microsoft', 'auth0')
61
61
  * @param config - Token refresh configuration
62
62
  * @param previousTokenHash - Hash of previous refresh token for rotation tracking
63
+ * @param customClaims - Resolved custom claims from initial authentication
63
64
  * @param event - H3Event for Nitro storage access
64
65
  * @returns The generated refresh token string
65
66
  */
66
- export declare function generateAndStoreRefreshToken(providerUserInfo: Record<string, unknown>, provider: string, config: TokenRefreshConfig, previousTokenHash?: string, event?: H3Event): Promise<string | undefined>;
67
+ export declare function generateAndStoreRefreshToken(providerUserInfo: Record<string, unknown>, provider: string, config: TokenRefreshConfig, previousTokenHash?: string, customClaims?: Record<string, unknown>, event?: H3Event): Promise<string | undefined>;
67
68
  /**
68
69
  * Delete all refresh tokens for a specific user
69
70
  * Used during password change or account deletion
@@ -77,7 +77,7 @@ export async function revokeRefreshToken(tokenHash, event) {
77
77
  data.isRevoked = true;
78
78
  await storeRefreshTokenData(tokenHash, data, event);
79
79
  }
80
- export async function generateAndStoreRefreshToken(providerUserInfo, provider, config, previousTokenHash, event) {
80
+ export async function generateAndStoreRefreshToken(providerUserInfo, provider, config, previousTokenHash, customClaims, event) {
81
81
  const refreshToken = randomBytes(32).toString("base64url");
82
82
  const expiresIn = config.cookie?.maxAge || 604800;
83
83
  await storeRefreshTokenData(hashRefreshToken(refreshToken), {
@@ -87,8 +87,10 @@ export async function generateAndStoreRefreshToken(providerUserInfo, provider, c
87
87
  previousTokenHash,
88
88
  providerUserInfo,
89
89
  // Store complete OAuth provider user data
90
- provider
90
+ provider,
91
91
  // Store provider name for custom claims refresh
92
+ customClaims
93
+ // Store resolved custom claims for consistent refresh
92
94
  }, event);
93
95
  return refreshToken;
94
96
  }
@@ -1,6 +1,6 @@
1
1
  import type { NuxtAegisRuntimeConfig, RedirectConfig, LoggingConfig } from './config.js';
2
2
  import type { TokenRefreshConfig } from './refresh.js';
3
- import type { TokenPayload } from './token.js';
3
+ import type { BaseTokenClaims } from './token.js';
4
4
  import type { ClientMiddlewareConfig } from './routes.js';
5
5
  import type { SuccessHookPayload } from './hooks.js';
6
6
  /**
@@ -41,7 +41,7 @@ declare module 'h3' {
41
41
  * Authenticated user data from JWT token
42
42
  * Available when request is authenticated via the auth middleware
43
43
  */
44
- user?: TokenPayload;
44
+ user?: BaseTokenClaims;
45
45
  /**
46
46
  * Original user data before impersonation
47
47
  * Available when impersonation is active
@@ -1,5 +1,5 @@
1
1
  import type { H3Event } from 'h3';
2
- import type { TokenPayload } from './token.js';
2
+ import type { BaseTokenClaims } from './token.js';
3
3
  /**
4
4
  * Nitro hook type definitions for Nuxt Aegis
5
5
  * These hooks allow users to customize authentication behavior via server plugins
@@ -52,7 +52,7 @@ export interface SuccessHookPayload {
52
52
  */
53
53
  export interface ImpersonateCheckPayload {
54
54
  /** JWT payload of the user requesting impersonation */
55
- requester: TokenPayload;
55
+ requester: BaseTokenClaims;
56
56
  /** Target user ID to impersonate */
57
57
  targetUserId: string;
58
58
  /** Optional reason for impersonation (for audit) */
@@ -72,7 +72,7 @@ export interface ImpersonateCheckPayload {
72
72
  */
73
73
  export interface ImpersonateFetchTargetPayload {
74
74
  /** JWT payload of the user requesting impersonation */
75
- requester: TokenPayload;
75
+ requester: BaseTokenClaims;
76
76
  /** Target user ID to impersonate */
77
77
  targetUserId: string;
78
78
  /** H3 event for server context access */
@@ -84,9 +84,9 @@ export interface ImpersonateFetchTargetPayload {
84
84
  */
85
85
  export interface ImpersonateStartPayload {
86
86
  /** JWT payload of the user who initiated impersonation */
87
- requester: TokenPayload;
87
+ requester: BaseTokenClaims;
88
88
  /** JWT payload of the impersonated user */
89
- targetUser: TokenPayload;
89
+ targetUser: BaseTokenClaims;
90
90
  /** Reason for impersonation */
91
91
  reason?: string;
92
92
  /** H3 event for server context access */
@@ -104,9 +104,9 @@ export interface ImpersonateStartPayload {
104
104
  */
105
105
  export interface ImpersonateEndPayload {
106
106
  /** JWT payload of the restored original user */
107
- restoredUser: TokenPayload;
107
+ restoredUser: BaseTokenClaims;
108
108
  /** JWT payload of the user who was being impersonated */
109
- impersonatedUser: TokenPayload;
109
+ impersonatedUser: BaseTokenClaims;
110
110
  /** H3 event for server context access */
111
111
  event: H3Event;
112
112
  /** Client IP address (for audit) */
@@ -3,7 +3,7 @@
3
3
  * Main barrel file for all type exports
4
4
  */
5
5
  import './augmentations.js';
6
- export type { TokenPayload, TokenConfig, RefreshTokenData, RefreshResponse, ClaimsValidationConfig, CustomClaimsCallback, ImpersonationContext, CustomTokenClaims, ExtractClaims, } from './token.js';
6
+ export type { BaseTokenClaims, TokenConfig, RefreshTokenData, RefreshResponse, ClaimsValidationConfig, CustomClaimsCallback, ImpersonationContext, CustomTokenClaims, ExtractClaims, } from './token.js';
7
7
  export type { OAuthProviderConfig, GoogleProviderConfig, MicrosoftProviderConfig, GithubProviderConfig, Auth0ProviderConfig, MockProviderConfig, CustomProviderConfig, OAuthConfig, } from './providers.js';
8
8
  export type { CookieConfig, TokenRefreshConfig, EncryptionConfig, StorageConfig, } from './refresh.js';
9
9
  export type { AuthCodeData, TokenExchangeRequest, TokenExchangeResponse, AuthCodeConfig, } from './authCode.js';
@@ -21,11 +21,16 @@ export interface ImpersonationContext {
21
21
  originalClaims?: Record<string, unknown>;
22
22
  }
23
23
  /**
24
- * JWT Token payload interface
25
- * Represents the decoded JWT token structure with standard and custom claims
26
- * This is what gets stored in the JWT and attached to event.context.user
24
+ * Base JWT token claims interface
25
+ *
26
+ * Defines the standard JWT claims that are always present in tokens.
27
+ * This serves as the foundation for CustomTokenClaims, which extends these
28
+ * base claims with your application-specific custom claims.
29
+ *
30
+ * Use CustomTokenClaims<T> to add your own claims on top of these base claims.
31
+ * This is what gets stored in the JWT and attached to event.context.user.
27
32
  */
28
- export interface TokenPayload {
33
+ export interface BaseTokenClaims {
29
34
  /** Subject identifier (user ID) - required claim */
30
35
  sub: string;
31
36
  /** User email address */
@@ -71,6 +76,8 @@ export interface RefreshTokenData {
71
76
  providerUserInfo: Record<string, unknown>;
72
77
  /** Provider name for dynamic custom claims generation during refresh (e.g., 'google', 'github', 'microsoft', 'auth0') */
73
78
  provider: string;
79
+ /** Resolved custom claims from initial authentication - reused during token refresh to maintain consistency */
80
+ customClaims?: Record<string, unknown>;
74
81
  }
75
82
  /**
76
83
  * Response from token refresh operation
@@ -114,30 +121,39 @@ export type JSONValue = string | number | boolean | null | undefined | string[]
114
121
  /**
115
122
  * Helper type for creating custom token payloads with type safety
116
123
  *
117
- * Extends TokenPayload with custom claims while ensuring type safety.
118
- * Prevents overriding standard JWT claims and ensures all custom claims
119
- * are JSON-serializable.
124
+ * Combines BaseTokenClaims (standard JWT claims like sub, email, name) with your
125
+ * application-specific custom claims. This is the primary type you'll use when
126
+ * working with authenticated users in your application.
127
+ *
128
+ * The relationship:
129
+ * - BaseTokenClaims: Standard JWT claims (sub, email, name, iss, exp, etc.)
130
+ * - CustomTokenClaims<T>: BaseTokenClaims + your custom claims (role, permissions, etc.)
131
+ *
132
+ * Ensures type safety by preventing override of standard JWT claims and ensuring
133
+ * all custom claims are JSON-serializable.
120
134
  *
121
- * @template T - Record of custom claims to add to the token payload
135
+ * @template T - Record of custom claims to add to the base token claims
122
136
  *
123
137
  * @example
124
138
  * ```typescript
125
- * // Define your custom claims
126
- * type AppTokenPayload = CustomTokenClaims<{
139
+ * // Define your custom claims on top of base claims
140
+ * type AppTokenClaims = CustomTokenClaims<{
127
141
  * role: string
128
142
  * permissions: string[]
129
143
  * organizationId: string
130
144
  * }>
131
145
  *
132
- * // Use with useAuth
133
- * const { user } = useAuth<AppTokenPayload>()
134
- * console.log(user.value?.role) // Type-safe access
146
+ * // Use with useAuth - you get both base claims (sub, email, name)
147
+ * // and your custom claims (role, permissions, organizationId)
148
+ * const { user } = useAuth<AppTokenClaims>()
149
+ * console.log(user.value?.email) // Base claim
150
+ * console.log(user.value?.role) // Custom claim - Type-safe access
135
151
  * ```
136
152
  *
137
153
  * @example
138
154
  * ```typescript
139
155
  * // With nested objects (one level)
140
- * type AppTokenPayload = CustomTokenClaims<{
156
+ * type AppTokenClaims = CustomTokenClaims<{
141
157
  * role: string
142
158
  * metadata: {
143
159
  * tenantId: string
@@ -149,27 +165,27 @@ export type JSONValue = string | number | boolean | null | undefined | string[]
149
165
  * @warning Never include sensitive data like passwords, API keys, or secrets in JWT tokens
150
166
  * @warning Keep token payloads small (< 1KB recommended) for performance
151
167
  */
152
- export type CustomTokenClaims<T extends Record<string, JSONValue>> = TokenPayload & T;
168
+ export type CustomTokenClaims<T extends Record<string, JSONValue>> = BaseTokenClaims & T;
153
169
  /**
154
170
  * Utility type to extract only custom claims from a token payload
155
171
  *
156
- * Removes all standard TokenPayload fields, leaving only your custom claims.
172
+ * Removes all standard BaseTokenClaims fields, leaving only your custom claims.
157
173
  * Useful for type composition and claim validation.
158
174
  *
159
- * @template T - A token payload type extending TokenPayload
175
+ * @template T - A token payload type extending BaseTokenClaims
160
176
  *
161
177
  * @example
162
178
  * ```typescript
163
- * type AppTokenPayload = CustomTokenClaims<{
179
+ * type AppTokenClaims = CustomTokenClaims<{
164
180
  * role: string
165
181
  * permissions: string[]
166
182
  * }>
167
183
  *
168
- * type CustomClaims = ExtractClaims<AppTokenPayload>
184
+ * type CustomClaims = ExtractClaims<AppTokenClaims>
169
185
  * // Result: { role: string, permissions: string[] }
170
186
  * ```
171
187
  */
172
- export type ExtractClaims<T extends TokenPayload> = Omit<T, keyof TokenPayload>;
188
+ export type ExtractClaims<T extends BaseTokenClaims> = Omit<T, keyof BaseTokenClaims>;
173
189
  /**
174
190
  * Custom claims callback function
175
191
  * Receives the full OAuth provider user data and tokens
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@peterbud/nuxt-aegis",
3
- "version": "1.1.0-alpha.1",
3
+ "version": "1.1.0-alpha.2",
4
4
  "description": "Nuxt module for authentication with JWT token generation and session management.",
5
5
  "publishConfig": {
6
6
  "access": "public"